Ak ste niekedy „ručne“ vložili súbor JSON-LD Schema.org do článku, videli ste skutočný problém: po 30 publikáciách je nekonzistentný, neúplný a nikto sa neodváži ho udržiavať.

Potreba / Prípad použitia

Štruktúrované dáta (JSON-LD) pomáhajú spoločnosti Google a iným vyhľadávačom presne pochopiť obsah: typ článku, autora, dátum publikácie, hlavný obrázok, entitu „o nás“, najčastejšie otázky atď. WordPressUž máte veľa spoľahlivých informácií (názov, úryvok, hlavný obrázok). Medzera predstavuje „význam“: subjekty, entity, zámery a niekedy aj správne typ Schema.org (Article vs. TechArticle vs. NewsArticle atď.).

AI je tu užitočná na vytvorenie sémantická vrstva na základe obsahu, sans Či už strávite 10 minút na článok výberom kľúčových slov, entít alebo sekcií „o nás“. Podľa mojich skúseností je to obzvlášť výhodné na:

  • Technické blogy (WordPress, vývoj, dáta): AI dobre extrahuje technológie, verzie a koncepty.
  • Redakčné webové stránky s mnohými editormi: štandardizujte značkovanie bez toho, aby ste museli všetkých zaškoliť na Schema.org.
  • „Obsah“ webových stránok elektronického obchodu (sprievodcovia, porovnania): obohatiť články bez zmeny popisov produktov.

Nakoniec budete vedieť, ako implementovať zapojiť (kompatibilné s WordPress 6.9.4 / PHP 8.1+), ktoré:

  • generuje súbor JSON-LD Schema.org pre každý článok prostredníctvom rozhrania AI API (volanie wp_remote_post())
  • ukladá odpoveď do vyrovnávacej pamäte (Transients API)
  • Regeneruje sa iba v prípade potreby (publikácia/aktualizácia).
  • vloží JSON-LD do <head> predná strana
  • Spracováva chyby (časové limity, kvóty, neplatný JSON) s čistými záložnými riešeniami.

