SEOJak 1.0.0: SEO plugin for WordPress

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alex Cube
2026-10-25 17:04:36 +03:00
co-authored by Claude Opus 5.5
commit 6793fbfc6d
5 changed files with 677 additions and 0 deletions
+630
View File
@@ -0,0 +1,630 @@
<?php
/**
* Plugin Name: SEOJak
* Description: SEO без лишнего: заголовок и описание записи, robots, canonical, Open Graph, Schema.org, карта сайта и IndexNow для Яндекса.
* Version: 1.0.0
* Author: Алексей Якубов
* Author URI: https://www.lexium.ru/about-me/
* License: GPL-2.0-or-later
* Requires at least: 6.4
* Requires PHP: 7.4
* Text Domain: seojak
*/
defined( 'ABSPATH' ) || exit;
const SEOJAK_VERSION = '1.0.0';
const SEOJAK_OPTION = 'seojak';
/* ===================== Настройки ===================== */
function seojak_defaults(): array {
return array(
'home_title' => get_bloginfo( 'name' ),
'home_description' => get_bloginfo( 'description' ),
'separator' => '|',
'person_name' => '',
'person_image' => '',
'default_image' => '',
'twitter_site' => '',
'same_as' => '',
'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, '…' );
}
/* ===================== Что за страница сейчас ===================== */
/** Всё, что нужно для <head> текущей страницы. */
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;
}
/* ===================== <head> ===================== */
// Пока активен 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 '<link rel="canonical" href="' . esc_url( $c['url'] ) . "\" />\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 '<link rel="' . $rel . '" href="' . esc_url( $link ) . "\" />\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 "<!-- SEOJak -->\n";
foreach ( $tags as list( $attr, $name, $content ) ) {
printf( "<meta %s=\"%s\" content=\"%s\" />\n", $attr, esc_attr( $name ), esc_attr( $content ) );
}
echo '<script type="application/ld+json">' . wp_json_encode( seojak_schema( $c ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) . "</script>\n";
echo "<!-- /SEOJak -->\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;
} );
// Старый адрес карты сайта и ключ IndexNow.
add_action( 'template_redirect', function () {
if ( defined( 'AIOSEO_VERSION' ) ) {
return;
}
$path = trim( (string) wp_parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ), '/' );
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 ) );
?>
<style>
#seojak label { display: block; font-weight: 600; margin: 10px 0 4px; }
#seojak input[type=text], #seojak textarea { width: 100%; }
#seojak .seojak-count { color: #646970; font-weight: 400; }
#seojak .seojak-count.is-long { color: #b32d2e; }
#seojak .seojak-snippet { margin-top: 12px; padding: 10px 12px; border: 1px solid #dcdcde; border-radius: 4px; max-width: 600px; font-family: arial, sans-serif; }
#seojak .seojak-snippet b { display: block; color: #1a0dab; font-size: 18px; font-weight: 400; }
#seojak .seojak-snippet i { display: block; color: #006621; font-style: normal; font-size: 13px; }
#seojak .seojak-snippet span { color: #4d5156; font-size: 13px; }
</style>
<label for="seojak-f-title">Заголовок для поисковиков <span class="seojak-count" data-for="seojak-f-title" data-max="60"></span></label>
<input type="text" id="seojak-f-title" name="seojak_title" value="<?php echo esc_attr( $title ); ?>" placeholder="<?php echo esc_attr( $post->post_title ); ?>">
<label for="seojak-f-desc">Описание <span class="seojak-count" data-for="seojak-f-desc" data-max="160"></span></label>
<textarea id="seojak-f-desc" name="seojak_description" rows="3" placeholder="<?php echo esc_attr( $auto ); ?>"><?php echo esc_textarea( $desc ); ?></textarea>
<label><input type="checkbox" name="seojak_noindex" value="1" <?php checked( $noindex ); ?>> Не индексировать эту страницу</label>
<div class="seojak-snippet"><b></b><i><?php echo esc_html( get_permalink( $post ) ); ?></i><span></span></div>
<script>
( function () {
var box = document.getElementById( 'seojak' ), t = box.querySelector( '#seojak-f-title' ), d = box.querySelector( '#seojak-f-desc' ),
post = document.getElementById( 'title' );
function val( el ) { return el.value.trim() || el.placeholder; }
function update() {
box.querySelector( '.seojak-snippet b' ).textContent = t.value.trim() || ( post && post.value ) || t.placeholder;
box.querySelector( '.seojak-snippet span' ).textContent = val( d );
box.querySelectorAll( '.seojak-count' ).forEach( function ( c ) {
var el = document.getElementById( c.dataset.for ), n = ( el.value.trim() || ( el === t && post ? post.value : el.placeholder ) ).length;
c.textContent = n + ' из ~' + c.dataset.max;
c.classList.toggle( 'is-long', n > +c.dataset.max );
} );
}
[ t, d, post ].forEach( function ( el ) { el && el.addEventListener( 'input', update ); } );
update();
}() );
</script>
<?php
}
add_action( 'save_post', function ( $post_id ) {
if ( ! isset( $_POST['seojak_nonce'] ) || ! wp_verify_nonce( $_POST['seojak_nonce'], 'seojak_save' )
|| ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$fields = array(
'_seojak_title' => 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' ), 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' );
?>
<div class="wrap">
<h1><img src="<?php echo esc_url( plugins_url( 'assets/icon.svg', __FILE__ ) ); ?>" alt="" width="32" height="32" style="vertical-align: middle; margin-right: 8px;">SEOJak</h1>
<form method="post" action="options.php">
<?php settings_fields( 'seojak' ); ?>
<table class="form-table" role="presentation">
<?php foreach ( $text as $key => $label ) : ?>
<tr><th><label for="sj-<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></label></th>
<td><input type="text" class="regular-text" id="sj-<?php echo esc_attr( $key ); ?>" name="<?php echo esc_attr( "{$name}[{$key}]" ); ?>" value="<?php echo esc_attr( $o[ $key ] ); ?>"></td></tr>
<?php if ( 'home_title' === $key ) : ?>
<tr><th><label for="sj-home_description">Описание главной</label></th>
<td><textarea class="large-text" rows="3" id="sj-home_description" name="<?php echo esc_attr( "{$name}[home_description]" ); ?>"><?php echo esc_textarea( $o['home_description'] ); ?></textarea></td></tr>
<?php endif; ?>
<?php endforeach; ?>
<tr><th><label for="sj-same_as">Профили автора в соцсетях</label></th>
<td><textarea class="large-text" rows="3" id="sj-same_as" name="<?php echo esc_attr( "{$name}[same_as]" ); ?>" placeholder="По одному адресу в строке"><?php echo esc_textarea( $o['same_as'] ); ?></textarea></td></tr>
<tr><th>Индексация</th><td>
<?php foreach ( $checks as $key => $label ) : ?>
<label><input type="checkbox" name="<?php echo esc_attr( "{$name}[{$key}]" ); ?>" value="1" <?php checked( $o[ $key ] ); ?>> <?php echo esc_html( $label ); ?></label><br>
<?php endforeach; ?>
<?php if ( $last ) : ?>
<p class="description">Последний IndexNow: <?php echo esc_html( wp_date( 'd.m.Y H:i', $last['time'] ) . ' — ' . $last['url'] . ' — ответ ' . $last['result'] ); ?></p>
<?php endif; ?>
</td></tr>
</table>
<?php submit_button(); ?>
</form>
<p>Карта сайта: <a href="<?php echo esc_url( home_url( '/wp-sitemap.xml' ) ); ?>"><?php echo esc_html( home_url( '/wp-sitemap.xml' ) ); ?></a></p>
</div>
<?php
}
/* ===================== Перенос из All in One SEO ===================== */
/**
* Копирует заголовки, описания и noindex из таблицы aioseo_posts в поля SEOJak.
* Заполненные поля SEOJak не перезаписывает. Возвращает число перенесённых записей.
*/
function seojak_import_aioseo(): int {
global $wpdb;
$table = $wpdb->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();
} );
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'seojak import-aioseo', function () {
WP_CLI::success( 'Перенесено записей: ' . seojak_import_aioseo() );
} );
}