หากคุณเคยคัดลอก JSON-LD Schema.org ลงในบทความด้วยตนเอง คุณจะเห็นปัญหาที่แท้จริง: หลังจากตีพิมพ์ไป 30 ครั้ง มันจะไม่สอดคล้องกัน ไม่สมบูรณ์ และไม่มีใครกล้าที่จะดูแลรักษาอีกต่อไป

ความต้องการ / กรณีการใช้งาน

ข้อมูลที่มีโครงสร้าง (JSON-LD) ช่วยให้ Google และเครื่องมือค้นหาอื่นๆ เข้าใจเนื้อหาได้อย่างแม่นยำยิ่งขึ้น เช่น ประเภทบทความ ผู้เขียน วันที่เผยแพร่ รูปภาพหลัก ส่วน "เกี่ยวกับ" คำถามที่พบบ่อย เป็นต้น WordPressคุณมีข้อมูลที่น่าเชื่อถืออยู่แล้วมากมาย (ชื่อเรื่อง บทคัดย่อ รูปภาพประกอบ) สิ่งที่ยังขาดอยู่คือ "ความหมาย" ได้แก่ หัวข้อ เนื้อหา เจตนา และบางครั้งก็รวมถึงสิ่งที่ถูกต้องด้วย ชนิด Schema.org (เช่น บทความทั่วไป บทความทางเทคนิค บทความข่าว เป็นต้น)

AI มีประโยชน์ในที่นี้ในการสร้าง ชั้นความหมาย โดยพิจารณาจากเนื้อหา Sans ไม่ว่าคุณจะใช้เวลา 10 นาทีต่อบทความในการเลือกคำหลัก เอนทิตี หรือส่วน "เกี่ยวกับ" ก็ตาม จากประสบการณ์ของผม วิธีนี้มีประโยชน์อย่างยิ่งในกรณีต่อไปนี้:

  • บล็อกทางเทคนิค (WordPress(dev, data): AI สามารถดึงข้อมูลเทคโนโลยี เวอร์ชัน และแนวคิดต่างๆ ได้อย่างดีเยี่ยม
  • เว็บไซต์บรรณาธิการ ด้วยบรรณาธิการจำนวนมาก: กำหนดมาตรฐานมาร์กอัปโดยไม่ต้องฝึกอบรมทุกคนเกี่ยวกับ Schema.org
  • “เนื้อหา” ของเว็บไซต์อีคอมเมิร์ซ (คู่มือ, การเปรียบเทียบ): เพิ่มเนื้อหาให้กับบทความโดยไม่ต้องเปลี่ยนแปลงคำอธิบายผลิตภัณฑ์

สุดท้ายแล้ว คุณจะรู้วิธีนำไปใช้ เสียบเข้าไป (ใช้งานร่วมกับ WordPress 6.9.4 / PHP 8.1 ขึ้นไป) ซึ่งประกอบด้วย:

  • สร้าง JSON-LD Schema.org สำหรับแต่ละบทความผ่าน AI API (เรียกใช้) wp_remote_post())
  • แคชการตอบสนอง (Transients API)
  • ระบบจะสร้างข้อมูลใหม่เฉพาะเมื่อจำเป็นเท่านั้น (เช่น การเผยแพร่/การอัปเดต)
  • แทรก JSON-LD เข้าไปใน <head> ด้านหน้า
  • ระบบนี้จัดการข้อผิดพลาด (หมดเวลา, โควต้า, JSON ไม่ถูกต้อง) ด้วยกลไกสำรองที่เรียบร้อย

