676 lines
29 KiB
PHP
676 lines
29 KiB
PHP
<?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' => '',
|
|
'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, '…' );
|
|
}
|
|
|
|
/* ===================== Что за страница сейчас ===================== */
|
|
|
|
/** Всё, что нужно для <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;
|
|
} );
|
|
|
|
/* ===================== Редиректы ===================== */
|
|
|
|
/** Правила из настроек: «/откуда/ куда» по строке, # — комментарий. Ключ — путь без крайних слешей, в нижнем регистре. */
|
|
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 ) );
|
|
?>
|
|
<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', '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' );
|
|
?>
|
|
<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><label for="sj-redirects">Редиректы 301</label></th>
|
|
<td><textarea class="large-text code" rows="6" id="sj-redirects" name="<?php echo esc_attr( "{$name}[redirects]" ); ?>" placeholder="/старый-адрес/ https://куда.ru/"><?php echo esc_textarea( $o['redirects'] ); ?></textarea>
|
|
<p class="description">По правилу в строке: путь на этом сайте, пробел, куда вести (полный адрес или путь). Строки с # — комментарии.</p></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();
|
|
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() );
|
|
} );
|
|
}
|