WordPress как на ладони
Прибыльная монетизация. Выгодно. Безопасно. Стабильно.

SEO мета-теги без плагинов: title, description, robots, Open Graph

Есть много плагинов для SEO, но почти у всех из них главная задача - это создать SEO мета-теги (метаданные) для страницы: title, description, robots, Open Graph. Иногда есть смысл не подключать подобные плагины, а использовать свой небольшой код - с ним проще управляться и решать нестандартные задачи... В этой статье я делюсь своим «велосипедом» в этой области.

Код ниже был написан уже давно, для статьи: Сравнение СЕО плагинов platinum SEO Pack и All in One SEO Pack. И, несмотря на то что есть плагины, то и дело появляется необходимость пользоваться этим кодом. Он используется иногда, как альтернатива, и судя по комментариям это бывает нужно не только мне. Поэтому было принято стратегическое решение smile вынести этот код в отдельную статью и поддерживать его уже здесь.

Одно из преимуществ этого кода над плагинами - понятность и максимальная простота. Все делается через хуки и тут и в плагинах, только найти нужных хук в плагине обычно занимает больше времени. Кроме того вы можете переписать часть кода под свою конкретную задачу.

Если вы не разбираетесь в PHP, то настоятельно рекомендую использовать SEO плагины - это гораздо удобнее! Впрочем, иногда плагины удобнее и для тех, кто знает PHP, я допустим не всегда юзаю этот код, и плагинами вроде SEO Yoast не брезгую.

Код вывода title, description, robots, Open Graph, twitter

Базовая задача SEO плагинов - это дать возможность создавать четыре метаполя для страницы: title, description, robots и keywords. А также создание дополнительных Open Graph данных.

Код, который все это реализует (см. комментарии в коде):

GitHub
<?php

/**
 * Simple SEO class for WordPress to create page metatags:
 * title, description, robots, keywords, Open Graph.
 *
 * IMPORTANT! Since version 1.7.0 robots code logic was chenged. Changed your code after update!
 * IMPORTANT! Since version 1.8.0 title code logic was 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 in `<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] ){

					// without protocol only: //domain.com/path
					if( substr( $url, 0, 2 ) === '//' ){
						$els['og:url'] = set_url_scheme( $url );
					}
					// without domain
					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 ){

						/**
						 * Allows to 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]'];
		}

		/**
		 * Allows change values of og / 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. Allows to add or remove any og/twitter properties.
		 *
		 * @param array  $els
		 */
		$metas = apply_filters( 'kama_og_meta_elements', $metas, $els );

		echo "\n\n". implode( "\n", $metas ) ."\n\n";
	}

	/**
	 * Generate string to show as document title.
	 *
	 * For posts and taxonomies specific title can be specified as 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'] ){
				/**
				 * Allow to set meta title for singular type page, before the default title will be taken.
				 *
				 * @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}}';
		}
		// other archives
		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 );
		}

		/**
		 * Allows to change parts of the document 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 );

		/** This filter is documented in 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' );
		}

		/** This filter is documented in 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 to set description for any posts.
	 * It also work for page setted as front page.
	 *
	 * Use `meta_description` meta-field to set description for any terms.
	 * Or 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, but leave markdown [foo](URL)
		$desc = preg_replace( '~\[[^\]]+\](?!\()~', '', $desc );

		/**
		 * Allow change or set the meta description.
		 *
		 * @param string $desc        Current description.
		 * @param string $origin_desc Description before cut.
		 * @param bool   $need_cut    Is need to cut?
		 * @param int    $maxchar     How many characters leave after cut.
		 */
		$desc = apply_filters( 'kama_meta_description', $desc );

		/**
		 * Allow to specify is the meta description need to be cutted.
		 *
		 * @param bool $need_cut
		 */
		$need_cut = apply_filters( 'kama_meta_description__need_cut', $need_cut );

		if( $need_cut ){

			/**
			 * Allow set max length of the 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 in 90% cases
				$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 introduced in 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.
	 *
	 * To set Your own keywords for post, create meta-field with key `keywords`
	 * and set the keyword to the value.
	 *
	 * Default keyword for a post generates from post tags nemes and categories names.
	 * If the `keywords` meta-field is not specified.
	 *
	 * You can specify the keywords for the any 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;
		}

		/**
		 * Allow to change resulting 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"
			: '';
	}

}

Теперь нужно подключить и запустить код

Для этого создайте файл Kama_SEO_Tags.php в корне темы (или где-то еще) и скопируйте в него php код (который выше). Подключите этот файл в файле темы functions.php.

require_once __DIR__ . '/Kama_SEO_Tags.php';

Kama_SEO_Tags::init();

Это все.

Если код не работает, то нужно убедиться используется ли функция wp_head() в файле темы header.php и не выводится ли там жестко тег <title> и другие SEO теги.

Метаполя в админке

Код выше только выводит нужные SEO мета-теги, но не добавляет метаполя для записей или элементов таксономий - их нужно создать отдельно.

На странице редактирования записи или термина вам нужно создать метабокс с мета-полями:

  • title - альтернативный SEO заголовок будет использоваться в мета-теги <title>, вместо заголовка записи.

  • description - описание для страницы. Если его нет, то будет взять текст из цитаты, если и его нет, то кусок из начала контента записи.

    meta_description - так должно называться метаполе описания для терминов, потому что ключ description для таксономий занят...

  • keywords - метатег keywords. Не знаю нужен он или нет в современных реалиях.

  • robots - указанное тут значение выводится как есть, например noindex,nofollow.
Как создать метабоксы с метаполями?

Сделать это можно разными способами, например с помощью:

Настройка (использование хуков)

Код содержит хуки, чтобы полностью или частично изменить вывод каждого метаполя.

Рассмотрим некоторые хуки, которые могут пригодится:

Open Graph

// disable `article:section` property.
add_filter( 'kama_og_meta_show_article_section', '__return_false' );
// turn off the image search in post content.
add_filter( 'kama_og_meta_thumb_id_find_in_content', '__return_false' );
// set `og:image` `og:image:width` `og:image:height` values if it's not.
add_filter( 'kama_og_meta_image', function( $image ){

	// $image = WP attachment ID or Image URL or [ image_url, width, height ] array.

	if( ! $image ){
		// постер сайта
		$image = [ wp_get_attachment_image_url( 125, 'full' ), 350, 350 ];
	}

	return $image;
} );

Title

Разделитель в заголовке меняется через родной ВП хук document_title_separator:

// изменим разделитель для title
add_filter( 'document_title_separator', fn() => ' - ' );

Description

Описание для главной страницы

// изменим описание для главной страницы
add_filter( 'home_meta_description', fn() => 'Описание для главной' );
// изменим описание
add_filter( 'kama_meta_description', 'change_meta_description' );
function change_meta_description( $desc ){

	return $desc;
}

Robots

# robots metatag.
add_filter( 'wp_robots', 'wp_kama_robots_tag' );

function wp_kama_robots_tag( $robots ){

	// close
	if(
		is_attachment() ||
		is_paged() ||
		is_post_type_archive()
	){
		$robots['noindex'] = true;
		$robots['follow'] = true;
	}

	// close taxonomies

	// close draft
	if( is_preview() ){
		$robots['none'] = true;
	}

	return $robots;
}
Полезные ссылки

Сделать плагин из SEO мета-теги без плагинов?

  • Добавить ответ
52 комментария
Полезные 7 Вопросы 2 Все
    Войти