สรุปด่วน

  • เราสร้าง JSON-LD โดยทางไปรษณีย์ผ่าน AI จากนั้นเรา คลังสินค้า ในโพสต์เมตา (และเราได้ใส่ ชั่วคราว (นอกจากนี้เพื่อหลีกเลี่ยงการโทรซ้ำ)
  • คีย์ API อยู่ใน WP-config.php ผ่านทาง define()ไม่เคยอยู่ในรูปแบบถาวร
  • เราเรียก AI API ว่า wp_remote_post() + การหยุดพักชั่วคราว + การจัดการข้อผิดพลาด
  • เราบังคับให้ AI ส่งข้อมูลกลับมา JSON ที่เข้มงวด (และเราตรวจสอบความถูกต้องในฝั่ง PHP)
  • เราแทรกสคริปต์ JSON-LD ผ่านทาง wp_head (ด้านหน้า) และเราหลีกเลี่ยงผู้ดูแลระบบ
  • เราเพิ่ม REST endpoint สำหรับผู้ดูแลระบบเท่านั้น งอกใหม่ ตามความต้องการ (ใช้งานได้จริงในงานควบคุมคุณภาพ)

ควรใช้ AI สำหรับเรื่องนั้นเมื่อใด

ใช้ AI หากคุณมีความต้องการที่แท้จริงในการเพิ่มความหมายเชิงลึก ไม่ใช่แค่ "ใส่คำว่า 'บทความ' ทุกที่" ตัวอย่างการใช้งานที่ดี:

  • เนื้อหายาว (มากกว่า 1000 คำ) ซึ่งการแยกแยะองค์ประกอบ (แบรนด์ เครื่องมือ แนวคิด) ทำให้เกิดความแม่นยำ
  • อนุกรมวิธานที่ไม่สมบูรณ์ (หมวดหมู่/แท็กที่ไม่น่าเชื่อถือ) และคุณต้องการช่อง "เกี่ยวกับ/การกล่าวถึง" ที่สะอาดตามากขึ้น
  • การเขียนร่วมกันของทีม ด้วยรูปแบบที่หลากหลาย: AI จะทำการปรับให้เป็นมาตรฐาน
  • การย้าย SEO (ธีมใหม่ ปลั๊กอิน SEO ใหม่): คุณสามารถสร้าง Schema ที่สอดคล้องกันได้โดยไม่ต้องเขียนโพสต์ใหม่

ฉันมักเห็นประโยชน์ของ AI ในเว็บไซต์ที่ส่วนคำอธิบายย่อของ WordPress ว่างเปล่าและผู้เขียนมีการเปลี่ยนแปลงบ่อย: AI สร้างคำอธิบายที่สอดคล้องกันซึ่งช่วยหลีกเลี่ยง "คำอธิบายย่อ" แบบสุ่ม

เมื่อใดที่ไม่ควรใช้ AI

ควรหลีกเลี่ยงการใช้ AI หากระบบของคุณเป็นแบบเชิงกลล้วนๆ และคาดเดาผลลัพธ์ได้อยู่แล้ว

  • เว็บไซต์แสดงผลงาน สำหรับเว็บไซต์ที่มี 10 หน้า: ทำได้ด้วยตนเองหรือใช้ปลั๊กอิน SEO
  • แผนภาพอย่างง่าย (องค์กร, เว็บไซต์, รายการเส้นทาง) ได้รับการจัดการโดยปลั๊กอิน SEO ของคุณแล้ว
  • เนื้อหาที่อาจก่อให้เกิดความอ่อนไหว (ด้านสุขภาพ กฎหมาย) หากคุณพึ่งพา AI ในการ "สร้าง" คุณสมบัติ ในกรณีนี้ AI ต้องทำหน้าที่ดึงข้อมูล ไม่ใช่สร้างสรรค์
  • งบประมาณจำกัด และปริมาณโพสต์จำนวนมากที่เผยแพร่ทุกวัน: ค่าใช้จ่าย API อาจเพิ่มขึ้นหากคุณสร้างใหม่บ่อยเกินไป

รูปแบบการเขียนโค้ดที่ไม่เหมาะสมอย่างคลาสสิกคือการเรียกใช้ AI ทุกครั้งที่แสดงหน้าเว็บ ซึ่งจะส่งผลให้เกิดการหมดเวลา การเสียค่าใช้จ่ายที่ไม่จำเป็น และบางครั้งอาจแสดงหน้าว่างเปล่าหากโค้ดมีการป้องกันที่ไม่ดี

ข้อกำหนดเบื้องต้น

สภาพแวดล้อมเป้าหมาย: WordPress 6.9.4 (เมษายน 2026) และ PHP 8.1 ขึ้นไป

คีย์ API และพื้นที่จัดเก็บข้อมูล

คุณสามารถใช้ OpenAI, Anthropic, Mistral หรือ Google ก็ได้ ผมจะยกตัวอย่าง OpenAI (การตอบสนองจาก API) เพราะมีความเสถียรมากในด้าน 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 (มาตรฐาน).

แหล่งข้อมูลทางการที่เป็นประโยชน์

สถาปัตยกรรมโซลูชัน

สตรีมข้อความที่ปลั๊กอินใช้งาน:

Éditeur WordPress (save_post) → préparation des données (titre, contenu, extrait, image, auteur) → wp_remote_post() vers API IA → réponse JSON → validation/sanitation → stockage post meta + transient → front (wp_head) injecte <script type=”application/ld+json”>…

เหตุใดเวิร์กโฟลว์นี้จึงใช้งานได้ดีในสภาพแวดล้อมการผลิต

  • รุ่น ณ เวลาที่ทำการออม (หรือตามความต้องการ) ไม่ใช่การแสดงผล: คุณจะไม่ปิดกั้นการแสดงผลส่วนหน้าหาก AI API ทำงานช้า
  • แคช : ช่วงเวลาสั้นๆ ช่วยหลีกเลี่ยงการสร้างใหม่แบบวนซ้ำเมื่อผู้แก้ไขคลิก "อัปเดต" 5 ครั้ง
  • โพสต์เมตา : คงทน สามารถส่งออกได้ และสามารถกำหนดเวอร์ชันได้ (หากคุณมีระบบทดสอบ)
  • การตรวจสอบความถูกต้องของ JSON หาก AI ส่งข้อความหรือ JSON ที่เสียหายกลับมา จะไม่มีการแทรกข้อมูลใดๆ (ใช้ข้อมูลสำรอง)

หมายเหตุสำคัญ: ปลั๊กอิน SEO และการทำซ้ำ

Yoast, Rank Math, SEOPress และอื่นๆ ได้ทำการแทรก JSON-LD ไว้แล้ว หากคุณเพิ่มเอง คุณอาจเสี่ยงต่อสิ่งต่อไปนี้:

  • สำเนา (สองรายการ) Article)
  • ความไม่สอดคล้องกัน (ผู้เขียนสองคน ภาพสองภาพ)

