← Назад в блог

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

Пошаговое решение JSON-LD разметки для Custom Post Type в WordPress: генерация через wp_head, валидация schema.org, устранение дублей и ошибок.

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

Требования

  • WordPress 6.x
  • PHP 8.x
  • Зарегистрированный Custom Post Type
  • Базовые знания hooks WordPress

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

Владельцы сайтов на WordPress сталкиваются с проблемой: после добавления Custom Post Type (например, «articles» или «news») JSON-LD разметка либо не появляется на страницах, либо генерируется с ошибками синтаксиса, либо дублируется в <head>. Google Search Console ругается на невалидный Structured Data, сниппеты в поиске не отображаются. Ниже — рабочий код генерации BlogPosting/Article через wp_head, проверка валидности JSON и защита от дублей.

Сниппеты по статье: 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"> в исходном коде
  • Или JSON ломается: Unexpected token < in JSON
  • Или разметка дублируется: 2-3 одинаковых блока в <head>

Пример ошибки в Google Rich Results Test

ERROR: Missing required field "author"
ERROR: Missing required field "datePublished"
WARNING: Multiple BlogPosting detected on page

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

  1. Тема не генерирует JSON-LD для CPT — стандартная разметка WordPress работает только для post
  2. Плагин SEO добавляет свою разметку — конфликт с кастомным кодом
  3. Неправильный hook — код вставлен в template_redirect вместо wp_head
  4. Формирование JSON вручную без wp_json_encode() — кавычки в заголовке ломают JSON

Рабочее решение

Шаг 1: Функция генерации JSON-LD для CPT

Создайте файл includes/class-jsonld-cpt.php в вашей теме или небольшом плагине. Готовый вариант с пояснениями: сниппет «JSON-LD класс для CPT».

<?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 $target_post_types = ['articles', 'news', 'publications'];
    
    public function __construct() {
        add_action('wp_head', [$this, 'output_jsonld'], 10);
    }
    
    public function output_jsonld() {
        // Проверяем, что это single страница нужного CPT
        if (!is_singular($this->target_post_types)) {
            return;
        }
        
        global $post;
        if (!$post) {
            return;
        }
        
        // Защита от дублей: проверяем, не вывел ли SEO-плагин уже разметку
        if (did_action('jsonld_cpt_output')) {
            return;
        }
        
        $jsonld = $this->build_blogposting_schema($post);
        
        if ($jsonld) {
            echo "\n" . '<script type="application/ld+json">' . "\n";
            echo wp_json_encode($jsonld, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
            echo "\n" . '</script>' . "\n";
            
            // Маркер для предотвращения дублей
            do_action('jsonld_cpt_output');
        }
    }
    
    private function build_blogposting_schema($post) {
        // Получаем данные поста
        $author_id = $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('c', $post);
        $modified_date = get_the_modified_date('c', $post);
        
        // Изображение: featured image или заглушка
        $image_id = 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';
        
        // Собираем схему BlogPosting
        $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,
                '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($post->post_content)
        ];
        
        // Добавляем keywords если есть теги
        $tags = get_the_terms($post->ID, 'post_tag');
        if ($tags && !is_wp_error($tags)) {
            $keywords = wp_list_pluck($tags, 'name');
            $schema['keywords'] = implode(', ', $keywords);
        }
        
        return $schema;
    }
}

// Инициализация
new JSONLD_CPT_Generator();

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

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

// Подключаем генератор JSON-LD
require_once get_stylesheet_directory() . '/includes/class-jsonld-cpt.php';

Шаг 3: Альтернатива — без класса (простой вариант)

Если не хотите использовать классы, добавьте в functions.php код из сниппета «JSON-LD для CPT без класса»:

<?php
add_action('wp_head', 'custom_cpt_jsonld_output', 10);

