DEV Community

Андрей Викулов (VProger)
Андрей Викулов (VProger)

Posted on • Originally published at viku-lov.ru on

WordPress: JSON-LD для CPT — дубли и ошибки

WordPress: JSON-LD для CPT — дубли и ошибки

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,
  // ...
]);
Enter fullscreen mode Exit fullscreen mode

Но на странице /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
Enter fullscreen mode Exit fullscreen mode

Почему это происходит

  1. Тема не генерирует JSON-LD для CPT — часто разметка делается только для стандартного post.
  2. SEO-плагин добавляет свою разметку — и вы “добавили ещё одну такую же”.
  3. Неправильный хук — вставили в template_redirect/шаблон, а не в wp_head.
  4. 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();
Enter fullscreen mode Exit fullscreen mode

Шаг 2: Подключение в functions.php

<?php
// wp-content/themes/your-theme/functions.php

require_once get_stylesheet_directory() . '/includes/class-jsonld-cpt.php';
Enter fullscreen mode Exit fullscreen mode

Шаг 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');
}
Enter fullscreen mode Exit fullscreen mode

Проверка результата

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'
Enter fullscreen mode Exit fullscreen mode

3) Проверка на дубли

curl -s https://yoursite.com/articles/my-post/ | grep -c 'application/ld+json'
Enter fullscreen mode Exit fullscreen mode

Ожидаемо: 1

4) Google Rich Results Test

Вбейте URL в Rich Results Test и смотрите:

  • BlogPosting/Article detected
  • ✅ критических ошибок нет
  • ⚠️ warnings допустимы (например, articleBody)

Типичные ошибки

❌ Ошибка 1: JSON ломается из-за кавычек в заголовке

Симптом:

Uncaught SyntaxError: Unexpected token " in JSON
Enter fullscreen mode Exit fullscreen mode

Причина: JSON собирается строкой или через json_encode без нормальной сериализации.

Исправление:

// ❌ плохо
echo '{"headline":"' . get_the_title() . '"}';

// ✅ нормально
echo wp_json_encode(['headline' => get_the_title()]);
Enter fullscreen mode Exit fullscreen mode

❌ Ошибка 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),
]
Enter fullscreen mode Exit fullscreen mode

❌ Ошибка 4: JSON-LD не появляется на CPT

Причина: is_singular() проверяет не тот post type (или CPT другой).

Исправление:

$target_types = ['articles', 'news', 'publications'];

if (!is_singular($target_types)) {
  return;
}
Enter fullscreen mode Exit fullscreen mode

Если не работает: чеклист

  1. CPT реально существует:
post_type_exists('articles'); // должно вернуть true
Enter fullscreen mode Exit fullscreen mode
  1. Хук вообще вызывается (временно):
error_log('JSONLD: output called');
Enter fullscreen mode Exit fullscreen mode
  1. Кэш мешает:
  • очистить кэш плагина/сервера,
  • отключить page cache на время проверки.
  1. Посмотреть PHP-логи:
tail -f /var/log/php/error.log
Enter fullscreen mode Exit fullscreen mode

Где применять

Среда Применимость Примечание
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

Read more on viku-lov.ru

Top comments (0)