กลยุทธ์ที่ฉันแนะนำ: ฉีดเข้าไป โครงการ “เสริม” (เช่น about, mentions, keywords, audience) ในครั้งเดียว Article ที่คุณควบคุมได้ หรือไม่ก็ผลิตออกมา @graph สะอาด โค้ดด้านล่างสร้าง... @graph เรียบง่ายและหลีกเลี่ยงการ "สร้างองค์กร/เว็บไซต์ขึ้นมาใหม่ทั้งหมด"

โค้ดฉบับสมบูรณ์ — ทีละขั้นตอน

ฉันแนะนำให้คุณใส่เข้าไป mu- ปลั๊กอิน ถ้าคุณต้องการให้มันใช้งานได้ต่อเนื่องแม้จะมีการเปลี่ยนธีมหรือ "การปิดใช้งานโดยไม่ได้ตั้งใจ" มิเช่นนั้นก็ใช้ปลั๊กอินมาตรฐานทั่วไปได้เลย

ขั้นตอนที่ 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 ที่ "น่าเชื่อถือ"

AI ไม่ควรสร้างวันที่ ผู้เขียน หรือ URL ขึ้นมาเอง เราควรดึงข้อมูลเหล่านั้นมาจาก WordPress แล้วขอให้ AI เพิ่มความหมายเชิงลึกเข้าไปเท่านั้น

