إذا سبق لك أن قمت بلصق مخطط JSON-LD Schema.org "يدويًا" في مقال، فقد رأيت المشكلة الحقيقية: بعد 30 منشورًا، يصبح غير متناسق وغير مكتمل، ولا يجرؤ أحد على صيانته.

الحاجة / حالة الاستخدام

تساعد البيانات المنظمة (JSON-LD) جوجل ومحركات البحث الأخرى على فهم المحتوى بدقة: نوع المقالة، والمؤلف، وتاريخ النشر، والصورة الرئيسية، وكيان "حول"، والأسئلة الشائعة، وما إلى ذلك. WordPressلديك بالفعل الكثير من المعلومات الموثوقة (العنوان، المقتطف، الصورة الرئيسية). تكمن الفجوة في "المعنى": المواضيع، الكيانات، النوايا، وأحيانًا الحق نوع Schema.org (Article مقابل TechArticle مقابل NewsArticle، إلخ).

يُعد الذكاء الاصطناعي مفيدًا هنا لإنتاج الطبقة الدلالية بناءً على المحتوى، بلا سواءً قضيتَ 10 دقائق لكل مقال في اختيار الكلمات المفتاحية أو الكيانات أو أقسام "نبذة عنا"، فإن هذا الأمر، بحسب تجربتي، مُربحٌ للغاية في:

  • تقنيات التدوين (WordPress(التطوير، البيانات): يستخرج الذكاء الاصطناعي التقنيات والإصدارات والمفاهيم بشكل جيد.
  • المواقع الإلكترونية التحريرية مع وجود العديد من المحررين: توحيد الترميز دون تدريب الجميع على Schema.org.
  • محتوى مواقع التجارة الإلكترونية (الأدلة، المقارنات): إثراء المقالات دون تغيير أوصاف المنتجات.

في النهاية، ستعرف كيفية تنفيذ المساعد (متوافق مع ووردبريس 6.9.4 / PHP 8.1+) والذي:

  • يقوم بإنشاء مخطط JSON-LD Schema.org لكل مقال عبر واجهة برمجة تطبيقات الذكاء الاصطناعي (استدعاء wp_remote_post())
  • يقوم بتخزين الاستجابة مؤقتًا (واجهة برمجة تطبيقات Transients)
  • لا يتم تجديدها إلا عند الضرورة (النشر/التحديث).
  • يقوم بإدخال JSON-LD في <head> الجانب الأمامي
  • يتعامل مع الأخطاء (مثل انتهاء المهلة، وتجاوز الحصة، وبيانات JSON غير الصالحة) باستخدام حلول بديلة نظيفة.

ملخص سريع

  • نقوم بإنشاء JSON-LD عن طريق البريد باستخدام الذكاء الاصطناعي، ثم نحن مخازن في بيانات ما بعد التعريف (ونضع عابر (بالإضافة إلى تجنب المكالمات المتكررة).
  • يوجد مفتاح API في الفسفور الابيض بين config.php ل بواسطة define()، وليس بشكل دائم.
  • نسمي واجهة برمجة تطبيقات الذكاء الاصطناعي بـ wp_remote_post() + مهلة بالإضافة إلى معالجة الأخطاء.
  • نجبر الذكاء الاصطناعي على إرسال رد JSON صارم (ونقوم بالتحقق من ذلك من جانب PHP).
  • نقوم بإدخال نص JSON-LD عبر wp_head (الواجهة) ونتجنب الإدارة.
  • نضيف نقطة نهاية REST خاصة بالمسؤولين فقط لـ تتجدد عند الطلب (عملي في ضمان الجودة).

متى يُستخدم الذكاء الاصطناعي لهذا الغرض؟

استخدم الذكاء الاصطناعي إذا كنت بحاجة حقيقية إلى إثراء دلالي، وليس لمجرد "وضع كلمة 'مقالة' في كل مكان". أمثلة جيدة على الاستخدام:

  • محتوى طويل (أكثر من 1000 كلمة) حيث يؤدي استخراج الكيانات (العلامات التجارية، والأدوات، والمفاهيم) إلى تحقيق الدقة.
  • التصنيفات غير المكتملة (فئات/علامات غير موثوقة) وتريد حقول "حول/إشارات" أنظف.
  • كتابة جماعية مع أنماط غير متجانسة: الذكاء الاصطناعي يُوحّد.
  • نقل تحسين محركات البحث (قالب جديد، إضافة جديدة لتحسين محركات البحث): يمكنك إنشاء مخطط متسق دون إعادة كتابة المنشورات.

لقد رأيت في كثير من الأحيان فائدة في المواقع التي تكون فيها مقتطفات ووردبريس فارغة والتي يقوم المؤلفون بتغييرها كثيرًا: ينتج الذكاء الاصطناعي وصفًا متسقًا يتجنب "المقتطف" العشوائي.

متى لا يجب استخدام الذكاء الاصطناعي

تجنب استخدام الذكاء الاصطناعي إذا كانت خطتك آلية بحتة وحتمية بالفعل.

  • مواقع العرض مع 10 صفحات: قم بذلك يدويًا أو عبر إضافة لتحسين محركات البحث.
  • رسومات تخطيطية بسيطة (المنظمة، الموقع الإلكتروني، قائمة التنقل) تتم إدارتها بالفعل بواسطة إضافة تحسين محركات البحث الخاصة بك.
  • محتوى حساس (في المجالين الصحي والقانوني) إذا اعتمدت على الذكاء الاصطناعي "لابتكار" خصائص معينة. هنا، يجب أن يظل الذكاء الاصطناعي استغلاليًا لا إبداعيًا.
  • ميزانية ضيقة وكمية كبيرة من المنشورات التي يتم نشرها كل يوم: يمكن أن تزداد تكاليف واجهة برمجة التطبيقات إذا قمت بإعادة إنشائها بشكل متكرر للغاية.

من الممارسات السيئة الشائعة استدعاء الذكاء الاصطناعي في كل مرة يتم فيها عرض صفحة. يؤدي هذا إلى انقطاع الاتصال، وفواتير غير ضرورية، وأحيانًا صفحات فارغة إذا كان الكود ضعيف الحماية.

المتطلبات الأساسية

البيئة المستهدفة: WordPress 6.9.4 (أبريل 2026) و PHP 8.1+.

مفتاح API والتخزين

يمكنك استخدام OpenAI أو Anthropic أو Mistral أو Google. سأقدم مثالاً على OpenAI (استجابات واجهة برمجة التطبيقات) لأنه مستقر للغاية فيما يتعلق ببيانات 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 (معيار).

مصادر رسمية مفيدة

بنية الحل

دفق النص المستخدم بواسطة الملحق:

محرر ووردبريس (حفظ المنشور) ← تجهيز البيانات (العنوان، المحتوى، المقتطف، الصورة، المؤلف) ← إرسال المنشور عن بُعد إلى واجهة برمجة تطبيقات الذكاء الاصطناعي ← استجابة JSON ← التحقق من الصحة/التنظيف ← بيانات المنشور + التخزين المؤقت ← حقن الواجهة الأمامية (wp_head) …

لماذا تنجح هذه الطريقة في الإنتاج؟

  • الجيل في وقت الادخار (أو عند الطلب)، وليس على الشاشة: أنت لا تعيق عرض الواجهة الأمامية إذا كانت واجهة برمجة تطبيقات الذكاء الاصطناعي بطيئة.
  • مخبأ : يؤدي إجراء مؤقت قصير إلى تجنب عمليات إعادة التوليد المتكررة عندما ينقر المحرر على "تحديث" 5 مرات.
  • ما بعد الميتا : دائم، وقابل للتصدير، وقابل للتحديث (إذا كان لديك نظام تجريبي).
  • التحقق من صحة JSON : إذا أعاد الذكاء الاصطناعي نصًا تالفًا أو JSON، فلن يتم إدخال أي شيء (الخيار الاحتياطي).

ملاحظة هامة: إضافات تحسين محركات البحث والنسخ المكررة

تقوم إضافات مثل 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;
}

الخطوة الثانية - الثوابت والخيارات والضمانات

نؤمّن الوصول منذ البداية: إذا لم يكن المفتاح موجودًا، فلن نحاول أي شيء. لقد رأيتُ مرارًا وتكرارًا مواقع تُعيد أخطاء 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 - استخراج بيانات ووردبريس "الموثوقة"

لا ينبغي للذكاء الاصطناعي أن يخترع التواريخ أو المؤلفين أو عناوين المواقع الإلكترونية. نحن نأخذها من ووردبريس، ثم نطلب من الذكاء الاصطناعي فقط إثراءها دلالياً.

/**
 * 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 صارم" + استدعاء واجهة برمجة التطبيقات عبر wp_remote_post()

المشكلة التي تُعيق معظم التطبيقات هي: أن الذكاء الاصطناعي يُعيد نصًا مُحيطًا ببيانات JSON، أو حقولًا غير مُتوافقة. لذا، نفرض تنسيقًا صارمًا، ثم نُجري عملية التحقق.

مثال باستخدام OpenAI (نقطة نهاية الاستجابة). مرجع واجهة برمجة التطبيقات الرسمي: واجهة برمجة تطبيقات OpenAI Responses.

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

شرح الكود

لماذا يتم التخزين في بيانات المنشور؟

تُوفّر بيانات المنشور حالةً مستقرة. فإذا تعطلت واجهة برمجة تطبيقات الذكاء الاصطناعي، سيظلّ ملف 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 منشور دفعة واحدة عبر حلقة حفظ.

تكاليف واجهة برمجة التطبيقات وتحسينها

تعتمد التكلفة على الطراز وحجم المحتوى المُرسَل. مع حد أقصى يبلغ 12000 حرف من النص (والذي يُمثل عادةً 2000-3000 كلمة بدون HTML)، فإن طلبك يُعتبر متوسطًا.

تقدير واقعي (من حيث الحجم)

  • مقال واحد = طلب واحد للنشر بواسطة الذكاء الاصطناعي + طلب واحد لكل تحديث هام.
  • إذا قمت بنشر 30 مقالة شهريًا وقمت بتحديث كل منها مرتين في المتوسط: ~90 مكالمة شهريًا.

للحصول على الأسعار الدقيقة، يُرجى مراجعة الصفحات الرسمية (فهي قابلة للتغيير). OpenAI: أسعار OpenAI.

التحسينات التي تعمل بالفعل

  • قلل المدخلات أرسل المقتطف + عناوين H2/H3 بدلاً من المحتوى بأكمله (إذا كان المحتوى الخاص بك طويلاً جدًا).
  • نموذج "ميني" : أكثر من كافٍ لاستخراج الكلمات الرئيسية/حول/الإشارات.
  • التجديد المشروط : قم بمقارنة تجزئة المحتوى (بيانات المنشور) وقم بإعادة إنشائها فقط إذا تغيرت التجزئة.
  • الدفعة غير متصلة بالإنترنت (WP-CLI) لعمليات الترحيل بدلاً من حفظ المنشورات المجمعة.

المتغيرات المتقدمة وحالات الاستخدام

الخيار 1 - إعادة التوليد فقط إذا تغير المحتوى (التجزئة)

لتجنب الدفع عندما يقوم شخص ما بتصحيح فاصلة في العنوان، قم بتخزين قيمة تجزئة (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;
}

الخيار الثاني — التوافق مع Divi 5 / Elementor / Avada

غالباً ما يقوم هؤلاء المطورون بتخزين المحتوى في post_content باستخدام رموز مختصرة داخلية/JSON. إذا أرسلت هذا كما هو إلى الذكاء الاصطناعي، فقد يتمكن من استخراج البيانات غير المرغوب فيها.

  • ديفي 5 سترى أحيانًا هياكل داخلية. wp_strip_all_tags() قد يكون ذلك مفيداً، ولكن ليس دائماً.
  • Elementor جزء من المحتوى موجود في البيانات الوصفية (بيانات Elementor). العرض النهائي أكثر دقة من النسخة الأصلية.
  • فتح مشكلة الرموز المختصرة في Fusion Builder، نفس المشكلة.

نهجان:

  • نهج "آمن" (موصى به): استخرج النص المرئي فقط عبر the_content بعد التصفية، يتم إزالة العلامات.
  • نهج "سريع" : يحفظ post_content وتقبّل الضوضاء.

النسخة "الآمنة" (احذر من القيام بذلك في حلقة تكرارية على القوائم، احتفظ بها لحفظ المنشور):

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 - إضافة صفحة الأسئلة الشائعة إذا كانت المقالة تحتوي على قسم للأسئلة الشائعة

إذا كانت مقالاتك تنتهي غالبًا بعبارة "أسئلة وأجوبة"، فبإمكان الذكاء الاصطناعي اكتشاف هذه العبارات. لكن كن حذرًا: فالأسئلة والأجوبة المصطنعة تُشكل خطرًا على تحسين محركات البحث والمحتوى التحريري. أنصحك بإنشاء قسم للأسئلة والأجوبة فقط إذا كان المحتوى يتضمن بالفعل أسئلة صريحة.

يمكنك إضافة قيد إلى الموجه: "استخراج الأسئلة الموجودة بالفعل كلمة بكلمة فقط".

السلامة وأفضل الممارسات

لا تكشف المفتاح من جانب العميل

تجنّب تمامًا إجراء أي استدعاء JavaScript من المتصفح إلى واجهة برمجة تطبيقات الذكاء الاصطناعي. سيؤدي ذلك إلى تسريب المفتاح (أدوات المطورين، شفرة المصدر، السجلات). هنا، يمر كل شيء عبر PHP. wp_remote_post().

الحد من معدل

يُعدّ "القفل" المؤقت بدايةً جيدة. إذا كان لديك موقع متعدد المؤلفين، فأضف حدًا أقصى لعدد الطلبات لكل مستخدم (على سبيل المثال، طلب مؤقت واحد لكل مُعرّف مستخدم) على نقطة نهاية REST.

التحقق من صحة الإدخال

لا تسمح للمستخدم بإدخال نص عشوائي مُرسَل إلى الذكاء الاصطناعي عبر مُعامل REST غير مُتحكَّم به. هنا، تأخذ نقطة النهاية post_id وأعاد بناء الحمولة من ووردبريس.

اللائحة العامة لحماية البيانات / الخصوصية

  • لا ترسل بيانات شخصية غير ضرورية (رسائل البريد الإلكتروني، عناوين IP، الحقول الخاصة).
  • تجنب إرسال التعليقات أو النماذج بدون أساس قانوني واضح.
  • قم بتوثيق المقاول الفرعي (OpenAI/Anthropic/إلخ) في سجلك وسياسة الخصوصية الخاصة بك إذا لزم الأمر.

توافق ذاكرة التخزين المؤقت

إذا كنت تستخدم ذاكرة تخزين مؤقتة للصفحات ذات أداء عالٍ، فسيتم حقن JSON-LD عبر wp_head سيتم تخزينها مؤقتًا كغيرها من البيانات، وهذا هو الغرض المقصود. تكمن المشكلة في إعادة إنشاء البيانات الوصفية ونسيان مسح ذاكرة التخزين المؤقت (ذاكرة التخزين المؤقت للملحق/شبكة توصيل المحتوى). في هذه الحالة، ستظهر لك المخططات القديمة لساعات.

كيفية الاختبار والتصحيح

1) الاختبار المحلي أولاً

مكن WP_DEBUG et WP_DEBUG_LOG في wp-config.php. مرجع : تصحيح في وورد.

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 أو أدوات اختبار النتائج المنسقة. أنا لا أقدم لك رابطًا لمدونة مُحسّنة لمحركات البحث، بل مراجع مُهيكلة.

إذا لم ينجح الأمر

عندما يحدث العطل، يكون السبب دائمًا تقريبًا أحد هذه النقاط: المفتاح، أو الحصة، أو JSON غير صالح، أو تشغيل الخطاف بشكل متكرر جدًا.

عرض السبب المحتمل التحقق الحلول
لا يوجد نص برمجي JSON-LD في المصدر البيانات الوصفية الفارغة (لم يتم إنشاؤها مطلقًا) أو حالة المنشور لا تساوي النشر انظر إلى الميتا _bpcab_ai_schema_jsonld (عبر إضافة تصحيح الأخطاء) انشر المقال، ثم أعد إنشائه عبر نقطة نهاية REST
السجلات: HTTP 401 / 403 مفتاح API مفقود/غير صحيح WP_DEBUG_LOG، رمز الخطأ في debug.log صحيح BPCAB_AI_OPENAI_API_KEY في wp-config.php
السجلات: مهلة زمنية واجهة برمجة التطبيقات بطيئة / مزود الاستضافة يحظر الطلبات الصادرة اختبار أ wp_remote_get() إلى موقع عام زيادة طفيفة timeoutتحقق من إعدادات جدار الحماية لديك وقم بالسماح لموقع api.openai.com.
خطأ: "المحتوى المُنشأ ليس بتنسيق JSON صالح" أعاد الذكاء الاصطناعي نصًا حول ملف JSON فحص generated في حالة الخطأ (التصحيح) اجعل التعليمات أكثر صرامة، وحافظ عليها text.formatيخفض درجة الحرارة
رسمان بيانيان. المقال موجود في الصفحة. تقوم إضافة تحسين محركات البحث بالفعل بإضافة المقالة مصدر HTML: ابحث عن عدة "@type":"Article" غيّر مخططك إلى "مجزأ" أو عطّل إخراج المقالات من إضافة تحسين محركات البحث إن أمكن.

مآزق محددة في ووردبريس

  • تم تعطيل المقتطف بواسطة إضافة المقتطفات بعض الإضافات تُغيّر ترتيب التحميل. باستخدام mu-plugins، يمكنك تقليل هذا الخطر.
  • إصدار PHP قديم جدًا إذا كان الموقع لا يزال يعمل بنظام PHP 7.x، فستواجه أخطاء في أنواع البيانات. لذا، يُنصح باستخدام PHP 8.1 أو أحدث.
  • أولوية الخطاف إذا قام مُلحق آخر بتعديل المحتوى بعد حفظ المنشور، فقد يكون مخططك "متأخرًا". اضبط الأولوية (من 20 إلى 30) أو أعد إنشائه عبر نقطة النهاية.

الموارد

الأسئلة الشائعة

هل تقوم جوجل تلقائيًا "بمكافأة" البيانات المنظمة التي يتم إنشاؤها بواسطة الذكاء الاصطناعي؟

لا. تساعد علامات التنسيق في التفسير، ولكن إذا لم يكن المحتوى متسقًا، فلن تستفيد. الفائدة الحقيقية تكمن في اتساق و دقة على نطاق واسع.

هل يُعدّ إدخال بيانات JSON-LD التي ينتجها نموذج ما أمراً محفوفاً بالمخاطر؟

نعم، إذا تركتَ للذكاء الاصطناعي حرية الابتكار. ولهذا السبب، يُجبر الكود على استخدام قيم ووردبريس للحقول الأساسية، ويتحقق من صحة البنية قبل إضافتها.

هل يمكنني استخدام أنثروبيك أو ميسترال بدلاً من ذلك؟

نعم. حافظ على نفس البنية: موجه JSON صارم، wp_remote_post()تصديق json_decode()التغيير الوحيد هو في تنسيق الطلب/الاستجابة.

لماذا لا يتم إنشاء مخطط كامل للمنظمة/الموقع الإلكتروني/قائمة التنقل؟

لأن هذه العناصر غالبًا ما تتم إدارتها بالفعل بواسطة إضافة لتحسين محركات البحث ولأنها على مستوى الموقع بأكمله (وليس لكل مقال)، فإن خلط مصدرين يخلق تناقضات.

كيفية تجنب التكرارات باستخدام Yoast/Rank Math/SEOPress؟

خياران واقعيان:

  • قم بإنشاء مخطط لا يكرر المقالة (على سبيل المثال، أ) DefinedTermSet أو Thing متعلق ب)
  • قم بتعطيل إخراج البيانات المنظمة (Schema) من إضافة تحسين محركات البحث (إن وُجد هذا الخيار) ودع الإضافة تُدير المقالة.

هل يمكنني إنشاء مخطط الصفحات (صفحة من نوع منشور)؟

نعم، ولكن لا تُجبر المستخدم على استخدام "مقالة". في بعض الصفحات، يمكنك أن تطلب من الذكاء الاصطناعي الاختيار بين 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() تم تعديله خصيصاً لهذه الإضافة.