WordPress: JSON-LD для CPT — дубли и ошибки
Пошаговое решение JSON-LD разметки для Custom Post Type в WordPress: генерация через wp_head, валидация schema.org, устранение дублей и ошибок.
Требования
- 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
Почему это происходит
- Тема не генерирует JSON-LD для CPT — стандартная разметка WordPress работает только для
post - Плагин SEO добавляет свою разметку — конфликт с кастомным кодом
- Неправильный hook — код вставлен в
template_redirectвместоwp_head - Формирование 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 вызывается несколько раз.
Как исправить:
- Отключите JSON-LD в настройках SEO-плагина для CPT
- Используйте
did_action('jsonld_cpt_output')для защиты - Проверьте, нет ли в
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».
Чеклист диагностики
-
Проверьте, что CPT зарегистрирован правильно:
post_type_exists('articles') // должно вернуть true -
Убедитесь, что hook подключён:
// Временно добавьте в начало функции error_log('JSONLD: output called'); -
Проверьте права доступа:
- CPT должен быть
public => true - Страница должна быть доступна без авторизации
- CPT должен быть
-
Отключите кэш:
- Очистите объектный кэш WordPress
- Отключите кэш-плагины на время теста
-
Проверьте логи 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
Другие сниппеты и справочник:
Итоги
В статье даны: рабочий код генерации JSON-LD BlogPosting для Custom Post Type, защита от дублей через did_action(), безопасная сериализация через wp_json_encode() и инструкция по валидации через Google и CLI. Разметка после правок проходит проверку Google Rich Results Test, не дублируется и не ломается на специальных символах.



Комментарии