get_bloginfo( 'name' ), 'home_description' => get_bloginfo( 'description' ), 'separator' => '|', 'person_name' => '', 'person_image' => '', 'default_image' => '', 'twitter_site' => '', 'same_as' => '', 'redirects' => '', 'noindex_category' => 1, 'noindex_tag' => 1, 'noindex_author' => 1, 'noindex_date' => 1, 'indexnow' => 1, 'indexnow_key' => '', ); } function seojak_opt( string $key ) { static $opts = null; if ( null === $opts ) { $opts = wp_parse_args( (array) get_option( SEOJAK_OPTION, array() ), seojak_defaults() ); } return $opts[ $key ] ?? null; } /* ===================== Данные записи ===================== */ const SEOJAK_META = array( '_seojak_title' => 'string', '_seojak_description' => 'string', '_seojak_noindex' => 'boolean', ); add_action( 'init', function () { foreach ( array( 'post', 'page' ) as $type ) { foreach ( SEOJAK_META as $key => $kind ) { register_post_meta( $type, $key, array( 'type' => $kind, 'single' => true, 'show_in_rest' => true, 'auth_callback' => function ( $allowed, $meta_key, $post_id ) { return current_user_can( 'edit_post', $post_id ); }, ) ); } } } ); function seojak_post_description( WP_Post $post ): string { $custom = trim( (string) get_post_meta( $post->ID, '_seojak_description', true ) ); if ( '' !== $custom ) { return $custom; } $text = '' !== trim( $post->post_excerpt ) ? $post->post_excerpt : strip_shortcodes( $post->post_content ); if ( function_exists( 'trim_text' ) ) { return trim_text( $text, 160 ); } return wp_trim_words( wp_strip_all_tags( $text ), 25, '…' ); } /* ===================== Что за страница сейчас ===================== */ /** Всё, что нужно для текущей страницы. */ function seojak_context(): array { static $ctx = null; if ( null !== $ctx ) { return $ctx; } $sep = seojak_opt( 'separator' ); $site = get_bloginfo( 'name' ); $ctx = array( 'title' => '', 'description' => '', 'url' => '', 'type' => 'website', 'image' => seojak_opt( 'default_image' ), 'noindex' => false, 'post' => null, ); if ( is_front_page() || is_home() ) { $ctx['title'] = seojak_opt( 'home_title' ); $ctx['description'] = seojak_opt( 'home_description' ); $ctx['url'] = home_url( '/' ); } elseif ( is_singular() ) { $post = get_queried_object(); $custom = trim( (string) get_post_meta( $post->ID, '_seojak_title', true ) ); $ctx['post'] = $post; $ctx['title'] = '' !== $custom ? $custom : single_post_title( '', false ); $ctx['description'] = seojak_post_description( $post ); $ctx['url'] = get_permalink( $post ); $ctx['type'] = 'article'; $ctx['noindex'] = (bool) get_post_meta( $post->ID, '_seojak_noindex', true ); if ( has_post_thumbnail( $post ) ) { $ctx['image'] = get_the_post_thumbnail_url( $post, 'large' ); } } elseif ( is_category() || is_tag() || is_tax() ) { $term = get_queried_object(); $ctx['name'] = $term->name; $ctx['description'] = wp_strip_all_tags( term_description( $term ) ); $ctx['url'] = get_term_link( $term ); $ctx['noindex'] = ( is_category() && seojak_opt( 'noindex_category' ) ) || ( is_tag() && seojak_opt( 'noindex_tag' ) ); } elseif ( is_author() ) { $ctx['name'] = get_the_author_meta( 'display_name', get_queried_object_id() ); $ctx['url'] = get_author_posts_url( get_queried_object_id() ); $ctx['noindex'] = (bool) seojak_opt( 'noindex_author' ); } elseif ( is_date() ) { $ctx['name'] = wp_strip_all_tags( get_the_archive_title() ); $ctx['noindex'] = (bool) seojak_opt( 'noindex_date' ); $ctx['url'] = is_year() ? get_year_link( get_query_var( 'year' ) ) : ( is_month() ? get_month_link( get_query_var( 'year' ), get_query_var( 'monthnum' ) ) : get_day_link( get_query_var( 'year' ), get_query_var( 'monthnum' ), get_query_var( 'day' ) ) ); } elseif ( is_search() ) { $ctx['name'] = get_search_query(); $ctx['noindex'] = true; } if ( isset( $ctx['name'] ) ) { $ctx['title'] = $ctx['name'] . " $sep $site"; } // Вторая и дальше страницы списков: свой адрес, без индексации, ссылки по ним — да. $paged = (int) get_query_var( 'paged' ); if ( $paged > 1 && '' !== $ctx['title'] ) { $ctx['noindex'] = true; if ( $ctx['url'] ) { $ctx['url'] = get_pagenum_link( $paged ); } $ctx['title'] .= " $sep Страница $paged"; } if ( ! $ctx['image'] && seojak_opt( 'person_image' ) ) { $ctx['image'] = seojak_opt( 'person_image' ); } return $ctx; } /* ===================== ===================== */ // Пока активен All in One SEO, фронтенд не трогаем: два плагина — два набора тегов. function seojak_front_enabled(): bool { return ! defined( 'AIOSEO_VERSION' ) && ! is_admin() && ! is_feed(); } add_filter( 'pre_get_document_title', function ( $title ) { if ( ! seojak_front_enabled() ) { return $title; } $t = seojak_context()['title']; return '' !== $t ? $t : $title; }, 20 ); add_filter( 'wp_robots', function ( array $robots ) { if ( seojak_front_enabled() && seojak_context()['noindex'] ) { $robots['noindex'] = true; $robots['follow'] = true; } return $robots; } ); add_action( 'wp', function () { if ( seojak_front_enabled() ) { remove_action( 'wp_head', 'rel_canonical' ); // свой canonical — для всех страниц, а не только записей } } ); add_action( 'wp_head', function () { if ( ! seojak_front_enabled() || is_404() ) { return; } $c = seojak_context(); $tags = array(); if ( $c['description'] ) { $tags[] = array( 'name', 'description', $c['description'] ); } if ( $c['url'] && ! $c['noindex'] ) { echo '\n"; } foreach ( array( 'prev' => get_previous_posts_page_link(), 'next' => get_next_posts_page_link() ) as $rel => $link ) { if ( ! is_singular() && $link && ( 'prev' === $rel ? get_query_var( 'paged' ) > 1 : max( 1, get_query_var( 'paged' ) ) < $GLOBALS['wp_query']->max_num_pages ) ) { echo '\n"; } } $tags[] = array( 'property', 'og:locale', get_locale() ); $tags[] = array( 'property', 'og:site_name', get_bloginfo( 'name' ) ); $tags[] = array( 'property', 'og:type', $c['type'] ); $tags[] = array( 'property', 'og:title', $c['title'] ); if ( $c['description'] ) { $tags[] = array( 'property', 'og:description', $c['description'] ); } if ( $c['url'] ) { $tags[] = array( 'property', 'og:url', $c['url'] ); } if ( $c['image'] ) { $tags[] = array( 'property', 'og:image', $c['image'] ); } if ( $c['post'] && 'article' === $c['type'] ) { $tags[] = array( 'property', 'article:published_time', get_post_time( 'c', true, $c['post'] ) ); $tags[] = array( 'property', 'article:modified_time', get_post_modified_time( 'c', true, $c['post'] ) ); } $tags[] = array( 'name', 'twitter:card', $c['image'] ? 'summary_large_image' : 'summary' ); if ( seojak_opt( 'twitter_site' ) ) { $tags[] = array( 'name', 'twitter:site', seojak_opt( 'twitter_site' ) ); } $tags[] = array( 'name', 'twitter:title', $c['title'] ); if ( $c['description'] ) { $tags[] = array( 'name', 'twitter:description', $c['description'] ); } if ( $c['image'] ) { $tags[] = array( 'name', 'twitter:image', $c['image'] ); } echo "\n"; foreach ( $tags as list( $attr, $name, $content ) ) { printf( "\n", $attr, esc_attr( $name ), esc_attr( $content ) ); } echo '\n"; echo "\n"; }, 1 ); /** Разметка Schema.org: сайт, автор, страница, статья, хлебные крошки. */ function seojak_schema( array $c ): array { $home = home_url( '/' ); $person = array( '@type' => 'Person', '@id' => $home . '#person', 'name' => seojak_opt( 'person_name' ) ?: get_bloginfo( 'name' ), 'url' => $home, ); if ( seojak_opt( 'person_image' ) ) { $person['image'] = seojak_opt( 'person_image' ); } $same = array_values( array_filter( array_map( 'trim', explode( "\n", (string) seojak_opt( 'same_as' ) ) ) ) ); if ( $same ) { $person['sameAs'] = $same; } $graph = array( array( '@type' => 'WebSite', '@id' => $home . '#website', 'url' => $home, 'name' => get_bloginfo( 'name' ), 'inLanguage' => get_bloginfo( 'language' ), 'publisher' => array( '@id' => $home . '#person' ), ), $person, ); if ( ! $c['url'] ) { return array( '@context' => 'https://schema.org', '@graph' => $graph ); } $page = array( '@type' => 'WebPage', '@id' => $c['url'] . '#webpage', 'url' => $c['url'], 'name' => $c['title'], 'isPartOf' => array( '@id' => $home . '#website' ), 'inLanguage' => get_bloginfo( 'language' ), ); if ( $c['description'] ) { $page['description'] = $c['description']; } $crumbs = array( array( 'Главная', $home ) ); if ( $c['post'] && 'post' === $c['post']->post_type ) { $cat = get_the_category( $c['post']->ID ); if ( $cat ) { foreach ( array_reverse( get_ancestors( $cat[0]->term_id, 'category' ) ) as $parent ) { $crumbs[] = array( get_cat_name( $parent ), get_category_link( $parent ) ); } $crumbs[] = array( $cat[0]->name, get_category_link( $cat[0] ) ); } } if ( ! is_front_page() && ! is_home() ) { $crumbs[] = array( $c['post'] ? get_the_title( $c['post'] ) : ( $c['name'] ?? $c['title'] ), $c['url'] ); $items = array(); foreach ( $crumbs as $i => list( $name, $url ) ) { $items[] = array( '@type' => 'ListItem', 'position' => $i + 1, 'name' => wp_strip_all_tags( $name ), 'item' => $url ); } $graph[] = array( '@type' => 'BreadcrumbList', '@id' => $c['url'] . '#breadcrumbs', 'itemListElement' => $items ); $page['breadcrumb'] = array( '@id' => $c['url'] . '#breadcrumbs' ); } $graph[] = $page; if ( $c['post'] && 'post' === $c['post']->post_type ) { $article = array( '@type' => 'BlogPosting', '@id' => $c['url'] . '#article', 'headline' => wp_strip_all_tags( get_the_title( $c['post'] ) ), 'datePublished' => get_post_time( 'c', true, $c['post'] ), 'dateModified' => get_post_modified_time( 'c', true, $c['post'] ), 'author' => array( '@id' => $home . '#person' ), 'publisher' => array( '@id' => $home . '#person' ), 'mainEntityOfPage' => array( '@id' => $c['url'] . '#webpage' ), 'inLanguage' => get_bloginfo( 'language' ), ); if ( $c['image'] ) { $article['image'] = $c['image']; } if ( $c['description'] ) { $article['description'] = $c['description']; } $graph[] = $article; } return array( '@context' => 'https://schema.org', '@graph' => $graph ); } /* ===================== Карта сайта (встроенная в WordPress) ===================== */ add_filter( 'wp_sitemaps_post_types', function ( $types ) { return array_intersect_key( $types, array_flip( array( 'post', 'page' ) ) ); } ); // Рубрики — в карту, только если открыты для индексации; метки не нужны. add_filter( 'wp_sitemaps_taxonomies', function ( $taxonomies ) { return seojak_opt( 'noindex_category' ) ? array() : array_intersect_key( $taxonomies, array( 'category' => 1 ) ); } ); add_filter( 'wp_sitemaps_add_provider', function ( $provider, $name ) { return 'users' === $name ? false : $provider; }, 10, 2 ); add_filter( 'wp_sitemaps_posts_query_args', function ( $args ) { $args['meta_query'] = array( 'relation' => 'OR', array( 'key' => '_seojak_noindex', 'compare' => 'NOT EXISTS' ), array( 'key' => '_seojak_noindex', 'value' => '1', 'compare' => '!=' ), ); return $args; } ); /* ===================== Редиректы ===================== */ /** Правила из настроек: «/откуда/ куда» по строке, # — комментарий. Ключ — путь без крайних слешей, в нижнем регистре. */ function seojak_redirects(): array { $rules = array(); foreach ( explode( "\n", (string) seojak_opt( 'redirects' ) ) as $line ) { $parts = preg_split( '/\s+/', trim( $line ) ); if ( 2 === count( $parts ) && '#' !== $parts[0][0] ) { $rules[ mb_strtolower( trim( rawurldecode( $parts[0] ), '/' ) ) ] = $parts[1]; } } return $rules; } // Старый адрес карты сайта, ключ IndexNow и свои редиректы. add_action( 'template_redirect', function () { $path = trim( rawurldecode( (string) wp_parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ) ), '/' ); $to = seojak_redirects()[ mb_strtolower( $path ) ] ?? ''; if ( $to ) { // Внешний адрес — как есть (партнёрские ссылки), свой путь — от корня сайта. wp_redirect( preg_match( '#^https?://#i', $to ) ? $to : home_url( '/' . ltrim( $to, '/' ) ), 301, 'SEOJak' ); // phpcs:ignore WordPress.Security.SafeRedirect -- адреса задаёт админ exit; } if ( defined( 'AIOSEO_VERSION' ) ) { return; } if ( in_array( $path, array( 'sitemap.xml', 'sitemap_index.xml', 'post-sitemap.xml', 'page-sitemap.xml' ), true ) ) { wp_safe_redirect( home_url( '/wp-sitemap.xml' ), 301 ); exit; } $key = seojak_indexnow_key(); if ( $key && $path === $key . '.txt' ) { status_header( 200 ); header( 'Content-Type: text/plain; charset=utf-8' ); echo $key; // phpcs:ignore WordPress.Security.EscapeOutput -- ключ из [a-f0-9] exit; } }, 0 ); /* ===================== IndexNow ===================== */ function seojak_indexnow_key(): string { if ( ! seojak_opt( 'indexnow' ) ) { return ''; } $key = (string) seojak_opt( 'indexnow_key' ); if ( ! preg_match( '/^[a-f0-9]{32}$/', $key ) ) { $key = md5( wp_generate_password( 32, true, true ) ); $opts = (array) get_option( SEOJAK_OPTION, array() ); $opts['indexnow_key'] = $key; update_option( SEOJAK_OPTION, $opts ); } return $key; } /** Сообщить Яндексу (и всем участникам IndexNow), что адрес изменился. */ function seojak_indexnow_ping( string $url ) { $key = seojak_indexnow_key(); if ( ! $key || 'production' !== wp_get_environment_type() && ! defined( 'SEOJAK_INDEXNOW_FORCE' ) ) { return; } $response = wp_remote_post( 'https://yandex.com/indexnow', array( 'timeout' => 5, 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), 'body' => wp_json_encode( array( 'host' => wp_parse_url( home_url(), PHP_URL_HOST ), 'key' => $key, 'keyLocation' => home_url( "/$key.txt" ), 'urlList' => array( $url ), ) ), ) ); $code = is_wp_error( $response ) ? $response->get_error_message() : wp_remote_retrieve_response_code( $response ); update_option( 'seojak_indexnow_last', array( 'time' => time(), 'url' => $url, 'result' => $code ), false ); } add_action( 'transition_post_status', function ( $new, $old, $post ) { if ( 'publish' !== $new || ! in_array( $post->post_type, array( 'post', 'page' ), true ) || wp_is_post_revision( $post ) ) { return; } // Не чаще раза в 10 минут на запись: «Обновить» жмут по нескольку раз подряд. if ( get_transient( 'seojak_in_' . $post->ID ) ) { return; } set_transient( 'seojak_in_' . $post->ID, 1, 10 * MINUTE_IN_SECONDS ); // После сохранения, чтобы адрес был уже окончательным. add_action( 'shutdown', function () use ( $post ) { if ( ! get_post_meta( $post->ID, '_seojak_noindex', true ) ) { seojak_indexnow_ping( get_permalink( $post->ID ) ); } } ); }, 10, 3 ); /* ===================== Блок в редакторе записи ===================== */ add_action( 'add_meta_boxes', function () { add_meta_box( 'seojak', 'SEO', 'seojak_meta_box', array( 'post', 'page' ), 'normal', 'high' ); } ); function seojak_meta_box( WP_Post $post ) { wp_nonce_field( 'seojak_save', 'seojak_nonce' ); $title = get_post_meta( $post->ID, '_seojak_title', true ); $desc = get_post_meta( $post->ID, '_seojak_description', true ); $noindex = get_post_meta( $post->ID, '_seojak_noindex', true ); $auto = seojak_post_description( get_post( $post->ID ) ); ?>
sanitize_text_field( wp_unslash( $_POST['seojak_title'] ?? '' ) ), '_seojak_description' => sanitize_textarea_field( wp_unslash( $_POST['seojak_description'] ?? '' ) ), '_seojak_noindex' => empty( $_POST['seojak_noindex'] ) ? '' : '1', ); foreach ( $fields as $key => $value ) { if ( '' === $value ) { delete_post_meta( $post_id, $key ); } else { update_post_meta( $post_id, $key, $value ); } } } ); /* ===================== Страница настроек ===================== */ add_action( 'admin_menu', function () { add_options_page( 'SEOJak', 'SEOJak', 'manage_options', 'seojak', 'seojak_settings_page' ); } ); add_action( 'admin_init', function () { register_setting( 'seojak', SEOJAK_OPTION, array( 'sanitize_callback' => function ( $in ) { $out = array(); $prev = (array) get_option( SEOJAK_OPTION, array() ); foreach ( seojak_defaults() as $key => $default ) { if ( is_int( $default ) ) { $out[ $key ] = empty( $in[ $key ] ) ? 0 : 1; } elseif ( in_array( $key, array( 'person_image', 'default_image' ), true ) ) { $out[ $key ] = esc_url_raw( $in[ $key ] ?? '' ); } elseif ( in_array( $key, array( 'home_description', 'same_as', 'redirects' ), true ) ) { $out[ $key ] = sanitize_textarea_field( $in[ $key ] ?? '' ); } else { $out[ $key ] = sanitize_text_field( $in[ $key ] ?? '' ); } } $out['indexnow_key'] = $prev['indexnow_key'] ?? ''; return $out; }, ) ); } ); function seojak_settings_page() { $o = wp_parse_args( (array) get_option( SEOJAK_OPTION, array() ), seojak_defaults() ); $name = SEOJAK_OPTION; $text = array( 'home_title' => 'Заголовок главной', 'separator' => 'Разделитель в заголовках', 'person_name' => 'Автор (Schema.org)', 'person_image' => 'Фото или логотип автора, URL', 'default_image' => 'Картинка для соцсетей по умолчанию, URL', 'twitter_site' => 'Twitter/X, @имя', ); $checks = array( 'noindex_category' => 'Не индексировать рубрики', 'noindex_tag' => 'Не индексировать метки', 'noindex_author' => 'Не индексировать страницы авторов', 'noindex_date' => 'Не индексировать архивы по датам', 'indexnow' => 'Сообщать Яндексу о новых и обновлённых записях (IndexNow)', ); $last = get_option( 'seojak_indexnow_last' ); ?>

SEOJak

$label ) : ?>