function custom_cpt_jsonld_output() {
    $target_types = ['articles', 'news'];
    
    if (!is_singular($target_types)) {
        return;
    }
    
    // Проверка на дубль
    if (did_action('jsonld_cpt_output')) {
        return;
    }
    
    global $post;
    if (!$post) return;
    
    $schema = [
        '@context' => 'https://schema.org',
        '@type' => 'BlogPosting',
        'headline' => get_the_title($post->ID),
        'datePublished' => get_the_date('c', $post),
        'dateModified' => get_the_modified_date('c', $post),
        'author' => [
            '@type' => 'Person',
            'name' => get_the_author_meta('display_name', $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);
    echo "\n" . '</script>' . "\n";
    
    do_action('jsonld_cpt_output');
}

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

1. Просмотр исходного кода страницы

Откройте страницу CPT и найдите в <head>. Команды для проверки и подсчёта блоков: сниппет «curl: проверка JSON-LD в head».

curl -s https://yoursite.com/articles/my-post/ | grep -A 50 'application/ld+json'

Ожидаемый вывод — один блок <script type="application/ld+json"> с валидным JSON.

2. Валидация через Google Rich Results Test

Перейдите на Google Rich Results Test и введите URL.

Ожидаемый результат:

  • ✅ BlogPosting detected
  • ✅ No critical errors
  • ⚠️ Предупреждения (warnings) допустимы (например, missing articleBody)

3. CLI-валидация JSON-LD

Используйте сниппет validate-jsonld-cli для локальной проверки:

node sd-validate.mjs "https://yoursite.com/articles/my-post/"

Ожидаемый вывод:

✅ Issues total: 0
❌ Errors:      0
⚠️  Warnings:   2

4. Проверка на дубли

Убедитесь, что в <head> только один блок JSON-LD:

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

Ожидаемый результат: 1


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

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

Симптом:

Uncaught SyntaxError: Unexpected token " в JSON

Причина: Используется echo json_encode($data) без флагов или ручная сборка JSON строкой.

Как исправить:

// ❌ Неправильно
echo '{"headline": "' . get_the_title() . '"}';

// ✅ Правильно
echo wp_json_encode(['headline' => get_the_title()]);

❌ Ошибка 2: Разметка дублируется

Симптом: В <head> 2-3 блока <script type="application/ld+json">.

Причина: Код добавлен и в тему, и в SEO-плагин (Yoast/RankMath), или hook вызывается несколько раз.

Как исправить:

  1. Отключите JSON-LD в настройках SEO-плагина для CPT
  2. Используйте did_action('jsonld_cpt_output') для защиты
  3. Проверьте, нет ли в functions.php дублирующих вызовов add_action('wp_head', ...)

❌ Ошибка 3: Missing required field “author”

Симптом: Google Rich Results Test показывает ошибку.

Причина: Поле author не указано или имеет неверную структуру.

Как исправить:

'author' => [
    '@type' => 'Person',
    'name' => get_the_author_meta('display_name', $post->post_author),
    'url' => get_author_posts_url($post->post_author)
]

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

Симптом: В исходном коде нет <script type="application/ld+json">.

Причина: is_singular() проверяет неверный тип поста.

Как исправить:

// Укажите ваши CPT явно
$target_types = ['articles', 'news', 'publications'];
if (!is_singular($target_types)) {
    return;
}

Если не работает

Пошаговый чеклист и код для отладки: сниппет «Диагностика JSON-LD для CPT».

Чеклист диагностики

  1. Проверьте, что CPT зарегистрирован правильно:

    post_type_exists('articles') // должно вернуть true
  2. Убедитесь, что hook подключён:

    // Временно добавьте в начало функции
    error_log('JSONLD: output called');
  3. Проверьте права доступа:

    • CPT должен быть public => true
    • Страница должна быть доступна без авторизации
  4. Отключите кэш:

    • Очистите объектный кэш WordPress
    • Отключите кэш-плагины на время теста
  5. Проверьте логи PHP:

    tail -f /var/log/php/error.log

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

СредаПрименимостьПримечание
Production✅ ДаОсновной сценарий
Dev/Staging✅ ДаДля тестирования перед деплоем
Docker✅ ДаБез изменений
BitrixVM✅ ДаПроверьте права на запись логов
Nginx/Apache✅ ДаНе влияет на генерацию
CI/CD⚠️ ЧастичноТолько для валидации через CLI

Связанные материалы

Сниппеты по статье:

Другие сниппеты и справочник:


Итоги

В статье даны: рабочий код генерации JSON-LD BlogPosting для Custom Post Type, защита от дублей через did_action(), безопасная сериализация через wp_json_encode() и инструкция по валидации через Google и CLI. Разметка после правок проходит проверку Google Rich Results Test, не дублируется и не ломается на специальных символах.

0 просмотров

Комментарии

Загрузка комментариев...
Пока нет комментариев. Будьте первым!