Если вы когда-либо вручную вставляли JSON-LD Schema.org в статью, вы видели настоящую проблему: после 30 публикаций она становится непоследовательной, неполной, и никто не осмеливается её поддерживать.
Потребность / Вариант использования
Структурированные данные (JSON-LD) помогают Google и другим поисковым системам точно понимать контент: тип статьи, автор, дата публикации, основное изображение, раздел «О нас», часто задаваемые вопросы и т. д. WordPressУ вас уже есть много достоверной информации (заголовок, отрывок, изображение). Проблема заключается в «смысле»: субъекты, сущности, намерения, а иногда и в правильном понимании. напишите Schema.org (статья, техническая статья, новостная статья и т. д.).
Искусственный интеллект здесь полезен для создания семантический слой исходя из содержания, без Независимо от того, тратите ли вы 10 минут на выбор ключевых слов, сущностей или разделов «О нас» для каждой статьи, по моему опыту, это особенно выгодно в следующих случаях:
- Методы блогов (WordPress(разработка, данные): ИИ хорошо извлекает технологии, версии и концепции.
- Редакционные веб-сайты При наличии множества редакторов: стандартизировать разметку, не обучая всех работе со Schema.org.
- «Контент» сайтов электронной коммерции (руководства, сравнения): обогащают статьи, не изменяя описания продуктов.
В итоге вы узнаете, как реализовать плагин (Совместимо с WordPress 6.9.4 / PHP 8.1+), который:
- генерирует схему JSON-LD Schema.org для каждой статьи с помощью API искусственного интеллекта (вызов).
wp_remote_post()) - кэширует ответ (API Transients)
- Он обновляется только при необходимости (публикация/обновление).
- внедряет JSON-LD в
<head>передний - Он обрабатывает ошибки (тайм-ауты, квоты, некорректный JSON) с корректными резервными вариантами.
Краткое резюме
- Мы генерируем JSON-LD по почте с помощью ИИ, затем мы магазины в метатеге поста (и мы добавили преходящий (в дополнение к предотвращению повторных звонков).
- Ключ API находится в WP-config.php с помощью
define(), никогда в постоянной форме. - Мы называем ИИ API с помощью
wp_remote_post()+ Тайм-аут + обработка ошибок. - Мы заставляем ИИ отправить обратно Строгий JSON (и мы проверяем это на стороне PHP).
- Мы внедряем скрипт JSON-LD через
wp_head(спереди) и мы избегаем администратора. - Мы добавляем REST-эндпоинт только для администраторов. регенерировать По запросу (практично в сфере контроля качества).
Когда следует использовать ИИ для этого?
Используйте ИИ, если у вас есть реальная потребность в семантическом обогащении, а не просто для того, чтобы «разместить „статью“ повсюду». Примеры успешного применения:
- Длинный контент (Более 1000 слов) где извлечение сущностей (брендов, инструментов, концепций) обеспечивает точность.
- Неполные таксономии (ненадежные категории/теги), и вам нужны более понятные поля «О нас/Упоминания».
- Совместное написание При наличии разнородных стилей: ИИ обеспечивает нормализацию.
- SEO-миграция (новая тема, новый SEO-плагин): вы можете создать согласованную схему без переписывания записей.
Я часто наблюдал положительный эффект на сайтах, где сниппет WordPress пуст и где авторы часто вносят изменения: ИИ создает согласованное описание, избегая случайного «сниппета».
Когда НЕ следует использовать ИИ
Избегайте использования ИИ, если ваша схема является чисто механической и уже детерминированной.
- Витрины сайтов Если у вас 10 страниц: сделайте это вручную или с помощью SEO-плагина.
- Простые диаграммы (Организация, Веб-сайт, Хлебные крошки) уже управляются вашим SEO-плагином.
- Деликатный контент (здравоохранение, юриспруденция) если вы полагаетесь на ИИ для «изобретения» объектов недвижимости. В этом случае ИИ должен оставаться добывающим, а не созидательным инструментом.
- Жесткий бюджет и большой объем публикаций, размещаемых ежедневно: затраты на API могут возрасти, если вы будете слишком часто его обновлять.
Классический антипаттерн — это вызов ИИ каждый раз при отображении страницы. Это приводит к таймаутам, ненужным затратам средств, а иногда и к пустым страницам, если код плохо защищен.
Предпосылки
Целевая среда: WordPress 6.9.4 (апрель 2026 г.) и PHP 8.1+.
Ключ API и хранилище
Вы можете использовать OpenAI, Anthropic, Mistral или Google. Я приведу пример OpenAI (API Responses), потому что он очень стабилен в строгом JSON-формате, но структура плагина позволяет легко заменить поставщика.
Сохраните ключ в wp-config.php (или, что еще лучше, переменная окружения, внедренная вашим хостинг-провайдером). Пример:
/**
* Clé API IA (ne jamais commiter ce fichier).
* Idéalement, utilisez une variable d'environnement et fallback sur define().
*/
define('BPCAB_AI_OPENAI_API_KEY', 'REMPPLACEZ-MOI');
Расширения PHP
- завить (часто включено) или allow_url_fopen (WordPress использует Requests, который, если доступен, полагается на cURL).
- JSON (стандарт).
Полезные официальные источники
- wp_remote_post() – Ресурсы для разработчиков WordPress
- API для работы с временными данными — ресурсы для разработчиков WordPress
- Справочник по REST API – Ресурсы для разработчиков WordPress
- json_decode() – PHP.net
- Обязательные к использованию плагины – WordPress.org
Архитектура решения
Текстовый поток, используемый плагином:
Редактор WordPress (save_post) → подготовка данных (заголовок, содержимое, отрывок, изображение, автор) → wp_remote_post() в AI API → JSON-ответ → валидация/очистка → метаданные записи + временное хранилище → внедрение на фронтенд (wp_head) …
Почему этот рабочий процесс хорошо работает в производственной среде
- Поколение на момент накопления (или по запросу), а не на экране: вы не блокируете рендеринг интерфейса, если API ИИ работает медленно.
- Кэш : короткий переходный процесс позволяет избежать зацикливания перегенерации, когда редактор нажимает кнопку «Обновить» 5 раз.
- Мета-пост : постоянный, экспортируемый и версионируемый (если у вас есть тестовая система).
- Проверка JSON Если ИИ возвращает некорректный текст или JSON, ничего не внедряется (резервный вариант).
Важное примечание: SEO-плагины и дубликаты.
Yoast, Rank Math, SEOPress и другие подобные сервисы уже внедряют JSON-LD-файлы. Если вы добавите свой собственный, вы рискуете:
- дубликаты (два)
Article) - несоответствия (два автора, два изображения)
Рекомендуемая мной стратегия: ввести «дополнительная» схема (пар. about, mentions, keywords, audience) в одном Article то, что вы контролируете, или же производите @graph чистый. Приведенный ниже код генерирует @graph Минималистичный дизайн, избегающий «переизобретения» организации/веб-сайта.
Полный код — шаг за шагом
Я советую вам это вставить. мю-плагин Если вы хотите, чтобы он сохранял свою работоспособность при смене темы оформления и "случайной деактивации". В противном случае, это стандартный плагин.
Шаг 1 — Минимальная структура плагинов
Создайте файл: wp-content/mu-plugins/bpcab-ai-schema.php (При необходимости создайте папку).
<?php
/**
* Plugin Name: BPCAB AI Schema (JSON-LD)
* Description: Génère et injecte des données structurées Schema.org via IA par article.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
*
* Conseil : placez ce fichier en mu-plugin pour éviter la désactivation accidentelle.
*/
if (!defined('ABSPATH')) {
exit;
}
Шаг 2 — Константы, варианты и меры безопасности
Мы обеспечиваем безопасность с самого начала: если ключа не существует, мы ничего не пытаемся сделать. Я часто видел, как сайты неоднократно возвращали ошибки 401, потому что код продолжал попытки, несмотря на отсутствие ключа.
/**
* Retourne la clé API OpenAI depuis wp-config.php.
*/
function bpcab_ai_schema_get_openai_key(): string {
if (defined('BPCAB_AI_OPENAI_API_KEY') && is_string(BPCAB_AI_OPENAI_API_KEY) && BPCAB_AI_OPENAI_API_KEY !== '') {
return BPCAB_AI_OPENAI_API_KEY;
}
return '';
}
/**
* Petite liste de post types autorisés.
* Ajustez selon votre site (ex: 'post', 'page', 'guide', etc.).
*/
function bpcab_ai_schema_allowed_post_types(): array {
return array('post');
}
Шаг 3 — Извлечение «надежных» данных WordPress
Искусственный интеллект не должен выдумывать даты, авторов или URL-адреса. Мы берём их из WordPress, а затем просим ИИ лишь о семантическом обогащении.
/**
* Construit un paquet de données "source of truth" depuis WordPress.
* On évite d'envoyer des données inutiles (coût + confidentialité).
*/
function bpcab_ai_schema_build_post_payload(int $post_id): array {
$post = get_post($post_id);
if (!$post) {
return array();
}
$title = get_the_title($post);
$content = $post->post_content;
// Option : limiter la taille envoyée à l'API (coût + latence).
// Ici, on garde le contenu brut, mais vous pouvez préférer wp_strip_all_tags().
$content_plain = wp_strip_all_tags($content);
$content_plain = mb_substr($content_plain, 0, 12000); // garde-fou
$excerpt = has_excerpt($post) ? $post->post_excerpt : wp_trim_words($content_plain, 55, '…');
$author_id = (int) $post->post_author;
$author_name = $author_id ? get_the_author_meta('display_name', $author_id) : '';
$permalink = get_permalink($post);
$published = get_the_date(DATE_W3C, $post);
$modified = get_the_modified_date(DATE_W3C, $post);
$image_id = get_post_thumbnail_id($post);
$image_url = '';
if ($image_id) {
$image = wp_get_attachment_image_src($image_id, 'full');
if (is_array($image) && !empty($image[0])) {
$image_url = $image[0];
}
}
return array(
'post_id' => $post_id,
'post_type' => $post->post_type,
'title' => $title,
'excerpt' => $excerpt,
'content' => $content_plain,
'permalink' => $permalink,
'datePublished'=> $published,
'dateModified' => $modified,
'authorName' => $author_name,
'image' => $image_url,
'language' => get_bloginfo('language'),
);
}
Шаг 4 — Искусственный интеллект подсказывает «строгий JSON» + вызов API через wp_remote_post()
Проблема, которая ломает большинство реализаций: ИИ возвращает текст вокруг JSON или поля, не соответствующие требованиям. Мы обеспечиваем строгий формат, а затем проводим валидацию.
Пример с использованием OpenAI (конечная точка ответа). Официальная справочная информация по API: API ответов OpenAI.
/**
* Appelle OpenAI pour générer un JSON Schema.org (ou un fragment) basé sur le contenu.
* Retourne un tableau PHP (décodé) ou WP_Error.
*/
function bpcab_ai_schema_call_openai(array $payload) {
$api_key = bpcab_ai_schema_get_openai_key();
if ($api_key === '') {
return new WP_Error('bpcab_no_api_key', 'Clé API OpenAI manquante (BPCAB_AI_OPENAI_API_KEY).');
}
// Prompt : on demande un JSON STRICT, sans texte.
$system = "Vous êtes un assistant spécialisé en SEO technique. Vous produisez uniquement du JSON strict, sans commentaire ni markdown.";
$user = array(
"Objectif: Générer un JSON-LD Schema.org pour un article WordPress.n"
. "Contraintes:n"
. "- Répondre uniquement avec un objet JSON valide.n"
. "- Ne pas inventer d'URL, de dates, d'auteur.n"
. "- Utiliser EXACTEMENT les valeurs fournies pour headline, url, datePublished, dateModified, author.name, image.n"
. "- Ajouter des champs sémantiques utiles: keywords (array), about (array of Thing), mentions (array of Thing), articleSection (string), inLanguage.n"
. "- Type recommandé: Article (ou TechArticle si le texte est technique).n"
. "- Produire un JSON-LD avec @context et @graph.n"
. "- Limiter keywords à 12 max. about/mentions: 8 max chacun.n"
. "- Ne pas inclure Organization/WebSite si vous n'avez pas les données.nn"
. "Données fiables (à utiliser telles quelles):n"
. wp_json_encode(array(
"headline" => $payload['title'] ?? '',
"description" => $payload['excerpt'] ?? '',
"url" => $payload['permalink'] ?? '',
"datePublished" => $payload['datePublished'] ?? '',
"dateModified" => $payload['dateModified'] ?? '',
"authorName" => $payload['authorName'] ?? '',
"image" => $payload['image'] ?? '',
"inLanguage" => $payload['language'] ?? 'fr-FR',
)) . "nn"
. "Contenu (extrait):n"
. ($payload['content'] ?? '')
);
$body = array(
'model' => 'gpt-4.1-mini',
'input' => array(
array('role' => 'system', 'content' => $system),
array('role' => 'user', 'content' => $user),
),
// Paramètres prudents : on veut du factuel, pas de créativité.
'temperature' => 0.2,
'max_output_tokens' => 900,
// Demande explicite de sortie JSON. Selon l'API, ce champ peut évoluer.
// Si OpenAI change, gardez la validation JSON côté PHP comme filet de sécurité.
'text' => array('format' => array('type' => 'json_object')),
);
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode($body),
'timeout' => 20, // évitez 60s : en front, c'est mort. Ici on est en save_post, mais restons raisonnables.
);
$response = wp_remote_post('https://api.openai.com/v1/responses', $args);
if (is_wp_error($response)) {
return $response;
}
$code = (int) wp_remote_retrieve_response_code($response);
$raw = wp_remote_retrieve_body($response);
if ($code < 200 || $code >= 300) {
return new WP_Error('bpcab_openai_http_error', 'Erreur HTTP OpenAI: ' . $code, array('body' => $raw));
}
$data = json_decode($raw, true);
if (!is_array($data)) {
return new WP_Error('bpcab_openai_bad_json', 'Réponse OpenAI non JSON (impossible à décoder).', array('body' => $raw));
}
// Selon le format de Responses API, le texte peut être dans output[...].
// On essaie d'extraire un bloc texte puis de décoder ce JSON.
$json_text = '';
// Extraction robuste (évite de dépendre d'un seul chemin).
if (!empty($data['output']) && is_array($data['output'])) {
foreach ($data['output'] as $item) {
if (!is_array($item) || empty($item['content']) || !is_array($item['content'])) {
continue;
}
foreach ($item['content'] as $content_item) {
if (is_array($content_item) && ($content_item['type'] ?? '') === 'output_text' && isset($content_item['text'])) {
$json_text .= $content_item['text'];
}
}
}
}
$json_text = trim($json_text);
if ($json_text === '') {
// Fallback : parfois l'API peut renvoyer directement un champ text.
if (isset($data['text']) && is_string($data['text'])) {
$json_text = trim($data['text']);
}
}
if ($json_text === '') {
return new WP_Error('bpcab_openai_empty_output', 'Sortie OpenAI vide ou non trouvée.', array('body' => $raw));
}
$schema = json_decode($json_text, true);
if (!is_array($schema)) {
return new WP_Error('bpcab_schema_not_json', 'Le contenu généré n’est pas un JSON valide.', array('generated' => $json_text));
}
return $schema;
}
Шаг 5 — Проверка и очистка JSON-LD.
JSON не "очищают" так же, как HTML. Правильный подход заключается в проверке минимальной структуры, удалении всего опасного (скриптов) и корректном кодировании во время отображения.
Распространенная ошибка: использование wp_kses_post() в JSON-файле. Это разрушает кавычки и делает JSON недействительным. Здесь мы проверяем его как массив, а затем... wp_json_encode().
/**
* Validation minimale du schéma.
* On vérifie @context et @graph. On peut être plus strict selon vos besoins.
*/
function bpcab_ai_schema_validate(array $schema) {
if (!isset($schema['@context']) || !is_string($schema['@context'])) {
return new WP_Error('bpcab_schema_missing_context', 'Schema invalide: @context manquant.');
}
if (!isset($schema['@graph']) || !is_array($schema['@graph'])) {
return new WP_Error('bpcab_schema_missing_graph', 'Schema invalide: @graph manquant.');
}
// Protection basique : on refuse toute tentative d'injection de balises.
$encoded = wp_json_encode($schema);
if ($encoded === false) {
return new WP_Error('bpcab_schema_encode_failed', 'Impossible d’encoder le schéma en JSON.');
}
if (stripos($encoded, '<script') !== false || stripos($encoded, '</script') !== false) {
return new WP_Error('bpcab_schema_script_detected', 'Contenu suspect détecté dans le schéma.');
}
return true;
}
/**
* Nettoyage "pragmatique" : on limite certaines longueurs et on force des types.
*/
function bpcab_ai_schema_normalize(array $schema): array {
// Limite de taille pour éviter un JSON-LD énorme (performance + crawl).
$max_graph_items = 12;
if (isset($schema['@graph']) && is_array($schema['@graph']) && count($schema['@graph']) > $max_graph_items) {
$schema['@graph'] = array_slice($schema['@graph'], 0, $max_graph_items);
}
return $schema;
}
Шаг 6 — временный кэш + хранилище пост-метаданных
Мы объединяем два уровня:
- пост мета (постоянный) для переднего дисплея
- преходящий (кратко) чтобы избежать слишком быстрой регенерации
/**
* Clés de stockage.
*/
function bpcab_ai_schema_meta_key(): string {
return '_bpcab_ai_schema_jsonld';
}
function bpcab_ai_schema_transient_key(int $post_id): string {
return 'bpcab_ai_schema_lock_' . $post_id;
}
/**
* Génère et stocke le schéma pour un post.
*/
function bpcab_ai_schema_generate_for_post(int $post_id) {
$payload = bpcab_ai_schema_build_post_payload($post_id);
if (empty($payload)) {
return new WP_Error('bpcab_no_payload', 'Payload vide, post introuvable ?');
}
// Lock anti-boucle (ex: autosave + update en rafale).
if (get_transient(bpcab_ai_schema_transient_key($post_id))) {
return new WP_Error('bpcab_locked', 'Génération déjà en cours ou trop récente (lock transient).');
}
set_transient(bpcab_ai_schema_transient_key($post_id), 1, 2 * MINUTE_IN_SECONDS);
$schema = bpcab_ai_schema_call_openai($payload);
if (is_wp_error($schema)) {
return $schema;
}
$valid = bpcab_ai_schema_validate($schema);
if (is_wp_error($valid)) {
return $valid;
}
$schema = bpcab_ai_schema_normalize($schema);
// Stockage en post meta (tableau encodé JSON).
$json = wp_json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
return new WP_Error('bpcab_encode_failed', 'Encodage JSON final impossible.');
}
update_post_meta($post_id, bpcab_ai_schema_meta_key(), $json);
// On relâche le lock un peu plus tôt si tout s'est bien passé.
delete_transient(bpcab_ai_schema_transient_key($post_id));
return true;
}
Шаг 7 — перехват функции save_post (без нарушения работы редактора)
Неправильный хук или неправильное условие — и вы запускаете ИИ для автосохранений, правок или предварительного просмотра в Elementor. Я постоянно это вижу.
/**
* Déclenchement à la sauvegarde.
*/
function bpcab_ai_schema_on_save_post(int $post_id, WP_Post $post, bool $update): void {
// Éviter autosave, révisions, et contexte non pertinent.
if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
return;
}
// Éviter l'exécution sur les types non autorisés.
if (!in_array($post->post_type, bpcab_ai_schema_allowed_post_types(), true)) {
return;
}
// Éviter les brouillons: souvent le contenu est incomplet.
// Ajustez selon votre workflow.
if ($post->post_status !== 'publish') {
return;
}
// Option : ne régénérer que si le contenu/titre a changé.
// Ici, on régénère à chaque update publié (simple et fiable).
$result = bpcab_ai_schema_generate_for_post($post_id);
// On log en debug uniquement.
if (is_wp_error($result) && defined('WP_DEBUG') && WP_DEBUG) {
error_log('[BPCAB AI Schema] save_post error: ' . $result->get_error_code() . ' - ' . $result->get_error_message());
}
}
add_action('save_post', 'bpcab_ai_schema_on_save_post', 20, 3);
Шаг 8 — внедрение JSON-LD в wp_head
Мы внедряем изменения только на стороне фронтенда, на отдельных узлах и только если существует метаузел. Никаких вызовов ИИ здесь нет.
/**
* Injecte le JSON-LD dans le head.
*/
function bpcab_ai_schema_print_jsonld(): void {
if (is_admin()) {
return;
}
if (!is_singular(bpcab_ai_schema_allowed_post_types())) {
return;
}
$post_id = get_queried_object_id();
if (!$post_id) {
return;
}
$json = get_post_meta($post_id, bpcab_ai_schema_meta_key(), true);
if (!is_string($json) || $json === '') {
return;
}
// Vérification finale : JSON valide.
$decoded = json_decode($json, true);
if (!is_array($decoded)) {
return;
}
// Encodage propre pour éviter les surprises.
$out = wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($out === false) {
return;
}
echo "<script type="application/ld+json">n";
echo $out;
echo "n</script>n";
}
add_action('wp_head', 'bpcab_ai_schema_print_jsonld', 99);
Шаг 9 — REST-интерфейс для регенерации по запросу (только для администратора)
Очень полезно, когда редактор говорит: «Схема не отображается», и вы хотите перегенерировать её, не сохраняя запись заново (и не предоставляя доступ к коду). Мы защищаем её с помощью возможностей + nonce.
/**
* Enregistre une route REST pour régénérer le schéma.
*/
function bpcab_ai_schema_register_rest_route(): void {
register_rest_route('bpcab/v1', '/schema/regenerate/(?P<id>d+)', array(
'methods' => 'POST',
'permission_callback' => function (WP_REST_Request $request) {
// Nonce REST standard: X-WP-Nonce (wp_create_nonce('wp_rest')).
if (!is_user_logged_in()) {
return false;
}
return current_user_can('edit_posts');
},
'callback' => function (WP_REST_Request $request) {
$post_id = (int) $request['id'];
if ($post_id <= 0) {
return new WP_REST_Response(array('ok' => false, 'error' => 'ID invalide'), 400);
}
$post = get_post($post_id);
if (!$post) {
return new WP_REST_Response(array('ok' => false, 'error' => 'Post introuvable'), 404);
}
if (!current_user_can('edit_post', $post_id)) {
return new WP_REST_Response(array('ok' => false, 'error' => 'Accès refusé'), 403);
}
$result = bpcab_ai_schema_generate_for_post($post_id);
if (is_wp_error($result)) {
return new WP_REST_Response(array(
'ok' => false,
'error' => $result->get_error_message(),
'code' => $result->get_error_code(),
'data' => $result->get_error_data(),
), 500);
}
return new WP_REST_Response(array('ok' => true), 200);
},
));
}
add_action('rest_api_init', 'bpcab_ai_schema_register_rest_route');
Полный собранный код
Скопируйте и вставьте этот файл как есть в wp-content/mu-plugins/bpcab-ai-schema.phpЗатем добавьте константу в wp-config.phpНе тестируйте это в рабочей среде без резервной копии: одна забытая скобка — и сайт не будет работать. белый экран.
<?php
/**
* Plugin Name: BPCAB AI Schema (JSON-LD)
* Description: Génère et injecte des données structurées Schema.org via IA par article.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
*/
if (!defined('ABSPATH')) {
exit;
}
function bpcab_ai_schema_get_openai_key(): string {
if (defined('BPCAB_AI_OPENAI_API_KEY') && is_string(BPCAB_AI_OPENAI_API_KEY) && BPCAB_AI_OPENAI_API_KEY !== '') {
return BPCAB_AI_OPENAI_API_KEY;
}
return '';
}
function bpcab_ai_schema_allowed_post_types(): array {
return array('post');
}
function bpcab_ai_schema_meta_key(): string {
return '_bpcab_ai_schema_jsonld';
}
function bpcab_ai_schema_transient_key(int $post_id): string {
return 'bpcab_ai_schema_lock_' . $post_id;
}
function bpcab_ai_schema_build_post_payload(int $post_id): array {
$post = get_post($post_id);
if (!$post) {
return array();
}
$title = get_the_title($post);
$content = $post->post_content;
$content_plain = wp_strip_all_tags($content);
$content_plain = mb_substr($content_plain, 0, 12000);
$excerpt = has_excerpt($post) ? $post->post_excerpt : wp_trim_words($content_plain, 55, '…');
$author_id = (int) $post->post_author;
$author_name = $author_id ? get_the_author_meta('display_name', $author_id) : '';
$permalink = get_permalink($post);
$published = get_the_date(DATE_W3C, $post);
$modified = get_the_modified_date(DATE_W3C, $post);
$image_id = get_post_thumbnail_id($post);
$image_url = '';
if ($image_id) {
$image = wp_get_attachment_image_src($image_id, 'full');
if (is_array($image) && !empty($image[0])) {
$image_url = $image[0];
}
}
return array(
'post_id' => $post_id,
'post_type' => $post->post_type,
'title' => $title,
'excerpt' => $excerpt,
'content' => $content_plain,
'permalink' => $permalink,
'datePublished' => $published,
'dateModified' => $modified,
'authorName' => $author_name,
'image' => $image_url,
'language' => get_bloginfo('language'),
);
}
function bpcab_ai_schema_call_openai(array $payload) {
$api_key = bpcab_ai_schema_get_openai_key();
if ($api_key === '') {
return new WP_Error('bpcab_no_api_key', 'Clé API OpenAI manquante (BPCAB_AI_OPENAI_API_KEY).');
}
$system = "Vous êtes un assistant spécialisé en SEO technique. Vous produisez uniquement du JSON strict, sans commentaire ni markdown.";
$user = array(
"Objectif: Générer un JSON-LD Schema.org pour un article WordPress.n"
. "Contraintes:n"
. "- Répondre uniquement avec un objet JSON valide.n"
. "- Ne pas inventer d'URL, de dates, d'auteur.n"
. "- Utiliser EXACTEMENT les valeurs fournies pour headline, url, datePublished, dateModified, author.name, image.n"
. "- Ajouter des champs sémantiques utiles: keywords (array), about (array of Thing), mentions (array of Thing), articleSection (string), inLanguage.n"
. "- Type recommandé: Article (ou TechArticle si le texte est technique).n"
. "- Produire un JSON-LD avec @context et @graph.n"
. "- Limiter keywords à 12 max. about/mentions: 8 max chacun.n"
. "- Ne pas inclure Organization/WebSite si vous n'avez pas les données.nn"
. "Données fiables (à utiliser telles quelles):n"
. wp_json_encode(array(
"headline" => $payload['title'] ?? '',
"description" => $payload['excerpt'] ?? '',
"url" => $payload['permalink'] ?? '',
"datePublished" => $payload['datePublished'] ?? '',
"dateModified" => $payload['dateModified'] ?? '',
"authorName" => $payload['authorName'] ?? '',
"image" => $payload['image'] ?? '',
"inLanguage" => $payload['language'] ?? 'fr-FR',
)) . "nn"
. "Contenu (extrait):n"
. ($payload['content'] ?? '')
);
$body = array(
'model' => 'gpt-4.1-mini',
'input' => array(
array('role' => 'system', 'content' => $system),
array('role' => 'user', 'content' => $user),
),
'temperature' => 0.2,
'max_output_tokens' => 900,
'text' => array('format' => array('type' => 'json_object')),
);
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode($body),
'timeout' => 20,
);
$response = wp_remote_post('https://api.openai.com/v1/responses', $args);
if (is_wp_error($response)) {
return $response;
}
$code = (int) wp_remote_retrieve_response_code($response);
$raw = wp_remote_retrieve_body($response);
if ($code < 200 || $code >= 300) {
return new WP_Error('bpcab_openai_http_error', 'Erreur HTTP OpenAI: ' . $code, array('body' => $raw));
}
$data = json_decode($raw, true);
if (!is_array($data)) {
return new WP_Error('bpcab_openai_bad_json', 'Réponse OpenAI non JSON (impossible à décoder).', array('body' => $raw));
}
$json_text = '';
if (!empty($data['output']) && is_array($data['output'])) {
foreach ($data['output'] as $item) {
if (!is_array($item) || empty($item['content']) || !is_array($item['content'])) {
continue;
}
foreach ($item['content'] as $content_item) {
if (is_array($content_item) && ($content_item['type'] ?? '') === 'output_text' && isset($content_item['text'])) {
$json_text .= $content_item['text'];
}
}
}
}
$json_text = trim($json_text);
if ($json_text === '' && isset($data['text']) && is_string($data['text'])) {
$json_text = trim($data['text']);
}
if ($json_text === '') {
return new WP_Error('bpcab_openai_empty_output', 'Sortie OpenAI vide ou non trouvée.', array('body' => $raw));
}
$schema = json_decode($json_text, true);
if (!is_array($schema)) {
return new WP_Error('bpcab_schema_not_json', 'Le contenu généré n’est pas un JSON valide.', array('generated' => $json_text));
}
return $schema;
}
function bpcab_ai_schema_validate(array $schema) {
if (!isset($schema['@context']) || !is_string($schema['@context'])) {
return new WP_Error('bpcab_schema_missing_context', 'Schema invalide: @context manquant.');
}
if (!isset($schema['@graph']) || !is_array($schema['@graph'])) {
return new WP_Error('bpcab_schema_missing_graph', 'Schema invalide: @graph manquant.');
}
$encoded = wp_json_encode($schema);
if ($encoded === false) {
return new WP_Error('bpcab_schema_encode_failed', 'Impossible d’encoder le schéma en JSON.');
}
if (stripos($encoded, '<script') !== false || stripos($encoded, '</script') !== false) {
return new WP_Error('bpcab_schema_script_detected', 'Contenu suspect détecté dans le schéma.');
}
return true;
}
function bpcab_ai_schema_normalize(array $schema): array {
$max_graph_items = 12;
if (isset($schema['@graph']) && is_array($schema['@graph']) && count($schema['@graph']) > $max_graph_items) {
$schema['@graph'] = array_slice($schema['@graph'], 0, $max_graph_items);
}
return $schema;
}
function bpcab_ai_schema_generate_for_post(int $post_id) {
$payload = bpcab_ai_schema_build_post_payload($post_id);
if (empty($payload)) {
return new WP_Error('bpcab_no_payload', 'Payload vide, post introuvable ?');
}
if (get_transient(bpcab_ai_schema_transient_key($post_id))) {
return new WP_Error('bpcab_locked', 'Génération déjà en cours ou trop récente (lock transient).');
}
set_transient(bpcab_ai_schema_transient_key($post_id), 1, 2 * MINUTE_IN_SECONDS);
$schema = bpcab_ai_schema_call_openai($payload);
if (is_wp_error($schema)) {
return $schema;
}
$valid = bpcab_ai_schema_validate($schema);
if (is_wp_error($valid)) {
return $valid;
}
$schema = bpcab_ai_schema_normalize($schema);
$json = wp_json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
return new WP_Error('bpcab_encode_failed', 'Encodage JSON final impossible.');
}
update_post_meta($post_id, bpcab_ai_schema_meta_key(), $json);
delete_transient(bpcab_ai_schema_transient_key($post_id));
return true;
}
function bpcab_ai_schema_on_save_post(int $post_id, WP_Post $post, bool $update): void {
if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
return;
}
if (!in_array($post->post_type, bpcab_ai_schema_allowed_post_types(), true)) {
return;
}
if ($post->post_status !== 'publish') {
return;
}
$result = bpcab_ai_schema_generate_for_post($post_id);
if (is_wp_error($result) && defined('WP_DEBUG') && WP_DEBUG) {
error_log('[BPCAB AI Schema] save_post error: ' . $result->get_error_code() . ' - ' . $result->get_error_message());
}
}
add_action('save_post', 'bpcab_ai_schema_on_save_post', 20, 3);
function bpcab_ai_schema_print_jsonld(): void {
if (is_admin()) {
return;
}
if (!is_singular(bpcab_ai_schema_allowed_post_types())) {
return;
}
$post_id = get_queried_object_id();
if (!$post_id) {
return;
}
$json = get_post_meta($post_id, bpcab_ai_schema_meta_key(), true);
if (!is_string($json) || $json === '') {
return;
}
$decoded = json_decode($json, true);
if (!is_array($decoded)) {
return;
}
$out = wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($out === false) {
return;
}
echo "<script type="application/ld+json">n";
echo $out;
echo "n</script>n";
}
add_action('wp_head', 'bpcab_ai_schema_print_jsonld', 99);
function bpcab_ai_schema_register_rest_route(): void {
register_rest_route('bpcab/v1', '/schema/regenerate/(?P<id>d+)', array(
'methods' => 'POST',
'permission_callback' => function (WP_REST_Request $request) {
if (!is_user_logged_in()) {
return false;
}
return current_user_can('edit_posts');
},
'callback' => function (WP_REST_Request $request) {
$post_id = (int) $request['id'];
if ($post_id <= 0) {
return new WP_REST_Response(array('ok' => false, 'error' => 'ID invalide'), 400);
}
$post = get_post($post_id);
if (!$post) {
return new WP_REST_Response(array('ok' => false, 'error' => 'Post introuvable'), 404);
}
if (!current_user_can('edit_post', $post_id)) {
return new WP_REST_Response(array('ok' => false, 'error' => 'Accès refusé'), 403);
}
$result = bpcab_ai_schema_generate_for_post($post_id);
if (is_wp_error($result)) {
return new WP_REST_Response(array(
'ok' => false,
'error' => $result->get_error_message(),
'code' => $result->get_error_code(),
'data' => $result->get_error_data(),
), 500);
}
return new WP_REST_Response(array('ok' => true), 200);
},
));
}
add_action('rest_api_init', 'bpcab_ai_schema_register_rest_route');
Код Пояснение
Зачем хранить данные в метатегах поста?
Метаданные записи обеспечивают стабильное состояние. Если API искусственного интеллекта выйдет из строя, ваш JSON-LD всё равно будет отображаться. А если у вас есть кэширование страниц (Varnish, плагин кэширования), вы избежите колебаний.
Зачем нужна дополнительная временная "блокировка"?
На сайтах, использующих Elementor или Divi, действие «Обновить» может инициировать несколько сохранений (автосохранение, редактирование, обновление). Даже если вы отфильтруете по автосохранению/редактированию, я видел двойные вызовы через плагины, которые «повторно сохраняют» запись. Временное сохранение предотвращает двойное списание средств.
Почему проверка намеренно сведена к минимуму?
Schema.org — это обширная система. Если слишком строго проверять данные, можно свести на нет полезные улучшения (например, about en Thing vs DefinedTermЗдесь мы просто проверяем инварианты (@context, @graph) и мы отклоняем подозрительный контент.
Почему бы нам не использовать функцию wp_kses_post() для JSON?
wp_kses_post() Это HTML-фильтр. При применении к JSON он разбивает символы и делает JSON недействительным. Вместо этого мы используем PHP-массив, проверяем его структуру, а затем кодируем с помощью wp_json_encode().
Реалистичные ошибки, которые я часто вижу
- Код вставлен в файл functions.php При обновлении родительской темы: потерянный код. Используйте mu-plugin.
- Забыли поставить точку с запятой? в
wp-config.phpпослеdefine()→ Немедленная фатальная ошибка. - Неподходящий зацеп (Например,
the_content) → Вызов ИИ для рендеринга → задержка + затраты. - Производственные испытания Без ограничения типа записи → вы можете сгенерировать 2000 записей одновременно с помощью цикла сохранения.
Стоимость и оптимизация API
Стоимость зависит от модели и объема отправляемого контента. При ограничении в 12 000 символов текста (что часто соответствует 2000–3000 словам без HTML) это запрос средней сложности.
Реалистичная оценка (порядок величины)
- 1 статья = 1 приглашение к публикации от ИИ + 1 приглашение за каждое значимое обновление.
- Если вы публикуете 30 статей в месяц и обновляете каждую в среднем 2 раза: ~90 звонков в месяц.
Точную информацию о ценах смотрите на официальных страницах (цены могут меняться). OpenAI: Цены OpenAI.
Оптимизации, которые действительно работают
- Уменьшите входные данные Отправьте выдержку и заголовки H2/H3, а не весь текст целиком (если ваш текст очень длинный).
- «Мини» модель Более чем достаточно для извлечения ключевых слов/информации/упоминаний.
- Условная регенерация : сравнить хеш содержимого (метаданных поста) и перегенерировать его только в случае изменения хеша.
- Пакетная обработка в автономном режиме (WP-CLI) для миграций вместо массового сохранения сообщений.
Расширенные варианты и сценарии использования
Вариант 1 — перегенерация только в случае изменения содержимого (хеша).
Чтобы избежать оплаты за исправление запятой в заголовке, сохраните хеш-таблицу.
function bpcab_ai_schema_hash_meta_key(): string {
return '_bpcab_ai_schema_content_hash';
}
function bpcab_ai_schema_should_regenerate(int $post_id, array $payload): bool {
$hash = hash('sha256', ($payload['title'] ?? '') . '|' . ($payload['excerpt'] ?? '') . '|' . ($payload['content'] ?? ''));
$old = get_post_meta($post_id, bpcab_ai_schema_hash_meta_key(), true);
if (!is_string($old) || $old === '') {
update_post_meta($post_id, bpcab_ai_schema_hash_meta_key(), $hash);
return true;
}
if (!hash_equals($old, $hash)) {
update_post_meta($post_id, bpcab_ai_schema_hash_meta_key(), $hash);
return true;
}
return false;
}
Вариант 2 — совместимость с Divi 5 / Elementor / Avada
Эти разработчики часто хранят контент в post_content с внутренними шорткодами/JSON. Если вы отправите это в неизменном виде ИИ, он сможет извлечь артефакты.
- Дива 5 Иногда вы будете видеть внутренние структуры.
wp_strip_all_tags()Помогает, но не всегда. - Elementor Часть контента содержится в метаданных (данные Elementor). Финальное отображение более точно соответствует оригиналу, чем исходная версия.
- Авада Проблема наблюдается и с шорткодами Fusion Builder.
Два подхода:
- «Безопасный» подход (рекомендуется): извлечь только видимый текст с помощью
the_contentотфильтрованы, затем удалены метки. - «Быстрый» подход : держать
post_contentи смиритесь с шумом.
«Безопасный» вариант (будьте осторожны, не используйте это в цикле для списков, оставьте его для save_post):
function bpcab_ai_schema_get_rendered_text(WP_Post $post): string {
// Applique les filtres (shortcodes, blocs, builders) pour obtenir un HTML proche du front.
$html = apply_filters('the_content', $post->post_content);
// Supprime scripts/styles éventuels.
$html = preg_replace('#<scriptb[^>]*>.*?</script>#is', '', $html ?? '');
$html = preg_replace('#<styleb[^>]*>.*?</style>#is', '', $html ?? '');
$text = wp_strip_all_tags($html);
return mb_substr($text, 0, 12000);
}
Вариант 3 — добавить FAQPage, если статья содержит раздел часто задаваемых вопросов (FAQ).
Если ваши статьи часто заканчиваются фразой «FAQ» (Часто задаваемые вопросы), ИИ может обнаружить пары «вопрос-ответ». Но будьте строги: вымышленный FAQ представляет собой риск для SEO и редакционной работы. Я рекомендую создавать FAQ только в том случае, если контент уже содержит явные вопросы.
Вы можете добавить ограничение к запросу: «извлекать только те вопросы, которые уже присутствуют, слово в слово».
Безопасность и передовой опыт
Никогда не раскрывайте ключ на стороне клиента.
Категорически избегайте вызовов JavaScript из браузера к API ИИ. Ключ будет раскрыт (инструменты разработчика, исходный код, логи). Здесь же всё проходит через PHP. wp_remote_post().
Ограничение скорости
Временная «блокировка» — это только начало. Если у вас сайт с несколькими авторами, добавьте ограничение на количество запросов для каждого пользователя (например, один временный запрос на каждый идентификатор пользователя) в REST-интерфейс.
Проверка ввода
Не позволяйте пользователю вводить произвольный текст, отправляемый в ИИ через неконтролируемый REST-параметр. В данном случае конечная точка принимает post_id и восстановил полезную нагрузку из WordPress.
GDPR / конфиденциальность
- Не отправляйте ненужные персональные данные (электронные адреса, IP-адреса, личные поля).
- Избегайте отправки комментариев или форм без четкого правового обоснования.
- При необходимости задокументируйте информацию о субподрядчике (OpenAI/Anthropic и т. д.) в вашем реестре и политике конфиденциальности.
Совместимость с кэшем
Если вы используете агрессивное кэширование страниц, JSON-LD внедряется через wp_head Как и всё остальное, данные будут кэшироваться. В этом и заключается их предназначение. Проблема в том, что при перегенерации метаданных забывают очистить кэш (кэш плагина/CDN). В этом случае вы будете видеть старую схему в течение нескольких часов.
Как тестировать и отлаживать
1) Сначала проведите локальное тестирование.
Включить WP_DEBUG et WP_DEBUG_LOG в wp-config.php. Ссылка : Отладка в WordPress.
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
2) Убедитесь, что JSON-LD выводится корректно.
- Откройте страницу статьи.
- Посмотреть исходный код.
- поиск
application/ld+json.
3) Проверьте REST-интерфейс (регенерацию).
Из браузера (войдя в систему как администратор) вы можете вызвать функцию через fetch в консоли или через curl, используя nonce. Пример curl (если вы получили nonce в панели администратора):
curl -X POST "https://example.com/wp-json/bpcab/v1/schema/regenerate/123"
-H "X-WP-Nonce: VOTRE_NONCE"
-H "Content-Type: application/json"
4) Проверьте JSON-LD
Используйте валидатор Schema.org или инструменты тестирования с расширенными результатами. Я не даю вам ссылку на "SEO-блог", а предоставляю структурированные ссылки:
Если это не сработает
Когда возникают проблемы, причина почти всегда одна из следующих: ключ, квота, некорректный JSON или слишком частое срабатывание хука.
| симптом | Причина вероятна | проверка | Решение |
|---|---|---|---|
| В исходном коде отсутствует скрипт JSON-LD. | Пустые метаданные (никогда не генерируются) или post_status ≠ publish | Посмотрите на мета _bpcab_ai_schema_jsonld (с помощью отладочного плагина) |
Опубликуйте статью, затем сгенерируйте ее заново через REST-интерфейс. |
| Журналы ошибок: HTTP 401 / 403 | Отсутствует/неверный ключ API | WP_DEBUG_LOGкод ошибки в debug.log |
Правильный BPCAB_AI_OPENAI_API_KEY в wp-config.php |
| Журнал событий: таймаут | Медленный API / Хостинг-провайдер блокирует исходящие запросы | Тест а wp_remote_get() на общедоступный сайт |
Небольшое увеличение timeoutПроверьте настройки брандмауэра и разрешите доступ к api.openai.com. |
| Ошибка: «Сгенерированный контент не является допустимым JSON-файлом» | Искусственный интеллект вернул текст, окружающий JSON-данные. | Осмотреть generated в ошибке (отладка) |
Сделайте условия более строгими, продолжайте text.formatснизить температуру |
| Две диаграммы. Статья на странице. | SEO-плагин уже внедряет статью | HTML-код: поиск нескольких "@type":"Article" |
Измените схему на «фрагмент» или, если возможно, отключите вывод статей в SEO-плагине. |
Конкретные подводные камни WordPress
- Сниппет не работает из-за плагина для сниппетов. Некоторые плагины изменяют порядок загрузки. Использование mu-плагинов снижает этот риск.
- Версия PHP слишком старая Если сайт всё ещё работает на PHP 7.x, вы будете получать ошибки типов. Лучше использовать PHP 8.1 и выше.
- Приоритет крючка Если другой плагин изменяет содержимое после выполнения команды save_post, ваша схема может "отставать". Измените приоритет (20 → 30) или перегенерируйте схему через конечную точку.
Ресурсы
- WordPress: wp_remote_post()
- WordPress: API временных данных
- WordPress: Руководство по REST API
- Ядро WordPress (зеркало) – GitHub
- WordPress Core Trac
- PHP: json_decode()
- OpenAI: API ответов
- OpenAI: Цены
- Schema.org: Статья
FAQ
Google автоматически «вознаграждает» за структурированные данные, сгенерированные искусственным интеллектом?
Нет. Разметка помогает в интерпретации, но если контент непоследователен, вы ничего не выиграете. Настоящая выгода заключается в... консистенция и точность в больших масштабах.
Опасно ли внедрять JSON-LD, созданный моделью?
Да, если позволить ИИ изобретать. Именно поэтому код принудительно использует значения WordPress для критически важных полей и проверяет структуру перед внедрением.
Можно ли использовать Anthropic или Mistral вместо них?
Да. Сохраняйте ту же архитектуру: строгая JSON-запрос, wp_remote_post()валидация json_decode()Изменяется только формат запроса/ответа.
Почему бы не создать полную схему «Организация/Веб-сайт/Хлебные крошки»?
Поскольку эти элементы часто уже управляются SEO-плагином и поскольку они применяются ко всему сайту (а не к каждой статье отдельно), смешивание двух источников приводит к несоответствиям.
Как избежать дубликатов в Yoast/Rank Math/SEOPress?
Два реалистичных варианта:
- создать схему, которая не дублирует статью (например,
DefinedTermSetилиThingсвязанный) - Отключите вывод схемы SEO-плагина (если такая опция существует) и позвольте вашему плагину управлять статьей.
Могу ли я сгенерировать схему для страниц (тип записи — страница)?
Да, но не нужно навязывать "Статью". На некоторых страницах можно попросить ИИ выбрать один из вариантов. WebPage, AboutPage, ContactPage. Добавлять page в bpcab_ai_schema_allowed_post_types() и адаптируйте подсказку.
Почему моя диаграмма не отображается на странице Elementor?
Часто это происходит потому, что вы тестируете предварительный просмотр или шаблон, а не что-то конкретное. is_singular() классический вариант. Проверьте конечный публичный URL, затем проверьте источник. Если содержимое на стороне post_content «пустое», используйте вариант «отображаемый текст» в зависимости от apply_filters('the_content', ...).
Могу ли я отобразить диаграмму в панели администратора для проверки?
Да. Добавьте метабокс только для чтения, который будет отображать JSON. Избегайте предоставления возможности редактирования этого поля, иначе вы потеряете контроль над проверкой данных.
Что мне делать, если API иногда возвращает некорректный JSON?
уменьшить temperatureУлучшите запрос («отвечайте только JSON-объектом») и оставьте проверку на стороне PHP. В продакшене я предпочитаю «отсутствие схемы» неработающей схеме.
Как перенести старый сайт с 2000 статьями?
Не запускайте 2000 резервных копий. Добавьте команду WP-CLI или пакетный скрипт, который обрабатывает резервные копии партиями (по 50 записей) с паузой и соблюдает лимит запросов. При желании я могу предоставить вам версию WP-CLI, основанную на... WP_CLI::add_command() адаптировано для этого плагина.