Карта сайта:

prefix . 'aioseo_posts'; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) { return 0; } $rows = $wpdb->get_results( "SELECT post_id, title, description, robots_noindex, robots_default FROM `$table` WHERE (title <> '' AND title IS NOT NULL) OR (description <> '' AND description IS NOT NULL) OR (robots_noindex = 1 AND robots_default = 0)" ); // phpcs:ignore WordPress.DB.PreparedSQL -- имя таблицы с префиксом $count = 0; foreach ( $rows as $r ) { if ( ! get_post( $r->post_id ) ) { continue; } $done = false; foreach ( array( '_seojak_title' => trim( (string) $r->title ), '_seojak_description' => trim( (string) $r->description ) ) as $key => $value ) { if ( '' !== $value && '' === (string) get_post_meta( $r->post_id, $key, true ) ) { update_post_meta( $r->post_id, $key, $value ); $done = true; } } if ( 1 === (int) $r->robots_noindex && 0 === (int) $r->robots_default ) { update_post_meta( $r->post_id, '_seojak_noindex', '1' ); $done = true; } $count += (int) $done; } return $count; } register_activation_hook( __FILE__, function () { if ( ! get_option( SEOJAK_OPTION ) ) { $opts = seojak_defaults(); // Настройки главной и соцсетей — из All in One SEO, если он стоял. $aio = json_decode( (string) get_option( 'aioseo_options' ), true ); if ( is_array( $aio ) ) { $g = $aio['searchAppearance']['global'] ?? array(); $s = $aio['searchAppearance']['global']['schema'] ?? array(); $opts['home_title'] = $g['siteTitle'] ?? $opts['home_title']; $opts['home_description'] = $g['metaDescription'] ?? $opts['home_description']; $opts['person_name'] = $s['personName'] ?? ''; $opts['person_image'] = $s['personLogo'] ?? ''; $opts['default_image'] = $aio['social']['facebook']['general']['defaultImagePosts'] ?? ''; $tw = $aio['social']['profiles']['urls']['twitterUrl'] ?? ''; $opts['twitter_site'] = $tw ? '@' . basename( untrailingslashit( $tw ) ) : ''; $opts['same_as'] = implode( "\n", array_filter( array_map( 'strval', $aio['social']['profiles']['urls'] ?? array() ) ) ); } add_option( SEOJAK_OPTION, $opts ); } seojak_import_aioseo(); seojak_import_redirects(); } ); /** Дописывает к своим редиректам правила Simple 301 Redirects, которых ещё нет. Возвращает число добавленных. */ function seojak_import_redirects(): int { $have = seojak_redirects(); $lines = array(); foreach ( (array) get_option( '301_redirects', array() ) as $from => $to ) { if ( $to && ! isset( $have[ mb_strtolower( trim( rawurldecode( $from ), '/' ) ) ] ) ) { $lines[] = "$from $to"; } } if ( $lines ) { $opts = (array) get_option( SEOJAK_OPTION, array() ); $opts['redirects'] = trim( ( $opts['redirects'] ?? '' ) . "\n" . implode( "\n", $lines ) ); update_option( SEOJAK_OPTION, $opts ); } return count( $lines ); } if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'seojak import-aioseo', function () { WP_CLI::success( 'Перенесено записей: ' . seojak_import_aioseo() ); } ); WP_CLI::add_command( 'seojak import-redirects', function () { WP_CLI::success( 'Добавлено редиректов из Simple 301 Redirects: ' . seojak_import_redirects() ); } ); }