WordPress как на ладони
WordCamp Saint Petersburg 2018 wordpress jino

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 данных.

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

// удалим стандартный вывод title
remove_action( 'wp_head', '_wp_render_title_tag', 1 );

// вызов всех функций в HEAD страницы
add_action( 'wp_head', 'kama_render_seo_tags', 1 );
function kama_render_seo_tags(){
	//remove_theme_support( 'title-tag' ); // не обязательно

	echo '<title>'. kama_meta_title(' » ') .'</title>'."\n\n";

	echo kama_meta_description('Текст для главной');
	echo kama_meta_keywords('ключи, для, главной');
	echo kama_meta_robots();

	echo kama_og_meta(); // Open Graph, twitter данные
}

/**
 * Open Graph, twitter данные в <head>.
 * документация: http://ogp.me/
 *
 * @version 5
 */
function kama_og_meta(){

	$obj = get_queried_object();

	// только для записей или терминов
	if( isset($obj->post_type) )
		$post = $obj;
	elseif( isset($obj->term_id) )
		$term = $obj;

	$is_post_page = isset($post);
	$is_term_page = isset($term);

	$title = kama_meta_title( '–' );
	$desc  = preg_replace( '/^.+content="([^"]*)".*$/s', '$1', kama_meta_description() );

	// Open Graph
	$els = array();
	$els['og:locale']      = '<meta property="og:locale" content="'. get_locale() .'" />';
	$els['og:site_name']   = '<meta property="og:site_name" content="'. esc_attr( get_bloginfo('name') ) .'" />';
	$els['og:title']       = '<meta property="og:title" content="'. esc_attr( $title ) .'" />';
	$els['og:description'] = '<meta property="og:description" content="'. esc_attr( $desc ) .'" />';
	$els['og:type']        = '<meta property="og:type" content="'.( is_singular() ? 'article' : 'object' ).'" />';

	if( $is_post_page ) $pageurl = get_permalink( $post );
	if( $is_term_page ) $pageurl = get_term_link( $term );
	if( isset($pageurl) )
		$els['og:url'] = '<meta property="og:url" content="'. esc_attr( $pageurl ) .'" />';

	if( apply_filters( 'kama_og_meta_show_article_section', true ) ){
		if( is_singular() && $post_taxname =  get_object_taxonomies($post->post_type) ){
			$post_terms = get_the_terms( $post, reset($post_taxname) );
			if( $post_terms && $post_term = array_shift($post_terms) )
				$els['article:section'] = '<meta property="article:section" content="'. esc_attr( $post_term->name ) .'" />';
		}
	}

	// image
	$thumb_id = 0;
	if    ( $is_post_page ) $thumb_id = get_post_thumbnail_id( $post );
	elseif( $is_term_page ) $thumb_id = get_term_meta( $term->term_id, '_thumbnail_id', 1 );
	$thumb_id = apply_filters( 'kama_og_meta_thumb_id', $thumb_id );

	if( $thumb_id ){
		list( $image_url, $img_width, $img_height ) = image_downsize( $thumb_id, 'full' );

		// Open Graph image
		$els['og:image']        = '<meta property="og:image" content="'. esc_url($image_url) .'" />';
		$els['og:image:width']  = '<meta property="og:image:width" content="'. (int) $img_width .'" />';
		$els['og:image:height'] = '<meta property="og:image:height" content="'. (int) $img_height .'" />';
	}

	// twitter
	$els['twitter:card']         = '<meta name="twitter:card" content="summary" />';
	$els['twitter:description']  = '<meta name="twitter:description" content="'. esc_attr( $desc ) .'" />';
	$els['twitter:title']        = '<meta name="twitter:title" content="'. esc_attr( $title ) .'" />';
	if( isset($image_url) )
		$els['twitter:image'] = '<meta name="twitter:image" content="'. esc_url($image_url) .'" />';

	$els = apply_filters( 'kama_og_meta_elements', $els );

	return "\n\n". implode("\n", $els ) ."\n\n";
}