/**
 * 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 — AI แจ้งเตือน “JSON ที่เข้มงวด” + เรียกใช้ API ผ่าน wp_remote_post()

ปัญหาที่ทำให้การใช้งานส่วนใหญ่ล้มเหลวคือ: AI ส่งคืนข้อความรอบๆ 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 — เพิ่ม hook save_post (โดยไม่ทำให้โปรแกรมแก้ไขข้อความเสียหาย)

ถ้าใช้ hook ผิด หรือเงื่อนไขไม่ถูกต้อง คุณก็จะไปเรียกใช้ AI ในการบันทึกอัตโนมัติ การแก้ไข หรือการแสดงตัวอย่าง 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

เราทำการฉีดข้อมูลเฉพาะที่ส่วนหน้า ที่โหนดเดี่ยวๆ และเฉพาะในกรณีที่เมตาโหนดมีอยู่เท่านั้น ไม่มีการเรียกใช้ AI ในส่วนนี้

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

คำอธิบายของโค้ด

เหตุใดจึงเก็บข้อมูลไว้ในเมตาของโพสต์?

ข้อมูลเมตาของโพสต์ช่วยให้สถานะคงที่ หาก AI API ล่ม ข้อมูล JSON-LD ของคุณก็จะยังคงแสดงผลได้ และหากคุณมีแคชหน้าเว็บ (Varnish, ปลั๊กอินแคช) คุณก็จะหลีกเลี่ยงความผันผวนได้

เหตุใดจึงต้องมี "ตัวล็อก" ชั่วคราวเพิ่มเติม?

ในเว็บไซต์ที่ใช้ Elementor หรือ Divi การกระทำ "อัปเดต" สามารถเรียกการบันทึกหลายครั้ง (บันทึกอัตโนมัติ แก้ไข อัปเดต) แม้ว่าคุณจะกรองเฉพาะการบันทึกอัตโนมัติ/แก้ไขแล้วก็ตาม ฉันเคยพบการเรียกใช้งานซ้ำสองครั้งผ่านปลั๊กอินที่ "บันทึกซ้ำ" โพสต์ การใช้ transient จะช่วยป้องกันการเรียกเก็บเงินซ้ำซ้อน

เหตุใดการตรวจสอบความถูกต้องจึงมีน้อยที่สุดโดยเจตนา?

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 แทน
  • ลืมใส่เครื่องหมายเซมิโคลอน dans wp-config.php หลังจากที่ define() → เกิดข้อผิดพลาดร้ายแรงทันที
  • ตะขอที่ไม่เหมาะสม (เช่น the_content) → การเรียกใช้ AI เพื่อแสดงผล → ความล่าช้า + ค่าใช้จ่าย
  • การทดสอบการผลิต หากไม่จำกัดประเภทของโพสต์ → คุณจะสร้างโพสต์ 2000 รายการพร้อมกันผ่านลูปการบันทึก

ต้นทุนและการเพิ่มประสิทธิภาพ API

ค่าใช้จ่ายขึ้นอยู่กับรุ่นและขนาดของเนื้อหาที่ส่ง หากจำกัดไว้ที่ 12,000 ตัวอักษร (ซึ่งโดยทั่วไปจะเท่ากับ 2000-3000 คำ หากไม่รวม HTML) ถือว่าเป็นการขอใช้บริการในระดับปานกลาง

การประมาณค่าที่สมจริง (ลำดับขนาด)

  • 1 บทความ = 1 การเรียกขอตีพิมพ์จาก AI + 1 การเรียกขอตีพิมพ์ต่อการอัปเดตที่สำคัญแต่ละครั้ง
  • หากคุณเผยแพร่บทความ 30 บทความต่อเดือน และอัปเดตแต่ละบทความโดยเฉลี่ย 2 ครั้ง: จะมีผู้โทรเข้ามาสอบถามประมาณ 90 ครั้งต่อเดือน

สำหรับราคาที่แน่นอน โปรดดูที่หน้าเว็บอย่างเป็นทางการ (ราคาอาจมีการเปลี่ยนแปลง) OpenAI: ราคาของ OpenAI.

การปรับปรุงประสิทธิภาพที่ได้ผลจริง

  • ลดปริมาณอินพุต : ส่งเฉพาะส่วนที่คัดลอกมา พร้อมหัวข้อ H2/H3 แทนที่จะส่งเนื้อหาทั้งหมด (หากเนื้อหาของคุณยาวมาก)
  • รุ่น “มินิ” : มากเกินพอที่จะดึงคำสำคัญ/เกี่ยวกับ/การกล่าวถึงออกมาได้
  • การสร้างใหม่แบบมีเงื่อนไข : เปรียบเทียบค่าแฮชของเนื้อหา (ข้อมูลเมตาของโพสต์) และสร้างใหม่เฉพาะเมื่อค่าแฮชเปลี่ยนแปลงเท่านั้น
  • การประมวลผลแบบออฟไลน์เป็นชุด (WP-CLI) สำหรับการย้ายข้อมูลแทนการใช้ save_post จำนวนมาก

รูปแบบขั้นสูงและกรณีการใช้งาน

ตัวเลือกที่ 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 โดยใช้ shortcode ภายใน/JSON หากคุณส่งข้อมูลนี้ไปให้ AI โดยตรง AI จะสามารถดึงข้อมูลสำคัญออกมาได้

  • Divi 5 บางครั้งคุณอาจเห็นโครงสร้างภายใน wp_strip_all_tags() ช่วยได้บ้าง แต่ไม่เสมอไป
  • Elementor เนื้อหาบางส่วนอยู่ในเมตาเดตา (ข้อมูล Elementor) การแสดงผลขั้นสุดท้ายจึงมีความสมจริงมากกว่าเวอร์ชันดิบ
  • Avada ชอร์ตโค้ดของ 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” บ่อยๆ AI สามารถตรวจจับคู่คำถามและคำตอบได้ แต่ควรระมัดระวัง: FAQ ที่สร้างขึ้นมาเองนั้นมีความเสี่ยงต่อ SEO และการแก้ไขบทความ ผมแนะนำให้สร้าง FAQ เฉพาะในกรณีที่เนื้อหาหลักมีคำถามที่ชัดเจนอยู่แล้วเท่านั้น

คุณสามารถเพิ่มข้อจำกัดให้กับข้อความแจ้งเตือนได้ เช่น “ดึงเฉพาะคำถามที่ปรากฏอยู่แล้วแบบคำต่อคำเท่านั้น”

ความปลอดภัยและแนวทางปฏิบัติที่ดีที่สุด

ห้ามเปิดเผยคีย์บนฝั่งไคลเอ็นต์เด็ดขาด

ห้ามเรียกใช้ JavaScript จากเบราว์เซอร์ไปยัง AI API โดยเด็ดขาด เพราะจะทำให้ข้อมูลสำคัญรั่วไหล (เครื่องมือสำหรับนักพัฒนา, ซอร์สโค้ด, บันทึกต่างๆ) ในกรณีนี้ ทุกอย่างจะดำเนินการผ่าน PHP แทน wp_remote_post().

อัตราจำกัด

การ "ล็อก" ชั่วคราวเป็นจุดเริ่มต้นที่ดี หากเว็บไซต์ของคุณมีผู้เขียนหลายคน ให้เพิ่มการจำกัดอัตราการใช้งานต่อผู้ใช้ (เช่น การใช้งานชั่วคราวหนึ่งครั้งต่อรหัสผู้ใช้) บน REST endpoint

การตรวจสอบความถูกต้องของการป้อนข้อมูล

ไม่อนุญาตให้ผู้ใช้แทรกข้อความใดๆ ที่ส่งไปยัง AI ผ่านพารามิเตอร์ REST ที่ไม่สามารถควบคุมได้ ในกรณีนี้ เอนด์พอยต์จะรับค่า post_id และสร้างเพย์โหลดขึ้นใหม่จาก WordPress

จีดีพีอาร์ / ความเป็นส่วนตัว

  • อย่าส่งข้อมูลส่วนบุคคลที่ไม่จำเป็น (เช่น อีเมล ที่อยู่ IP ข้อมูลส่วนตัว)
  • ควรหลีกเลี่ยงการส่งความคิดเห็นหรือแบบฟอร์มใดๆ หากไม่มีพื้นฐานทางกฎหมายที่ชัดเจน
  • หากจำเป็น ให้บันทึกข้อมูลเกี่ยวกับผู้รับเหมาช่วง (OpenAI/Anthropic/ฯลฯ) ในทะเบียนและนโยบายความเป็นส่วนตัวของคุณ

ความเข้ากันได้ของแคช

หากคุณใช้แคชหน้าเว็บแบบเข้มข้น JSON-LD ที่ถูกแทรกผ่าน wp_head มันจะถูกแคชไว้เหมือนกับข้อมูลอื่นๆ นั่นคือจุดประสงค์หลัก ข้อผิดพลาดคือการสร้างเมตาเดตาใหม่และลืมล้างแคช (แคชปลั๊กอิน/CDN) ในกรณีนั้น คุณจะเห็นโครงสร้างข้อมูลเก่าเป็นเวลาหลายชั่วโมง

วิธีการทดสอบและแก้ไขข้อผิดพลาด

1) ทดสอบในพื้นที่ก่อน

ทำให้สามารถ WP_DEBUG et WP_DEBUG_LOG dans 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 ไม่ถูกต้อง หรือ hook ถูกเรียกใช้งานบ่อยเกินไป

อาการ สาเหตุที่เป็นไปได้ การตรวจสอบ Solution
ไม่มีสคริปต์ JSON-LD ในซอร์สโค้ด ข้อมูลเมตาว่างเปล่า (ไม่เคยสร้าง) หรือ post_status ≠ publish ดูข้อมูลเมตา _bpcab_ai_schema_jsonld (ผ่านปลั๊กอินดีบัก) เผยแพร่บทความ จากนั้นสร้างใหม่ผ่านทาง REST endpoint
บันทึก: HTTP 401 / 403 คีย์ API หายไป/ไม่ถูกต้อง WP_DEBUG_LOGรหัสข้อผิดพลาดใน debug.log ถูกต้อง BPCAB_AI_OPENAI_API_KEY dans wp-config.php
บันทึก: หมดเวลา API ทำงานช้า / ผู้ให้บริการโฮสติ้งบล็อกคำขอขาออก ทดสอบ wp_remote_get() ไปยังเว็บไซต์สาธารณะ เพิ่มขึ้นเล็กน้อย timeoutตรวจสอบการตั้งค่าไฟร์วอลล์ของคุณและอนุญาตให้ api.openai.com เข้าถึงได้
ข้อผิดพลาด: “เนื้อหาที่สร้างขึ้นไม่ใช่ JSON ที่ถูกต้อง” AI ส่งข้อความกลับมาโดยใช้รูปแบบ JSON ตรวจสอบ generated ในข้อผิดพลาด (ดีบัก) ทำให้ข้อความแจ้งเตือนเข้มงวดขึ้น คงไว้ซึ่ง... text.formatลดอุณหภูมิ
แผนภาพสองภาพ บทความอยู่ในหน้านั้น ปลั๊กอิน SEO ได้แทรกบทความไว้แล้ว โค้ด HTML: ค้นหาหลายรายการ "@type":"Article" เปลี่ยน Schema ของคุณเป็น “fragment” หรือปิดใช้งานการแสดงผลบทความของปลั๊กอิน SEO หากเป็นไปได้

ข้อผิดพลาดเฉพาะในการใช้งาน WordPress

  • โค้ดตัวอย่างเสียหายเนื่องจากปลั๊กอินโค้ดตัวอย่าง ปลั๊กอินบางตัวอาจเปลี่ยนแปลงลำดับการโหลด การใช้ mu-plugins จะช่วยลดความเสี่ยงนี้ได้
  • เวอร์ชัน PHP เก่าเกินไป หากเว็บไซต์ยังคงใช้ PHP 7.x คุณจะพบข้อผิดพลาดเกี่ยวกับชนิดข้อมูล ควรใช้ PHP 8.1 ขึ้นไป
  • ลำดับความสำคัญของตะขอ หากปลั๊กอินอื่นแก้ไขเนื้อหาหลังจากที่คุณบันทึกโพสต์แล้ว โครงสร้างข้อมูลของคุณอาจ "ล้าหลัง" ปรับลำดับความสำคัญ (20 → 30) หรือสร้างใหม่ผ่านทางเอนด์พอยต์

ทรัพยากร

คำถามที่พบบ่อย

Google ให้ "รางวัล" แก่ข้อมูลที่มีโครงสร้างซึ่งสร้างโดย AI โดยอัตโนมัติหรือไม่?

ไม่ การใช้มาร์กอัปช่วยในการตีความ แต่ถ้าเนื้อหาไม่สอดคล้องกัน คุณก็จะไม่ได้รับประโยชน์ ประโยชน์ที่แท้จริงอยู่ที่... ความมั่นคง และ ความถูกต้อง ในขนาดใหญ่

การแทรกข้อมูล JSON-LD ที่สร้างโดยโมเดลนั้นมีความเสี่ยงหรือไม่?

ใช่ ถ้าคุณปล่อยให้ AI คิดค้นเอง นั่นเป็นเหตุผลที่โค้ดบังคับให้ใช้ค่าของ WordPress สำหรับฟิลด์ที่สำคัญ และตรวจสอบโครงสร้างก่อนการแทรกข้อมูล

ฉันสามารถใช้ Anthropic หรือ Mistral แทนได้ไหม?

ใช่แล้ว คงโครงสร้างเดิมไว้: การแจ้งเตือนแบบ JSON ที่เข้มงวด wp_remote_post()การตรวจสอบความถูกต้อง json_decode()มีเพียงรูปแบบการร้องขอ/การตอบกลับเท่านั้นที่เปลี่ยนแปลงไป

ทำไมไม่ลองสร้างสคีมาสำหรับองค์กร/เว็บไซต์/รายการเส้นทางนำทางแบบสมบูรณ์ดูล่ะ?

เนื่องจากองค์ประกอบเหล่านี้มักได้รับการจัดการโดยปลั๊กอิน SEO อยู่แล้ว และเนื่องจากเป็นองค์ประกอบระดับเว็บไซต์ (ไม่ใช่ต่อบทความ) การผสมผสานแหล่งข้อมูลสองแหล่งจึงทำให้เกิดความไม่สอดคล้องกัน

วิธีหลีกเลี่ยงการแสดงผลลัพธ์ซ้ำซ้อนเมื่อใช้ Yoast/Rank Math/SEOPress?

มีสองทางเลือกที่เป็นไปได้:

  • สร้างสคีมาที่ไม่ซ้ำซ้อนกับบทความ (เช่น a) DefinedTermSet หรือ Thing ที่เกี่ยวข้อง)
  • ปิดใช้งานการแสดงผล Schema ของปลั๊กอิน SEO (หากมีตัวเลือกนี้) และปล่อยให้ปลั๊กอินของคุณจัดการบทความเอง

ฉันสามารถสร้างสคีมาสำหรับหน้าเว็บ (ประเภทโพสต์คือหน้าเพจ) ได้หรือไม่?

ใช่ แต่อย่าบังคับให้ใช้ "บทความ" ในบางหน้า คุณสามารถขอให้ AI เลือกได้ระหว่าง... WebPage, AboutPage, ContactPageเพิ่ม page dans 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() ดัดแปลงสำหรับปลั๊กอินนี้