Rýchle zhrnutie

  • Generujeme JSON-LD poštou prostredníctvom umelej inteligencie, potom my zásoby v meta príspevku (a vložíme prechodný (okrem toho, aby sa predišlo opakovaným hovorom).
  • Kľúč API je v wp-config.php via define(), nikdy nie v trvalej forme.
  • Voláme AI API s wp_remote_post() + timeout + spracovanie chýb.
  • Nútime umelú inteligenciu poslať späť Prísne JSON (a overíme to na strane PHP).
  • Skript JSON-LD vložíme cez wp_head (vpredu) a vyhýbame sa administrátorovi.
  • Pridáme REST endpoint iba pre správcu pre regenerovať na požiadanie (praktické v QA).

Kedy na to použiť umelú inteligenciu

Použite umelú inteligenciu, ak skutočne potrebujete sémantické obohatenie, nielen „všade umiestniť článok“. Dobré prípady použitia:

  • Dlhý obsah (viac ako 1000 slov), kde extrakcia entít (značky, nástroje, koncepty) prináša presnosť.
  • Neúplné taxonómie (nespoľahlivé kategórie/značky) a chcete prehľadnejšie polia „o mne/zmienky“.
  • Tímové písanie s heterogénnymi štýlmi: AI normalizuje.
  • Migrácia SEO (nová téma, nový SEO plugin): môžete vygenerovať konzistentnú schému bez prepisovania príspevkov.

Často som videl výhodu na stránkach, kde je úryvok WordPressu prázdny a kde sa autori veľa menia: umelá inteligencia vytvára konzistentný popis, ktorý sa vyhýba náhodnému „úryvku“.

Kedy NEPOUŽÍVAŤ AI

Vyhnite sa umelej inteligencii, ak je vaša schéma čisto mechanická a už deterministická.

  • Prezentačné webové stránky s 10 stranami: urobte to manuálne alebo pomocou SEO pluginu.
  • Jednoduché diagramy (Organizácia, Webová stránka, Navigačný zoznam) už spravované vaším SEO doplnkom.
  • Citlivý obsah (zdravotníctvo, právo), ak sa spoliehate na umelú inteligenciu pri „vynájdení“ nehnuteľností. V tomto prípade musí umelá inteligencia zostať ťažobná, nie kreatívna.
  • Napätý rozpočet a veľké množstvo príspevkov publikovaných každý deň: náklady na API sa môžu zvýšiť, ak budete generovať údaje príliš často.

Klasickým anti-vzorom je volanie umelej inteligencie pri každom zobrazení stránky. To vedie k časovým limitom, zbytočným faktúram a niekedy aj k prázdnym stránkam, ak je kód zle chránený.

predpoklady

Cieľové prostredie: WordPress 6.9.4 (apríl 2026) a PHP 8.1+.

Kľúč API a úložisko

Môžete použiť OpenAI, Anthropic, Mistral alebo Google. Uvediem príklad OpenAI (API Responses), pretože je veľmi stabilný na strane striktného JSON, ale štruktúra pluginu umožňuje ľahko nahradiť poskytovateľa.

Uschovajte si kľúč v wp-config.php (alebo ešte lepšie, premenná prostredia vložená vaším poskytovateľom hostingu). Príklad:

/**
 * 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');

Rozšírenia PHP

  • cURL (často povolené) alebo allow_url_fopen (WordPress používa Requests, ktoré sa spoliehajú na cURL, ak je k dispozícii).
  • JSON (štandard).

Užitočné oficiálne zdroje

Architektúra riešenia

Textový prúd používaný pluginom:

WordPress editor (save_post) → príprava údajov (názov, obsah, úryvok, obrázok, autor) → wp_remote_post() do AI API → JSON odpoveď → validácia/dezinfekcia → metadáta príspevku + prechodné úložisko → vkladanie údajov do front-endu (wp_head) …

Prečo tento pracovný postup funguje dobre v produkcii

  • Generácia v čase sporenia (alebo na požiadanie), nie na displeji: ak je rozhranie API umelej inteligencie pomalé, neblokujete vykresľovanie na front-ende.
  • Cache krátky prechodový jav zabráni cyklickým regeneráciám, keď editor 5-krát klikne na tlačidlo „Aktualizovať“.
  • Meta príspevku : perzistentné, exportovateľné a verziovateľné (ak máte systém pre staging).
  • Overenie JSON Ak AI vráti poškodený text alebo JSON, nič sa nevloží (záložná funkcia).

Dôležitá poznámka: SEO pluginy a duplikáty

Yoast, Rank Math, SEOPress atď. už vkladajú JSON-LD. Ak pridáte svoje vlastné, riskujete:

  • duplikáty (dva Article)
  • nezrovnalosti (dvaja autori, dva obrázky)

Stratégia, ktorú odporúčam: vstreknúť „doplnková“ schéma (napr. about, mentions, keywords, audience) v jednom Article ktoré ovládate, alebo inak vytvárate @graph čistý. Nasledujúci kód generuje @graph minimálny a vyhýba sa „znovuobjavovaniu“ organizácie/webovej stránky.

Kompletný kód – krok za krokom

Odporúčam ti to dať tam mu-plugin Ak chcete, aby prežil zmeny témy a „náhodné deaktivácie“. Inak štandardný plugin.

Krok 1 – Minimálna štruktúra pluginu

Vytvorte súbor: wp-content/mu-plugins/bpcab-ai-schema.php (v prípade potreby vytvorte priečinok).

<?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;
}

Krok 2 – Konštanty, možnosti a ochranné opatrenia

Zabezpečujeme to od začiatku: ak kľúč neexistuje, o nič sa nepokúšame. Často som videl stránky opakovane vracať chyby 401, pretože kód sa stále pokúšal o zadanie aj napriek chýbajúcemu kľúču.

/**
 * 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');
}

Krok 3 – Extrakcia „spoľahlivých“ údajov z WordPressu

Umelá inteligencia by si nemala vymýšľať dátumy, autorov ani URL adresy. Prevezmeme ich z WordPressu a potom už len požiadame umelú inteligenciu o sémantické obohatenie.

/**
 * 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'),
	);
}

Krok 4 – Výzva AI „strict JSON“ + volanie API cez wp_remote_post()

Problém, ktorý narúša väčšinu implementácií: umelá inteligencia vracia text okolo JSON alebo nekompatibilné polia. Vynucujeme prísny formát a potom overujeme.

Príklad s OpenAI (koncový bod odpovede). Oficiálna referencia API: Rozhranie API odpovedí 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;
}

Krok 5 – Validácia a sanitácia JSON-LD

JSON sa „nedezinfikuje“ ako HTML. Správny prístup je overiť minimálnu štruktúru, odstrániť všetko nebezpečné (skripty) a správne kódovať v čase zobrazenia.

Časté úskalie: používanie wp_kses_post() v súbore JSON. Toto preruší úvodzovky a súbor JSON bude neplatný. Tu ho overíme ako pole a potom 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;
}

Krok 6 – prechodná vyrovnávacia pamäť + úložisko po metadátach

Kombinujeme dve úrovne:

  • post meta (perzistentné) pre predný displej
  • prechodný (krátke), aby sa zabránilo príliš rýchlej regenerácii
/**
 * 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;
}

Krok 7 – zaháknutie save_post (bez prerušenia editora)

Nesprávny háčik alebo nesprávna podmienka a voláte umelú inteligenciu pri automatických ukladaniach, revíziách alebo ukážkach Elementoru. Vidím to stále.

/**
 * 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);

Krok 8 – vloženie JSON-LD do wp_head

Vkladáme iba na front-end, na jednotlivé uzly a iba ak meta uzol existuje. Tu nie sú žiadne volania umelej inteligencie.

/**
 * 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);

Krok 9 – Koncový bod REST pre regeneráciu na požiadanie (iba pre správcu)

Veľmi užitočné, keď editor oznámi, že „schéma sa nezobrazuje“ a vy chcete schému regenerovať bez opätovného uloženia príspevku (a bez poskytnutia prístupu ku kódu). Chránime ju pomocou capabilities + 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');

Kompletný zostavený kód

Skopírujte a vložte tento súbor tak, ako je, do wp-content/mu-plugins/bpcab-ai-schema.phpPotom pridajte konštantu do wp-config.phpNetestujte to najprv v produkcii bez zálohy: jedna zabudnutá zátvorka a stránka je nefunkčná. biela obrazovka.

<?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');

Vysvetlenie kódu

Prečo ukladať do meta údajov príspevku?

Meta príspevku poskytuje stabilný stav. Ak rozhranie AI API zlyhá, váš JSON-LD sa bude stále zobrazovať. A ak máte vyrovnávaciu pamäť stránky (Varnish, doplnok vyrovnávacej pamäte), vyhnete sa výkyvom.

Prečo dodatočný prechodný „zámok“?

Na stránkach používajúcich Elementor alebo Divi môže akcia „Aktualizovať“ spustiť viacero uložení (automatické uloženie, revízia, aktualizácia). Aj keď filtrujete automatické uloženie/revíziu, videl som dvojité volania prostredníctvom pluginov, ktoré „znovu ukladajú“ príspevok. Tento prechodný mechanizmus zabraňuje dvojitému fakturovaniu.

Prečo je validácia zámerne minimálna?

Schema.org je rozsiahly. Ak overujete príliš prísne, porušíte užitočné vylepšenia (napr. about en Thing vs DefinedTermTu len skontrolujeme invarianty (@context, @graph) a podozrivý obsah odmietame.

Prečo nepoužívame wp_kses_post() na JSON?

wp_kses_post() je HTML filter. Pri použití na JSON sa znaky prerušia a JSON sa stane neplatným. Namiesto toho ponecháme PHP pole, skontrolujeme jeho štruktúru a potom ho zakódujeme pomocou wp_json_encode().

Realistické chyby, ktoré často vidím

  • Kód vložený do súboru functions.php Z nadradenej témy: aktualizácia témy = stratený kód. Použite mu-plugin.
  • Zabudnutie na bodkočiarku v wp-config.php po define() → Okamžitá fatálna chyba.
  • Nevhodný háčik (Napr. the_content) → Volanie umelej inteligencie na vykresľovanie → latencia + náklady.
  • Testovanie výroby bez obmedzenia typu príspevku → regenerujete 2000 príspevkov naraz pomocou slučky ukladania.

Náklady na API a optimalizácia

Cena závisí od modelu a veľkosti odoslaného obsahu. S limitom 12 000 znakov textu (čo často predstavuje 2 000 – 3 000 slov bez HTML) ide o miernu požiadavku.

Realistický odhad (rádová hodnota)

  • 1 článok = 1 výzva umelej inteligencie na publikovanie + 1 výzva na každú významnú aktualizáciu.
  • Ak publikujete 30 článkov mesačne a každý z nich aktualizujete v priemere 2-krát: ~90 hovorov mesačne.

Presné ceny nájdete na oficiálnych stránkach (menia sa). OpenAI: Ceny OpenAI.

Optimalizácie, ktoré skutočne fungujú

  • Znížte vstup : pošlite výňatok + nadpisy H2/H3, nie celý obsah (ak je váš obsah veľmi dlhý).
  • Model „Mini“ viac než dosť na extrakciu kľúčových slov/o nás/zmien.
  • Podmienená regenerácia : porovnať hash obsahu (meta hodnoty príspevku) a regenerovať iba v prípade zmeny hashu.
  • Dávkovanie offline (WP-CLI) pre migrácie namiesto hromadného save_post.

Pokročilé varianty a prípady použitia

Variant 1 – regenerovať iba v prípade zmeny obsahu (hash)

Aby ste sa vyhli plateniu za opravu čiarky v názve, uložte si hash.

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;
}

Variant 2 – kompatibilita s Divi 5 / Elementor / Avada

Títo tvorcovia často ukladajú obsah do post_content s internými shortcodes/JSON. Ak to odošlete umelej inteligencii tak, ako je, dokáže extrahovať artefakty.

  • Divi 5 Niekedy uvidíte vnútorné štruktúry. wp_strip_all_tags() pomôcť, ale nie vždy.
  • Elementor Časť obsahu je v metadátach (dáta Elementoru). Výsledné vykreslenie je vernejšie ako surová verzia.
  • Avada Krátke kódy Fusion Builderu, rovnaký problém.

Dva prístupy:

  • „Bezpečný“ prístup (odporúčané): extrahovať iba viditeľný text pomocou the_content filtrované a potom odstránené značky.
  • „Rýchly“ prístup : ponechať post_content a prijať hluk.

„Bezpečná“ verzia (pozor, nerobte to v slučke na zoznamoch, ponechajte si ju pre 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);
}

Možnosť 3 – pridajte stránku s najčastejšími otázkami, ak článok obsahuje sekciu s najčastejšími otázkami

Ak vaše články často končia slovom „FAQ“, umelá inteligencia dokáže odhaliť dvojice otázok a odpovedí. Buďte však prísni: vymyslená FAQ predstavuje SEO a redakčné riziko. Odporúčam generovať FAQ iba v prípade, že obsah už obsahuje explicitné otázky.

Do výzvy môžete pridať obmedzenie: „extrahovať iba otázky, ktoré sú už prítomné slovo od slova“.

Bezpečnosť a osvedčené postupy

Nikdy nezverejňujte kľúč na strane klienta

Rozhodne sa vyhnite volaniu JavaScriptu z prehliadača do AI API. Kľúč by unikol (DevTools, zdrojový kód, logy). Tu všetko prechádza cez PHP prostredníctvom wp_remote_post().

Obmedzenie sadzby

Dočasný „zámok“ je začiatok. Ak máte stránku s viacerými autormi, pridajte na koncovom bode REST limit rýchlosti na používateľa (napr. jeden dočasný zámok na ID používateľa).

Overenie vstupu

Nedovoľte používateľovi vkladať ľubovoľný text odoslaný do AI prostredníctvom nekontrolovaného parametra REST. V tomto prípade koncový bod preberá post_id a prepracoval užitočné zaťaženie z WordPressu.

GDPR / ochrana osobných údajov

  • Neposielajte nepotrebné osobné údaje (e-maily, IP adresy, súkromné ​​polia).
  • Vyhnite sa posielaniu komentárov alebo formulárov bez jasného právneho základu.
  • V prípade potreby zdokumentujte subdodávateľa (OpenAI/Anthropic/atď.) vo svojom registri a zásadách ochrany osobných údajov.

Kompatibilita vyrovnávacej pamäte

Ak používate agresívnu vyrovnávaciu pamäť stránok, JSON-LD sa vloží cez wp_head Bude sa ukladať do vyrovnávacej pamäte ako všetko ostatné. To je zamýšľaný účel. Úskalím je regenerácia metadát a zabudnutie na vymazanie vyrovnávacej pamäte (vyrovnávacia pamäť pluginu/CDN). V takom prípade budete starú schému vidieť celé hodiny.

Ako testovať a ladiť

1) Najprv lokálny test

umožniť WP_DEBUG et WP_DEBUG_LOG v wp-config.phpReferencia: Ladenie vo WordPress.

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);

2) Overte, či je JSON-LD vygenerovaný správne

  • Otvorte stránku s článkom.
  • Pozrite si zdrojový kód.
  • Vyhľadávanie application/ld+json.

3) Otestujte koncový bod REST (regenerácia)

Z prehliadača (po prihlásení ako správca) môžete volať cez fetch v konzole alebo cez curl s použitím nonce. Príklad curl (ak načítate nonce v administrátorskom paneli):

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) Overte JSON-LD

Použite validátor Schema.org alebo nástroje na testovanie rozšírených výsledkov. Nedávam vám odkaz na „SEO blog“, ale štruktúrované referencie:

Ak to nefunguje

Keď sa to pokazí, takmer vždy je to jeden z týchto bodov: kľúč, kvóta, neplatný JSON alebo príliš často spúšťaný hook.

symptóm Príčina pravdepodobná overenie Riešenie
V zdroji nie je žiadny skript JSON-LD Prázdne metadáta (nikdy sa nevygenerujú) alebo post_status ≠ publish Pozrite sa na meta _bpcab_ai_schema_jsonld (prostredníctvom ladiaceho pluginu) Publikujte článok a potom ho regenerujte pomocou koncového bodu REST
Záznamy: HTTP 401 / 403 Chýbajúci/nesprávny kľúč API WP_DEBUG_LOG, chybový kód v debug.log Správne BPCAB_AI_OPENAI_API_KEY v wp-config.php
Záznamy: časový limit Pomalé API / Poskytovateľ hostingu blokuje odchádzajúce požiadavky Otestujte wp_remote_get() na verejné miesto Mierne zvýšiť timeoutSkontrolujte nastavenia brány firewall a povoľte prístup k api.openai.com.
Chyba: „Vygenerovaný obsah nie je platný súbor JSON“ Umelá inteligencia vrátila text okolo JSON súboru Skontrolovať generated v chybe (ladenie) Sprísnite výzvu, dodržujte text.formatznížiť teplotu
Dva diagramy. Článok na stránke. SEO plugin už vkladá článok Zdroj HTML: vyhľadajte niekoľko "@type":"Article" Zmeňte schému na „fragment“ alebo, ak je to možné, vypnite výstup článku z SEO pluginu.

Konkrétne úskalia WordPressu

  • Úryvok poškodený doplnkom pre úryvky Niektoré pluginy menia poradie načítavania. Používaním mu-pluginov toto riziko znižujete.
  • Verzia PHP je príliš stará Ak stránka stále beží na PHP 7.x, budú sa zobrazovať typové chyby. Snažte sa používať PHP 8.1+.
  • Priorita háčika Ak iný plugin upraví obsah po vašom save_post, vaša schéma môže „zaostávať“. Upravte prioritu (20 → 30) alebo ju regenerujte cez koncový bod.

zdroje

Často kladené otázky

Odmeňuje Google automaticky štruktúrované dáta generované umelou inteligenciou?

Nie. Značkovanie pomáha s interpretáciou, ale ak obsah nie je konzistentný, nebudete z toho mať žiadny úžitok. Skutočným prínosom je konzistencia a presnosť vo veľkom meradle.

Je riskantné vkladať JSON-LD vytvorený modelom?

Áno, ak necháte umelú inteligenciu vymýšľať. Preto kód vynucuje použitie hodnôt WordPressu pre kritické polia a pred vstreknutím overuje štruktúru.

Môžem namiesto toho použiť Anthropic alebo Mistral?

Áno. Zachovajte rovnakú architektúru: striktný JSON výzva, wp_remote_post()validácia json_decode()Mení sa iba formát požiadavky/odpovede.

Prečo nevygenerovať kompletnú schému Organizácia/Webová stránka/BreadcrumbList?

Keďže tieto prvky sú často už spravované SEO pluginom a pretože sú pre celú stránku (nie pre jednotlivé články), miešanie dvoch zdrojov vytvára nezrovnalosti.

Ako sa vyhnúť duplikátom pomocou Yoast/Rank Math/SEOPress?

Dve realistické možnosti:

  • vygenerovať schému, ktorá neduplikuje článok (napr. DefinedTermSet alebo Thing súvisiace)
  • Zakážte výstup schémy SEO pluginu (ak táto možnosť existuje) a nechajte svoj plugin spravovať článok.

Môžem vygenerovať schému pre stránky (typ príspevku stránka)?

Áno, ale nevnucujte „Článok“. Na niektorých stránkach môžete požiadať umelú inteligenciu, aby si vybrala medzi WebPage, AboutPage, ContactPagePridať page v bpcab_ai_schema_allowed_post_types() a upravte výzvu.

Prečo sa môj diagram nezobrazuje na stránke Elementoru?

Často preto, že testujete ukážku alebo šablónu, nie is_singular() klasika. Otestujte konečnú verejnú URL adresu a potom skontrolujte zdroj. Ak je obsah na strane post_content „prázdny“, použite variant „vykreslený text“ založený na apply_filters('the_content', ...).

Môžem si zobraziť diagram v administrátorskom paneli na overenie?

Áno. Pridajte metabox iba na čítanie, ktorý zobrazuje JSON. Nenastavujte toto pole na úpravu, inak stratíte kontrolu nad overovaním.

Čo mám robiť, ak API niekedy vráti neplatný JSON?

Znížiť temperatureVylepšite výzvu („odpovedať iba s objektom JSON“) a ponechajte validáciu na strane PHP. V produkčnom prostredí uprednostňujem „žiadnu schému“ pred nefunkčnou schémou.

Ako migrovať starú webovú stránku s 2000 článkami?

Nespúšťajte 2000 záloh. Pridajte príkaz WP-CLI alebo dávkový skript, ktorý spracováva zálohy v dávkach (50 príspevkov) s pauzou a rešpektuje limit rýchlosti. Ak chcete, môžem vám poskytnúť verziu WP-CLI založenú na WP_CLI::add_command() prispôsobené pre tento plugin.