SEO мета-теги без плагінів: title, description, robots, Open Graph
Є багато плагінів для SEO, але майже всі з них головне завдання – це створити SEO мета-теги (метадані) для сторінки: title, description, robots, Open Graph. Іноді є сенс не підключати подібні плагіни, а використовувати свій невеликий код – з ним простіше керуватися і вирішувати нестандартні завдання… У цій статті я поділяюсь своїм «велосипедом» у цій галузі.
Код нижче був написаний вже давно для статті: Порівняння СЕО плагінів platinum SEO Pack і All in One SEO Pack . І, незважаючи на те, що є плагіни, раз у раз з’являється необхідність користуватися цим кодом. Він використовується іноді як альтернатива, і судячи з коментарів це буває потрібно не тільки мені. Тому було ухвалено стратегічне рішення винести цей код в окрему статтю та підтримувати його вже тут.
Одна з переваг цього коду над плагінами – зрозумілість та максимальна простота. Все робиться через хуки і тут і в плагінах, тільки знайти потрібних хук у плагіні зазвичай займає більше часу. Крім того, ви можете переписати частину коду під своє конкретне завдання.
Якщо ви не розумієтеся на PHP, то настійно рекомендую використовувати SEO плагіни – це набагато зручніше! Втім, іноді плагіни зручніші і для тих, хто знає PHP, я припустимо не завжди юзаю цей код, і плагінами на кшталт SEO Yoast не гидую.
Код виведення title, description, robots, Open Graph, twitter
Базове завдання SEO плагінів – це дати можливість створювати чотири метаполі для сторінки: title, description, robots та keywords. А також створення додаткових Open Graph даних.
Код, який все це реалізує (див. коментарі у коді):
<?php
/**
* Simple SEO class for WordPress для створення page metatags:
* title, description, robots, keywords, Open Graph.
*
* IMPORTANT! Since version 1.7.0 robots code logic chenged. Changed your code after update!
* IMPORTANT! Since version 1.8.0 title code logic chenged. Changed your code after update!
*
* @see https://github.com/doiftrue/Kama_SEO_Tags
*
* @author Kama
*
* @version 1.9.11
*/
class Kama_SEO_Tags {
static function init(){
// force WP document_title function to run
add_theme_support( 'title-tag');
add_filter( 'pre_get_document_title', [__CLASS__, 'meta_title'], 1);
add_action( 'wp_head', [ __CLASS__, 'meta_description' ], 1 );
add_action( 'wp_head', [ __CLASS__, 'meta_keywords' ], 1 );
add_action( 'wp_head', [__CLASS__, 'og_meta'], 1); // Open Graph, twitter дані
// WP 5.7+
add_filter( 'wp_robots', [__CLASS__, 'wp_robots_callback'], 11);
}
/**
* Open Graph, twitter data в `<head>`.
*
* @See Documentation: http://ogp.me/
*/
static function og_meta(){
$obj = get_queried_object();
if( isset( $obj->post_type ) ) $post = $obj;
elseif( isset( $obj->term_id ) ) $term = $obj;
$ is_post = isset ($ post);
$ is_term = isset ($ term);
$title = self::meta_title();
$desc = preg_replace( '/^.+content="([^"]*)".*$/s', '$1', self::meta_description() );
// Open Graph
$els = [];
$els['og:locale'] = get_locale();
$els['og:site_name'] = get_bloginfo('name');
$els['og:title'] = $title;
$els['og:description'] = $desc;
$els['og:type'] = is_singular() ? 'article': 'object';
// og:url
if( 'url' ){
if( $is_post ) $url = get_permalink( $post );
if( $is_term ) $url = get_term_link( $term );
if( ! empty( $url ) ){
$els['og:url'] = $url;
// relative (not allowed)
if( '/' === $url[0] ){
// без протоколу тільки: //domain.com/path
if( substr( $url, 0, 2 ) === '//' ){
$els['og:url'] = set_url_scheme($url);
}
// без вдома
else {
$parts = wp_parse_url($url);
$els['og:url'] = home_url( $parts['path'] ) . ( isset( $parts['query'] ) ? "?{$parts['query']}" : '' );
}
}
}
}
/**
* Allow to disable `article:section` property.
*
* @param bool $is_on
*/
if( apply_filters( 'kama_og_meta_show_article_section', true ) && is_singular() ){
$post_taxname = get_object_taxonomies( $post->post_type );
if( $post_taxname ){
$post_terms = get_the_terms( $post, reset( $post_taxname ) );
if( $post_terms && $post_term = array_shift( $post_terms ) ){
$els['article:section'] = $post_term->name;
}
}
}
// og:image
if( 'image' ){
/**
* Allow to change `og:image` `og:image:width` `og:image:height` values.
*
* @param int|string|array|WP_Post $image_data WP attachment ID or Image URL or Array [ image_url, width, height ].
*/
$image = apply_filters( 'pre_kama_og_meta_image', null );
if(! $image) {
$attach_id_from_text__fn = static function( $text ){
if(
preg_match( '/<img +src *= *['"]([^'"]+)['"]/', $text, $mm )
&&
( '/' === $mm[1][0] || strpos($mm[1], $_SERVER['HTTP_HOST']) )
) {
$ name = basename ($ mm [1]);
$name = preg_replace( '~-[0-9]+x[0-9]+(?=..{2,6})~', '', $name ); // Видалимо розмір (-80x80)
$name = preg_replace( '~.[^.]+$~', '', $name ); // видалимо розширення
$name = sanitize_title( sanitize_file_name( $name ) );
Global $wpdb;
$attach_id = $wpdb->get_var( $wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE post_name = %s AND post_type = 'attachment'", $name
)));
return (int) $attach_id;
}
return 0;
};
if($is_post) {
$image = get_post_thumbnail_id($post);
if(! $image) {
/**
* Натисніть на turn off the image search in post content.
*
* @param bool $is_on
*/
if( apply_filters( 'kama_og_meta_thumb_id_find_in_content', true ) ){
$image = $attach_id_from_text__fn( $post->post_content );
// Перше вкладення посту
if(! $image) {
$ attach = get_children ([
'numberposts' => 1,
'post_mime_type' => 'image',
'post_type' => 'attachment',
'post_parent' => $post->ID,
]);
if( $attach && $attach = array_shift( $attach ) ){
$image = $attach->ID;
}
}
}
}
}
elseif( $is_term ){
$ image = get_term_meta ($ term-> term_id, '_thumbnail_id', 1);
if(! $image)
$image = $attach_id_from_text__fn( $term->description );
}
/**
* Allow to set `og:image` `og:image:width` `og:image:height` values if it's not.
*
* @param int|string|array|WP_Post $image WP attachment ID or Image URL or [ image_url, width, height ] array.
*/
$image = apply_filters( 'kama_og_meta_image', $image );
$image = apply_filters( 'kama_og_meta_thumb_id', $image ); // backcompat
}
if($image) {
if(
$image instanceof WP_Post
||
( is_numeric( $image ) && $image = get_post( $image ) )
) {
// full size
[
$els['og:image[1]'],
$els['og:image[1]:width'],
$els['og:image[1]:height'],
$els['og:image[1]:alt'],
$els['og:image[1]:type']
] = array_merge(
array_slice( image_downsize( $image->ID, 'full' ), 0, 3 ),
[ $image->post_excerpt, $image->post_mime_type ]
);
if( ! $els['og:image[1]:alt'] ){
unset( $els['og:image[1]:alt'] );
}
// thumbnail size
[
$els['og:image[2]'],
$els['og:image[2]:width'],
$els['og:image[2]:height']
] = array_slice( image_downsize( $image->ID, 'thumbnail' ), 0, 3 );
}
elseif( is_array( $image ) ){
[
$els['og:image[1]'],
$els['og:image[1]:width'],
$els['og:image[1]:height']
] = $ image;
}
else {
$els['og:image[1]'] = $image;
}
}
}
// twitter
$els['twitter:card'] = 'summary';
$els['twitter:title'] = $els['og:title'];
$els['twitter:description'] = $els['og:description'];
if( ! empty( $els['og:image[1]'] ) ){
$els['twitter:image'] = $els['og:image[1]'];
}
/**
* Застосовують зміна величини ог / twitter meta properties.
*
* @param array $els
*/
$els = apply_filters( 'kama_og_meta_elements_values', $els );
$els = array_filter( $els );
ksort($els);
// make <meta> tags
$ Metas = [];
foreach( $els as $key => $val ){
// og:image[1] > og:image || og:image[1]:width > og:image:width
$fixed_key = preg_replace( '/[d]/', '', $key );
if( 0 === strpos( $key, 'twitter:' ) )
$metas[] = '<meta name="' . $fixed_key . '" content="' . esc_attr( $val ) . '" />';
else
$metas[] = '<meta property="' . $fixed_key . '" content="' . esc_attr( $val ) . '" />';
}
/**
* Filter resulting properties. Натисніть на будь-який інший або більше повідомлень / twitter properties.
*
* @param array $els
*/
$metas = apply_filters( 'kama_og_meta_elements', $metas, $els );
echo "n". implode("n", $metas). "nn";
}
/**
* Generate string to show as document title.
*
* Для повідомлень і taxonomies specific title може бути визначений як metadata with name `title`. *
*
* @param string $title `pre_get_document_title` passed value.
*
* @return string
*/
static function meta_title( $title = '' ){
global $post;
// support for `pre_get_document_title` hook.
if($title)
return $title;
static $cache; if( $cache ) return $cache;
$l10n = apply_filters( 'kama_meta_title_l10n', [
'404' => 'Помилка 404: такої сторінки не існує',
'search' => 'Результати пошуку на запит: %s',
'compage' => 'Коментарі %s',
'author' => 'Статті автора: %s',
'archive' => 'Архів за',
'paged' => 'Сторінка %d',
]);
$parts = [
'prev' => '',
'title' => '',
'page' => '',
'after' => '',
];
// 404
if( is_404() ){
$parts['title'] = $l10n['404'];
}
// search
elseif( is_search() ){
$parts['title'] = sprintf( $l10n['search'], get_query_var( 's' ) );
}
// front_page
elseif( is_front_page() ){
if( is_page() && $parts['title'] = get_post_meta( $post->ID, 'title', 1 ) ){
// $parts['title'] defined
}
else {
$parts['title'] = get_bloginfo( 'name', 'display' );
$parts['after'] = '{{description}}';
}
}
//Singular
elseif( is_singular() || ( is_home() && ! is_front_page() ) || ( is_page() && ! is_front_page() ) ){
$parts['title'] = get_post_meta( $post->ID, 'title', 1);
if( ! $parts['title'] ){
/**
* Натиснути на meta meta title для окремого типу сторінки, до default title буде йти.
*
* @param string $title
* @param WP_Post $post
*/
$parts['title'] = apply_filters( 'kama_meta_title_singular', '', $post );
}
if( ! $parts['title'] ){
$parts['title'] = single_post_title('', 0);
}
if( $cpage = get_query_var( 'cpage' ) ){
$parts['prev'] = sprintf( $l10n['compage'], $cpage );
}
}
//post_type_archive
elseif( is_post_type_archive() ){
$parts['title'] = post_type_archive_title('', 0);
$parts['after'] = '{{blog_name}}';
}
// taxonomy
elseif( is_category() || is_tag() || is_tax() ){
$term = get_queried_object();
$parts['title'] = $term? get_term_meta( $term->term_id, 'title', 1) : '';
if( ! $parts['title'] ){
$parts['title'] = single_term_title('', 0);
if( is_tax() ){
$parts['prev'] = get_taxonomy( $term->taxonomy )->labels->name;
}
}
$parts['after'] = '{{blog_name}}';
}
// author posts archive
elseif( is_author() ){
$parts['title'] = sprintf( $l10n['author'], get_queried_object()->display_name );
$parts['after'] = '{{blog_name}}';
}
// date archive
elseif( ( get_locale() === 'ru_RU' ) && ( is_day() || is_month() || is_year() ) ){
$rus_month = [
'', 'січень', 'лютий', 'березень', 'квітень', 'травень', 'червень',
'липень', 'серпень', 'вересень', 'жовтень', 'листопад', 'грудень'
];
$rus_month2 = [
'', 'січня', 'лютого', 'березня', 'квітня', 'травня', 'червня',
'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня'
];
$year = get_query_var('year');
$monthnum = get_query_var('monthnum');
$day = get_query_var('day');
if( is_year() ) $dat = "$year рік";
elseif( is_month() ) $dat = "{$rus_month[ $monthnum ]} $year року";
elseif( is_day() ) $dat = "$day {$rus_month2[ $monthnum ]} $year року";
$parts['title'] = sprintf( $l10n['archive'], $dat);
$parts['after'] = '{{blog_name}}';
}
// інші архіви
else {
$parts['title'] = get_the_archive_title();
$parts['after'] = '{{blog_name}}';
}
// pagination
$pagenum = get_query_var('paged')?: get_query_var('page');
if( $pagenum && ! is_404() ){
$parts['page'] = sprintf( $l10n['paged'], $pagenum );
}
/**
* Натисніть на зміну елементів у документі title.
*
* @param array $parts Title parts. It then will be joined.
* @param array $l10n Localisation strings.
*/
$parts = apply_filters( 'kama_meta_title_parts', $parts, $l10n);
/** Цей filter is documented в wp-includes/general-template.php */
$parts = apply_filters( 'document_title_parts', $parts);
// handle placeholders
if( '{{blog_name}}' === $parts['after'] ){
$parts['after'] = get_bloginfo( 'name', 'display' );
}
elseif( '{{description}}' === $parts['after'] ){
$parts['after'] = get_bloginfo( 'description', 'display' );
}
/** Цей filter is documented в wp-includes/general-template.php */
$sep = apply_filters( 'document_title_separator', '-' );
$title = implode(''. trim( $sep ) .' ', array_filter( $parts ) );
//$title = wptexturize($title);
//$title = convert_chars($title);
$title = esc_html($title);
$title = capital_P_dangit($title);
return $cache = $title;
}
/**
* Display `description` metatag.
*
* Must be used on hook `wp_head`.
*
* Use `description` meta-field до set description for any posts.
* Вона також працює на сторінці, яку встановлюють як сторінку.
*
* Use `meta_description` meta-field до set description for any terms.
* або use default `description` field of a term.
*
* @return string Description.
*/
static function meta_description(){
global $post;
// Called from `wp_head` hook
$echo_result = ( func_num_args() === 1 );
static $cache = null;
if ( isset ( $ cache ) ) {
if($echo_result)
echo $ cache;
return $cache;
}
$desc = '';
$ need_cut = true;
// front
if( is_front_page() ){
// коли для головної встановлено сторінку
if( is_page() ){
$desc = get_post_meta($post->ID, 'description', true);
$need_cut = false;
}
if( ! $desc ){
/**
* Allow to change front_page meta description.
*
* @param string $home_description
*/
$desc = apply_filters( 'home_meta_description', get_bloginfo( 'description', 'display' ) );
}
}
// any post
elseif( is_singular() ){
if( $desc = get_post_meta( $post->ID, 'description', true ) ){
$need_cut = false;
}
if( ! $desc ){
$desc = $post->post_excerpt?: $post->post_content;
}
$ Desc = trim (strip_tags ($ Desc));
}
// any term (taxonomy element)
elseif( ( $term = get_queried_object() ) && ! empty( $term->term_id ) ){
$desc = get_term_meta($term->term_id, 'meta_description', true);
if(! $desc)
$desc = get_term_meta($term->term_id, 'description', true);
$need_cut = false;
if( ! $desc && $term->description ){
$desc = strip_tags($term->description);
$ need_cut = true;
}
}
$desc = str_replace(["n", "r"], '', $desc);
// remove shortcodes, але leave markdown [foo](URL)
$desc = preg_replace( '~[[^]]+](?!()~', '', $desc );
/**
* Змінити зміну або налаштування meta description.
*
* @param string $desc Current description.
* @param string $origin_desc Description before cut.
* @param bool $need_cut Це потрібно?
* @param int $maxchar How many characters leave after cut.
*/
$desc = apply_filters( 'kama_meta_description', $desc );
/**
* Дозволяє до специфічності is meta description необхідно для того, щоб cutted.
*
* @param bool $need_cut
*/
$need_cut = apply_filters( 'kama_meta_description__need_cut', $need_cut );
if( $need_cut ){
/**
* Allow set max length of meta description.
*
* @param int $maxchar
*/
$maxchar = apply_filters( 'kama_meta_description__maxchar', 260);
$char = mb_strlen($desc);
if( $char > $maxchar ){
$ Desc = mb_substr ($ Desc, 0, $ Maxchar);
$words = explode('', $desc);
$maxwords = count($words) - 1; // remove last word, it incomplete в 90% випадків
$desc = implode('', array_slice($words, 0, $maxwords)). '...';
}
}
// remove multi-space
$desc = preg_replace('/s+/s', '', $desc);
$cache = $desc
? sprintf( "<meta name="description" content="%s" />n", esc_attr( trim( $desc ) ) )
: '';
if($echo_result)
echo $ cache;
return $cache;
}
/**
* Wrpper for WP Robots API введено в WP 5.7+.
*
* Must be used on hook `wp_robots`.
*
* @param array $robots
*/
static function wp_robots_callback( $robots ){
if( is_singular() ){
$robots_str = get_post_meta( get_queried_object_id(), 'robots', true );
}
elseif( is_tax() || is_category() || is_tag() ){
$robots_str = get_term_meta( get_queried_object_id(), 'robots', true );
}
if( ! empty( $robots_str ) ){
// split by spece or comma
$robots_parts = preg_split( '/(?<!:)[s,]+/', $robots_str, -1, PREG_SPLIT_NO_EMPTY );
foreach( $robots_parts as $directive ){
// for max-snippet:2
if( strpos( $directive, ':' ) ){
[$key, $value] = explode(':', $directive);
$ robots [ $ key ] = $ value;
}
else {
$ robots [ $ directive ] = true;
}
}
}
if( ! empty( $robots['none'] ) || ! empty( $robots['noindex'] ) ){
unset( $robots['max-image-preview'] );
}
return $robots;
}
/**
* Generate `<meta name="keywords">` meta-tag fore <head> part of the page.
*
* Для того, щоб встановити свої ключові слова для post, створити meta-field with key `keywords`
* and set the keyword to the value.
*
* Default keyword for post generates from post tags nemes and categories names.
* Якщо "keywords" meta-field is не specified.
*
* Ви можете скористатися ключовими словами для будь-якого taxonomy element (term) using shortcode
* `[keywords=word1, word2, word3]` in the description field.
*
* @param string $home_keywords Keywords for home page. Ex: 'word1, word2, word3'
* @param string $def_keywords наскрізні ключові слова - вкажемо і вони додаватимуться
* До решти на всіх сторінках.
*/
static function meta_keywords( $home_keywords = '', $def_keywords = '' ){
global $post;
$out = [];
if( is_front_page() ){
$out[] = $home_keywords;
}
elseif( is_singular() ){
$meta_keywords = get_post_meta( $post->ID, 'keywords', true );
if( $meta_keywords ){
$out[] = $meta_keywords;
}
elseif( $post->post_type === 'post' ){
$res = wp_get_object_terms( $post->ID, [ 'post_tag', 'category' ], [ 'orderby' => 'none' ] );
if( $res && ! is_wp_error( $res ) ){
foreach( $res as $tag ){
$out[] = $tag->name;
}
}
}
}
elseif( is_category() || is_tag() || is_tax() ){
$term = get_queried_object();
// wp 4.4
if( function_exists( 'get_term_meta' ) && $term ){
$out[] = get_term_meta( $term->term_id, 'keywords', true );
}
else {
preg_match( '![keywords=([^]]+)]!iU', $term->description, $match );
$out[] = isset( $match[1] ) ? trim($match[1]): '';
}
}
if( $def_keywords ){
$out[] = $def_keywords;
}
/**
* Дозволяє змінювати результат string of meta_keywords() method.
*
* @param string $out
*/
$out = apply_filters( 'kama_meta_keywords', implode( ', ', $out ) );
echo $out
? '<meta name="keywords" content="'. esc_attr( $out ) .'" />' . "n"
: '';
}
}
Тепер потрібно підключити та запустити код
Для цього створіть файл class-Kama_SEO_Tags.php в корені теми (або ще десь) і скопіюйте в нього php код (який вище). Підключіть цей файл до файлу теми functions.php .
Якщо код не працює, то потрібно переконатися, чи використовується функція wp_head() у файлі теми header.php і чи не виводиться там жорстко тег <title> та інші SEO теги.
Метаполя в адмінці
Код вище лише виводить потрібні SEO мета-теги, але не додає метаполя для записів чи елементів таксономій – їх потрібно створити окремо.
На сторінці редагування запису або терміну вам потрібно створити метабокс з метаполями:
title– альтернативний SEO заголовок буде використовуватися в мета-теги <title> замість заголовка запису.
description– Опис для сторінки. Якщо його немає, то буде взяти текст із цитати, якщо його немає, то шматок з початку контенту запису.
meta_description– так має називатися метапол опису для термінів, тому що ключ description для таксономій зайнятий…
keywords– метатег keywords. Не знаю чи потрібен він чи ні взагалі в сучасних реаліях.
robots– Вказане тут значення виводиться так, наприклад , noindex, nofollow .
Як створити метабокси з метаполями?
Зробити це можна з різними способами, наприклад, за допомогою: