WordPress: JSON-LD для CPT — дубли и ошибки
WordPress: JSON-LD для CPT — дубли и ошибки
Владельцы сайтов на WordPress часто ловят одну и ту же боль: добавили Custom Post Type (например, articles или news), а JSON-LD разметка либо не появляется, либо ломается синтаксисом, либо дублируется в <head>. В итоге Google Search Console/ Rich Results Test ругаются на невалидный Structured Data — и красивые сниппеты в выдаче не прилетают.
Ниже — рабочий вариант генерации BlogPosting/Article через wp_head, с безопасной сериализацией JSON (wp_json_encode) и защитой от дублей.
Сниппеты по статье:
- JSON-LD класс для CPT
- JSON-LD для CPT без класса
- curl: проверка JSON-LD в head
- Диагностика JSON-LD для CPT
В чём проблема
Реальный симптом
Вы регистрируете Custom Post Type:
register_post_type('articles', [
'public' => true,
'has_archive' => true,
// ...
]);
Но на странице /articles/my-post/ происходит одно из трёх:
- В исходнике нет блока
<script type="application/ld+json">…</script> - JSON ломается (в консоли/валидаторе):
Unexpected token < in JSON - Разметка дублируется: 2–3 одинаковых блока в
<head>
Пример из Rich Results Test:
ERROR: Missing required field "author"
ERROR: Missing required field "datePublished"
WARNING: Multiple BlogPosting detected on page
Почему это происходит
-
Тема не генерирует JSON-LD для CPT — часто разметка делается только для стандартного
post. - SEO-плагин добавляет свою разметку — и вы “добавили ещё одну такую же”.
-
Неправильный хук — вставили в
template_redirect/шаблон, а не вwp_head. -
JSON собирается руками строкой — кавычки, спецсимволы, HTML в excerpt → привет, сломанный JSON. Нужен
wp_json_encode().
Рабочее решение
Шаг 1: Класс генерации JSON-LD для CPT
Создайте файл:
wp-content/themes/your-theme/includes/class-jsonld-cpt.php
(или в небольшом плагине — логика та же).
<?php
/**
* JSON-LD Generator for Custom Post Types
* Place in:
* - wp-content/themes/your-theme/includes/class-jsonld-cpt.php
* Or:
* - wp-content/plugins/your-plugin/includes/class-jsonld-cpt.php
*/
class JSONLD_CPT_Generator
{
private array $target_post_types = ['articles', 'news', 'publications'];
public function __construct()
{
add_action('wp_head', [$this, 'output_jsonld'], 10);
}
public function output_jsonld(): void
{
// Только single страницы нужных CPT
if (!is_singular($this->target_post_types)) {
return;
}
global $post;
if (!$post instanceof WP_Post) {
return;
}
// Защита от дублей: если уже выводили — выходим
if (did_action('jsonld_cpt_output')) {
return;
}
$jsonld = $this->build_blogposting_schema($post);
if (!$jsonld) {
return;
}
echo "\n" . '<script type="application/ld+json">' . "\n";
echo wp_json_encode($jsonld, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
echo "\n" . '</script>' . "\n";
// Маркер, что мы уже вывели JSON-LD
do_action('jsonld_cpt_output');
}
private function build_blogposting_schema(WP_Post $post): array
{
$author_id = (int) $post->post_author;
$author_name = get_the_author_meta('display_name', $author_id);
$author_url = get_author_posts_url($author_id);
$publish_date = get_the_date(DATE_W3C, $post);
$modified_date = get_the_modified_date(DATE_W3C, $post);
// Изображение: featured image или заглушка
$image_id = (int) get_post_thumbnail_id($post->ID);
$image_url = $image_id
? wp_get_attachment_image_url($image_id, 'full')
: (get_stylesheet_directory_uri() . '/assets/images/og-default.png');
// Категория (первая)
$categories = get_the_terms($post->ID, 'category');
$section = ($categories && !is_wp_error($categories))
? $categories[0]->name
: 'General';
$schema = [
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'mainEntityOfPage' => [
'@type' => 'WebPage',
'@id' => get_permalink($post->ID),
],
'headline' => get_the_title($post->ID),
'description' => get_the_excerpt($post->ID),
'image' => [
'@type' => 'ImageObject',
'url' => $image_url,
'width' => 1200,
'height' => 630,
],
'datePublished' => $publish_date,
'dateModified' => $modified_date,
'author' => [
'@type' => 'Person',
'name' => $author_name ?: get_bloginfo('name'),
'url' => $author_url,
],
'publisher' => [
'@type' => 'Organization',
'name' => get_bloginfo('name'),
'logo' => [
'@type' => 'ImageObject',
'url' => get_stylesheet_directory_uri() . '/assets/images/logo.png',
'width' => 600,
'height' => 60,
],
],
'articleSection' => $section,
'wordCount' => str_word_count(wp_strip_all_tags($post->post_content)),
];
// keywords из тегов
$tags = get_the_terms($post->ID, 'post_tag');
if ($tags && !is_wp_error($tags)) {
$schema['keywords'] = implode(', ', wp_list_pluck($tags, 'name'));
}
return $schema;
}
}
// Инициализация
new JSONLD_CPT_Generator();
Шаг 2: Подключение в functions.php
<?php
// wp-content/themes/your-theme/functions.php
require_once get_stylesheet_directory() . '/includes/class-jsonld-cpt.php';
Шаг 3: Альтернатива без класса (быстро и грубо, но работает)
<?php
add_action('wp_head', 'custom_cpt_jsonld_output', 10);
function custom_cpt_jsonld_output(): void
{
$target_types = ['articles', 'news'];
if (!is_singular($target_types)) {
return;
}
if (did_action('jsonld_cpt_output')) {
return;
}
global $post;
if (!$post instanceof WP_Post) {
return;
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'headline' => get_the_title($post->ID),
'datePublished' => get_the_date(DATE_W3C, $post),
'dateModified' => get_the_modified_date(DATE_W3C, $post),
'author' => [
'@type' => 'Person',
'name' => get_the_author_meta('display_name', (int) $post->post_author),
],
'publisher' => [
'@type' => 'Organization',
'name' => get_bloginfo('name'),
],
];
echo '<script type="application/ld+json">' . "\n";
echo wp_json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
echo "\n" . '</script>' . "\n";
do_action('jsonld_cpt_output');
}
Проверка результата
1) Проверка в исходнике
Откройте страницу CPT и найдите в <head>:
- должен быть один блок
<script type="application/ld+json"> - внутри — валидный JSON (без HTML, без “поехавших” кавычек)
2) Быстрая проверка через curl
curl -s https://yoursite.com/articles/my-post/ | grep -A 60 'application/ld+json'
3) Проверка на дубли
curl -s https://yoursite.com/articles/my-post/ | grep -c 'application/ld+json'
Ожидаемо: 1
4) Google Rich Results Test
Вбейте URL в Rich Results Test и смотрите:
- ✅
BlogPosting/Article detected - ✅ критических ошибок нет
- ⚠️ warnings допустимы (например,
articleBody)
Типичные ошибки
❌ Ошибка 1: JSON ломается из-за кавычек в заголовке
Симптом:
Uncaught SyntaxError: Unexpected token " in JSON
Причина: JSON собирается строкой или через json_encode без нормальной сериализации.
Исправление:
// ❌ плохо
echo '{"headline":"' . get_the_title() . '"}';
// ✅ нормально
echo wp_json_encode(['headline' => get_the_title()]);
❌ Ошибка 2: Разметка дублируется
Симптом: 2–3 блока JSON-LD в <head>.
Причина: тема + SEO-плагин (Yoast/RankMath) или несколько add_action('wp_head', ...).
Исправление:
- выключить structured data для CPT в SEO-плагине (если есть такая настройка),
- оставить один источник разметки,
- использовать маркер
did_action('jsonld_cpt_output').
❌ Ошибка 3: Missing required field "author"
Причина: неверная структура author или он пустой.
Исправление (правильная структура):
'author' => [
'@type' => 'Person',
'name' => get_the_author_meta('display_name', (int) $post->post_author),
'url' => get_author_posts_url((int) $post->post_author),
]
❌ Ошибка 4: JSON-LD не появляется на CPT
Причина: is_singular() проверяет не тот post type (или CPT другой).
Исправление:
$target_types = ['articles', 'news', 'publications'];
if (!is_singular($target_types)) {
return;
}
Если не работает: чеклист
- CPT реально существует:
post_type_exists('articles'); // должно вернуть true
- Хук вообще вызывается (временно):
error_log('JSONLD: output called');
- Кэш мешает:
- очистить кэш плагина/сервера,
- отключить page cache на время проверки.
- Посмотреть PHP-логи:
tail -f /var/log/php/error.log
Где применять
| Среда | Применимость | Примечание |
|---|---|---|
| Production | ✅ Да | основной сценарий |
| Dev/Staging | ✅ Да | тест перед деплоем |
| Docker | ✅ Да | без изменений |
| BitrixVM | ✅ Да | только проверь права на логи |
| Nginx/Apache | ✅ Да | на генерацию не влияет |
| CI/CD | ⚠️ Частично | удобно валидировать через CLI |
Связанные материалы
Сниппеты по статье:
- JSON-LD класс для CPT
- JSON-LD для CPT без класса
- curl: проверка JSON-LD в head
- Диагностика JSON-LD для CPT
Другие сниппеты и справочник:
- CLI валидатор JSON-LD
- CLI валидатор микроразметки
- Термин: JSON-LD
- Термин: Schema.org
Итоги
Если JSON-LD для CPT в WordPress не появляется, ломается или дублируется, почти всегда виноваты: неправильный хук, ручная сборка JSON и конфликт с SEO-плагином. Решение простое и скучное (значит хорошее): wp_head + wp_json_encode() + маркер от дублей через did_action().
Read more on viku-lov.ru: https://viku-lov.ru/blog/wordpress-json-ld-blogposting-custom-post-type

Top comments (0)