/**
 * Выводит заголовок страницы <title>
 *
 * Для меток и категорий указывается в настройках, в описании: [title=Заголовок].
 * Для записей, если нужно, чтобы заголовок страницы отличался от заголовка записи,
 * создайте произвольное поле title и впишите туда произвольный заголовок.
 *
 * @version 4.8
 *
 * @param string     $sep            разделитель
 * @param true|false $add_blog_name  добавлять ли название блога в конец заголовка для архивов.
 */
function kama_meta_title( $sep = '»', $add_blog_name = true ){
	static $cache; if( $cache ) return $cache;

	global $post;

	$l10n = apply_filters( 'kama_meta_title_l10n', array(
		'404'     => 'Ошибка 404: такой страницы не существует',
		'search'  => 'Результаты поиска по запросу: %s',
		'compage' => 'Комментарии %s',
		'author'  => 'Статьи автора: %s',
		'archive' => 'Архив за',
		'paged'   => '(страница %d)',
	) );

	$parts = array(
		'prev'  => '',
		'title' => '',
		'after' => '',
		'paged' => '',
	);
	$title = & $parts['title']; // упростим
	$after = & $parts['after']; // упростим

	if(0){}
	// 404
	elseif ( is_404() ){
		$title = $l10n['404'];
	}
	// поиск
	elseif ( is_search() ){
		$title = sprintf( $l10n['search'], get_query_var('s') );
	}
	// главная
	elseif( is_front_page() ){
		if( is_page() && $title = get_post_meta( $post->ID, 'title', 1 ) ){
			// $title определен
		} else {
			$title = get_bloginfo('name');
			$after = get_bloginfo('description');
		}
	}
	// отдельная страница
	elseif( is_singular() || ( is_home() && ! is_front_page() ) || ( is_page() && ! is_front_page() ) ){
		$title = get_post_meta( $post->ID, 'title', 1 ); // указанный title у записи в приоритете

		if( ! $title ) $title = apply_filters( 'kama_meta_title_singular', '', $post );
		if( ! $title ) $title = single_post_title( '', 0 );

		if( $cpage = get_query_var('cpage') )
			$parts['prev'] = sprintf( $l10n['compage'], $cpage );
	}
	// архив типа поста
	elseif ( is_post_type_archive() ){
		$title = post_type_archive_title('', 0 );
		$after = 'blog_name';
	}
	// таксономии
	elseif( is_category() || is_tag() || is_tax() ){
		$term = get_queried_object();

		$title = get_term_meta( $term->term_id, 'title', 1 );

		if( ! $title ){
			$title = single_term_title('', 0 );

			if( is_tax() )
				$parts['prev'] = get_taxonomy($term->taxonomy)->labels->name;
		}

		$after = 'blog_name';
	}
	// архив автора
	elseif ( is_author() ){
		$title = sprintf( $l10n['author'], get_queried_object()->display_name );
		$after = 'blog_name';
	}
	// архив даты
	elseif ( ( get_locale() === 'ru_RU' ) && ( is_day() || is_month() || is_year() ) ){
		$rus_month  = array('', 'январь', 'февраль', 'март', 'апрель', 'май', 'июнь', 'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь');
		$rus_month2 = array('', 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря');
		$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 года";

		$title = sprintf( $l10n['archive'], $dat );
		$after = 'blog_name';
	}
	// остальные архивы
	else {
		$title = get_the_archive_title();
		$after = 'blog_name';
	}

	// номера страниц для пагинации и деления записи
	$pagenum = get_query_var('paged') ?: get_query_var('page');
	if( $pagenum )
		$parts['paged'] = sprintf( $l10n['paged'], $pagenum );

	// позволяет фильтровать title как угодно. Сам заголово
	// $parts содержит массив с элементами: prev - текст до, title - заголовок, after - текст после
	$parts = apply_filters_ref_array( 'kama_meta_title_parts', array($parts, $l10n) );

	if( $after == 'blog_name' )
		$after = $add_blog_name ? get_bloginfo('name') : '';

	// добавим пагинацию в title
	if( $parts['paged'] ){
		$parts['title'] .=  " {$parts['paged']}";
		unset( $parts['paged'] );
	}

	$title = implode( ' '. trim($sep) .' ', array_filter($parts) );

	//$title = apply_filters( 'kama_meta_title', $title );

	$title = wptexturize( $title );
	$title = esc_html( $title );

	return $cache = $title;
}

/**
 * Выводит метатег description.
 *
 * Для элементов таксономий: метаполе description или в описании такой шоткод [description = текст описания]
 * У постов сначала проверяется, метаполе description, или цитата, или начальная часть контента.
 * Цитата или контент обрезаются до указанного в $maxchar символов.
 *
 * @param $home_description Указывается описание для главной страницы сайта.
 * @param $maxchar          Максимальная длина описания (в символах).
 *
 * @version 0.15
 */
function kama_meta_description( $home_description = '', $maxchar = 160 ){
	static $cache; if( $cache ) return $cache;

	global $post;

	$cut = true;
	$desc = '';

	// front
	if( is_front_page() ){
		// когда для главной установлена страница
		if( is_page() && $desc = get_post_meta($post->ID, 'description', true )  ){
			$cut = false;
		}

		if( ! $desc )
			$desc = $home_description ?: get_bloginfo( 'description', 'display' );
	}
	// singular
	elseif( is_singular() ){
		if( $desc = get_post_meta($post->ID, 'description', true ) )
			$cut = false;

		if( ! $desc ) $desc = $post->post_excerpt ?: $post->post_content;

		$desc = trim( strip_tags( $desc ) );
	}
	// term
	elseif( is_category() || is_tag() || is_tax() ){
		$term = get_queried_object();

		$desc = get_term_meta( $term->term_id, 'meta_description', true );
		$cut = false;

		if( ! $desc && $term->description ){
			$desc = strip_tags( $term->description );
			$cut = true;
		}
	}

	if( $desc ){
		$origin_out = $desc;
		$desc = str_replace( array("\n", "\r"), ' ', $desc );
		$desc = preg_replace( '~\[[^\]]+\](?!\()~', '', $desc ); // удаляем шоткоды. Оставляем маркдаун [foo](URL)

		$desc = apply_filters( 'kama_meta_description_pre_cut', $desc );

		if( $cut ){
			$char = mb_strlen( $desc );
			if( $char > $maxchar ){
				$desc     = mb_substr( $desc, 0, $maxchar );
				$words    = explode(' ', $desc );
				$maxwords = count($words) - 1; // убираем последнее слово, оно в 90% случаев неполное
				$desc     = join(' ', array_slice($words, 0, $maxwords)).' ...';
			}
		}

		$desc = apply_filters( 'kama_meta_description', $desc, $origin_out, $cut, $maxchar );
		if( $desc )
			return $cache = '<meta name="description" content="'. esc_attr( trim($desc) ) .'" />'."\n";
	}

}

/**
 * Генерирует метатег keywords для head части сайта
 *
 * Чтобы задать свои keywords для записи, создайте произвольное поле keywords и впишите в значения необходимые ключевые слова.
 * Для постов (post) ключевые слова генерируются из меток и названия категорий, если не указано произвольное поле keywords.
 *
 * Для меток, категорий и произвольных таксономий, ключевые слова указываются в описании, в шоткоде: [keywords=слово1, слово2, слово3]
 *
 * @ $home_keywords: Для главной, ключевые слова указываются в первом параметре: kama_meta_keywords( 'слово1, слово2, слово3' );
 * @ $def_keywords: сквозные ключевые слова - укажем и они будут прибавляться к остальным на всех страницах
 *
 * version 0.7
 */
function kama_meta_keywords( $home_keywords = '', $def_keywords = '' ){
	global $wp_query, $post;
	$out = '';

	if ( is_front_page() ){
		$out = $home_keywords;
	}
	elseif( is_singular() ){
		$out = get_post_meta( $post->ID, 'keywords', true );

		// для постов указываем ключами метки и категории, если не указаны ключи в произвольном поле
		if( ! $out && $post->post_type == 'post' ){
			$res = wp_get_object_terms( $post->ID, array('post_tag', 'category'), array('orderby' => 'none') ); // получаем категории и метки

			if( $res && ! is_wp_error($res) ) foreach( $res as $tag ) $out .= ", $tag->name";

			$out = ltrim( $out, ', ' );
		}
	}
	elseif ( is_category() || is_tag() || is_tax() ){
		$term = get_queried_object();

		// wp 4.4
		if( function_exists('get_term_meta') ){
			$out = get_term_meta( $term->term_id, "keywords", true );
		}
		else{
			preg_match( '!\[keywords=([^\]]+)\]!iU', $term->description, $match );
			$out = isset($match[1]) ? $match[1] : '';
		}

	}

	if( $out && $def_keywords )
		$out = $out .', '. $def_keywords;

	if( $out )
		return "<meta name=\"keywords\" content=\"$out\" />\n";
}

/**
 * Метатег robots
 *
 * Чтобы задать свои атрибуты метатега robots записи, создайте произвольное поле с ключом robots
 * и необходимым значением, например: noindex,nofollow
 *
 * Укажите параметр $allow_types, чтобы разрешить индексацию типов страниц.
 *
 * @ $allow_types Какие типы страниц нужно индексировать (через запятую):
 *                cpage, is_category, is_tag, is_tax, is_author, is_year, is_month,
 *                is_attachment, is_day, is_search, is_feed, is_post_type_archive, is_paged
 *                (можно использовать любые условные теги в виде строки)
 *                cpage - страницы комментариев
 * @ $robots      Как закрывать индексацию: noindex,nofollow
 *
 * version 0.6
 */
function kama_meta_robots( $allow_types = null, $robots = 'noindex,nofollow' ){
	global $post;

	if( null === $allow_types ) $allow_types = 'cpage, is_category, is_attachment, is_tag, is_tax, is_paged, is_post_type_archive';

	if( ( is_home() || is_front_page() ) && ! is_paged() ) return;

	if( is_singular() ){
		// если это не вложение или вложение но оно разрешено
		if( ! is_attachment() || false !== strpos($allow_types,'is_attachment') ){
			$robots = get_post_meta( $post->ID, 'robots', true );
		}
	}
	else {
		$types = preg_split('~[, ]+~', $allow_types );
		$types = array_filter( $types );

		foreach( $types as $type ){
			if( $type == 'cpage' && strpos($_SERVER['REQUEST_URI'], '/comment-page') ) $robots = false;
			elseif( function_exists($type) && $type() )                                $robots = false;
		}
	}

	$robots = apply_filters( 'kama_meta_robots_close', $robots );

	if( $robots )
		return "<meta name=\"robots\" content=\"$robots\" />\n";
}

Подключение

Рекомендую скопировать код в отдельный php файл, например seo.php и подключить его в файл темы functions.php.

require_once 'seo.php';

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

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

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

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

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

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

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

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

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

Сделать это можно с помощью класса Kama_Post_Meta_Box, просто кодом или с помощью плагинов Carbon Fields, Meta Box.

Для элементов таксономий (терминов), используйте пример из хука (taxonomy)_edit_form_fields или все те же плагины.

Тонкая настройка вывода

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

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

SEO мета-теги без плагинов: title, description, robots, Open Graph 14 комментов
Полезные 3 Все
  • VjokerA

    было бы интересно почитать как реализовать свой собственный плагин для SEO, хочу отказаться от тяжелого Yoast SEO, хотя бы на php + хуки wp, спасибо за статью

    Ответить2 месяца назад #
    • Kama5141

      Основное в этой статье, дальше уже кому что нужно для СЕО...

      Ответить2 месяца назад #
  • Станислав

    Скажите, пожалуйста, данный код не конфликтует с вашим "kama_meta_description"? И нет ли подобного решения, но отдельно для Open Graph?

    Ответить2 месяца назад #
    • Kama5141

      Конфликтует, вообще это он и есть только версия более новая. Для добавления опенграф просто вытащи эту функцию и подключи и все...

      Ответитьмесяц назад #
  • BigDimoz15 cайт: pribylvseti.ru

    Тимур привет. Скажи, а вот описание сайта вслед за заголовком на главной странице по умолчанию включено, так и должно быть?

    Ответить2 месяца назад #
    • Kama5141

      Да как правило так лучше, тут все конечно зависит от заголовка и описания сайта. Ну и такое описание включено только для страниц архивов, не для записей, там это вроде как не нужно.

      Ответитьмесяц назад #
  • Leo

    Немного не хватает некоторых опций. Например, установить картинку по умолчанию для всего сайта, чтобы и на главной, и в архивах, и в постах если нет миниатюры, она подхватывалась Оpen Graph. Можно реализовать это?

    Вот такую нашел плюшку для плагина JetPack

    function fb_home_image( $tags ) {
    	if ( is_home() || is_front_page() ) {
    		// Remove the default blank image added by Jetpack
    		unset( $tags['og:image'] );
    
    		$fb_home_img = 'YOUR_IMAGE_URL';
    		$tags['og:image'] = esc_url( $fb_home_img );
    	}
    	return $tags;
    }
    add_filter( 'jetpack_open_graph_tags', 'fb_home_image' );

    Это для главной страницы, а для всех страниц

    function custom_jetpack_default_image() {
    	return 'YOUR_IMAGE_URL';
    }
    add_filter( 'jetpack_open_graph_image_default', 'custom_jetpack_default_image' );

    Можно же как то эти функции переделать под seo-kama?

    1
    Ответитьмесяц назад #
    • Kama5141

      Для этого фильтр есть:

      $thumb_id = apply_filters( 'kama_og_meta_thumb_id', $thumb_id );

      Фильтр должен вернуть ID вложения. Пример использования:

      add_filter( 'kama_og_meta_thumb_id', function( $thumb_id ){
      
      	// для архива и главных страниц 
      	if( is_front_page() || is_home() || is_archive() )
      		$thumb_id = 8690; // ID вложения
      
      	// если картинки нет для других страниц, например записей
      	if( ! $thumb_id )
      		$thumb_id = 8686; // ID вложения - картинка сайта
      
      	return $thumb_id;
      } );

      Картинки нужно загрузить в медиатеку сайта...

      Ответитьмесяц назад #
  • keywords больше не учитывается поисковиками, а вот canonical часто нужен, особенно если у вас интернет магазин на вордпрессе ))

    Ответитьмесяц назад #
  • vodnicear cайт: weburoki.pro

    Интересная и полезная статья! Спасибо

    Ответить19 дней назад #
  • ИМХО лучше просто хукать Yoast Seo, или другой сео плагин, который позволяет это делать.

    https://yoast.com/wordpress/plugins/seo/api/

    //хукаем тайтл
    
    add_filter('wpseo_title', 'add_to_page_titles');
    function add_to_page_titles($title) {
    
    	//тут делаем всякое с татйтлом
    
    	return $title;
    }
    

    Кому он очень "ТЯЖЕЛЫЙ" рекомендую просто научится его настраивать, тк там можно просто отключить все что ненужно. Тяжесть, оправдана уймой плюсов из коробки!

    Настраиваемая Карта сайта
    Крошки (нужно капельку кодить, не все темы поддерживают)
    Автозаполняемые теги
    Микроразметка
    Анализ текста на плотность слов и сео стандарты
    Массовое редактирование сео тегов
    *JSON-RD

    имхо по части Сео нужно это ВСЕ... Сам будешь писать в ручную? )

    Ответить10 дней назад #
  • Лео

    Значение $sep в коде никак не влияет ни на что. Меняешь его с двойной кавычки на | и разницы никакой. Почему?

    Ответить9 дней назад #
    • newbie21 cайт: yumchief.com

      Попробуй тут вместо "»" указать "|"

      echo '<title>'. kama_meta_title(' » ') .'</title>'."\n\n";
      Ответить8 дней назад #
      • Kama5141

        Правильно говоришь, надо при вызове функции указывать разделитель, а не в коде менять его...

        Ответить8 дней назад #

Здравствуйте, !

Ваш комментарий
Предпросмотр