Ако някога сте публикували статия и след това, две седмици по-късно, сте осъзнали, че мета описанието е оставено празно (или още по-лошо: автоматично откъсване, отрязано на грешното място), вече сте се докоснали до истинския проблем: то е повтарящо се, лесно се забравя и това си личи в резултатите от Google.

Нуждата / Случаят на употреба

Мета описанието е краткият текст (често 150–160 знака), който търсачките могат да покажат под заглавието. WordPress Версия 6.9.4 не генерира собствено „SEO“ мета описание: в зависимост от вашата тема, ще получите или нищо, или само фрагмент, базиран на съдържанието. SEO плъгините (Yoast, Rank Math, SEOPress и др.) добавят поле, но не го попълват вместо вас.

Изкуственият интелект е полезен тук за бързо създаване на описание:

  • в съответствие с действителното съдържание на статията,
  • в рамките на контролирана дължина,
  • с постоянен тон (информативен, търговски, неутрален...),
  • без да се налага да препрочитате по 2000 думи с всяка публикация.

До края ще знаете как да имплементирате малък плъгин WordPress (WP 6.9.4 / PHP 8.1+ съвместим), който:

  • добавя бутон „Генериране с AI“ в редактора (Gutenberg),
  • извиква AI API чрез wp_remote_post(),
  • кешира резултатите с преходните процеси,
  • запазва мета описанието в мета данни за публикацията (и можете също да попълните полето на SEO плъгин, ако желаете).

Често виждам този работен процес в сайтове с множество автори: никой не „притежава“ задачата за мета описание и SEO натрупаните задачи се натрупват. Автоматизирането на генерирането по време на редактирането решава този проблем, без да ви обвързва с конкретен инструмент.

Бързо обобщение

  • Съхранявате API ключа в wp-config.php (никога не е твърдо кодирано в плъгин).
  • Вие инсталирате mu-плъгин (автоматично зарежда се), което предоставя защитена REST крайна точка.
  • Издателят извиква тази крайна точка, която извиква AI ​​API с wp_remote_post().
  • Отговорът беше почистен (санитизиран), съкратен и след това запазен като post-meta.
  • Кеш чрез get_transient()/set_transient() да се ограничат разходите.

Кога да използваме изкуствен интелект за това

Използвайте изкуствен интелект за генериране на мета описания, ако:

  • Публикувате много (или с няколко души) и искате систематично „минимално SEO“,
  • Статиите ви имат редовна структура (уроци, продуктови ревюта, рецепти, информационни листове...),
  • Вече имате определен редакционен тон и можете да го опишете в 2-3 изречения в подкана.
  • Искате да ускорите актуализирането на старо съдържание (одит + генериране на пакети).

Според моя опит, най-голяма е ползата, когато комбинирате изкуствен интелект + прости правила: дължина, без кавички, без непроверими обещания и без „Кликнете тук“.

Кога НЕ е добре да използвате изкуствен интелект

Избягвайте (или ограничете) ИИ, ако:

  • Съдържанието ви засяга чувствителни теми (здраве, право, финанси) и нямате стриктна корекция.
  • Имате силни законови ограничения (GDPR, лични данни в съдържанието),
  • Можете да се справите по-добре с детерминистично правило: например, ако въведението ви винаги съдържа перфектно изречение, е достатъчно просто извличане + почистване.

Класическото решение (без изкуствен интелект) може да бъде по-просто и безплатно: вземете екстракта WordPressПремахнете шорткодовете, ограничете го до 155 знака и запазете. Това често е „достатъчно“ за малък блог.

Друг сценарий: ако вече използвате SEO плъгин с динамични (променливи) шаблони, понякога можете да генерирате правилно описание без изкуствен интелект. В този случай изкуственият интелект се превръща в инструмент за корекция/оптимизация, а не в източника.

Предварителни

Ще направите HTTP повикване от WordPress към услуга Чужд, а API Приложно-програмният интерфейс (API) е интерфейс, който позволява на един софтуер да отправя заявки към друг. На практика: изпращате HTTP заявка (често във формат JSON) към URL адрес и получавате JSON отговор.

WordPress предоставя wp_remote_post() за да се правят тези заявки от сървър към сървър. Официална документация: wp_remote_post().

Версии и среда

  • WordPress: 6.9.4 (или по-нова версия)
  • PHP: минимум 8.1 (препоръчва се 8.2/8.3, ако вашият хостинг доставчик го позволява)
  • HTTPS е активиран в администраторския панел (силно препоръчително)
  • Необходим е администраторски достъп за инсталиране на mu-плъгин

API ключ и цени

Можете да използвате OpenAI, Anthropic, Mistral, Google… Кодът по-долу е насочен към OpenAI (API отговори) защото е стабилен и добре документиран. След това се адаптирайте, ако предпочитате друг доставчик.

Предупреждение за разходи: Всяко поколение изразходва токени. Ако генерирате 500 описания наведнъж, цената може бързо да се натрупа. Кеширането (преходни процеси) и „мини“ шаблоните значително намаляват цената.

Къде да се съхранява ключът (задължително: wp-config.php)

Добавете това към wp-config.php, в идеалния случай точно над реда „Това е всичко, спрете да редактирате!“:

define( 'BPCAB_OPENAI_API_KEY', 'VOTRE_CLE_API_ICI' );

Никога не използвайте този ключ:

  • в рамките на тема (риск от загуба на ключа по време на актуализация),
  • в публично Git хранилище,
  • в JavaScript (това би било видимо за всички).

Къде да поставите кода на плъгина

За начинаещ най-надеждният е mu-плъгин (Задължително за употреба). Зарежда се автоматично, дори ако смените темата.

  • Създайте папката: wp-content/mu-plugins/ (ако не съществува)
  • Създайте файла: wp-content/mu-plugins/ai-meta-description.php

Справка: Задължителни плъгини (mu-plugins).

Архитектура на решението

Поток (текстова схема):

WordPress редактор (бутон) → вътрешно REST повикване (администратор) → WordPress крайна точка → wp_remote_post() → AI API → JSON отговор → почистване + контрол на дължината → запазване в мета данни за публикацията → връщане към редактора

Подробности за стъпките

  • Потребителски интерфейс от страна на редактора: Малък JS скрипт добавя бутон към лентата с документи (Gutenberg) и извиква REST API.
  • Крайна точка на REST: път /bpcab/v1/meta-description reçoit post_id и режим („чернова“ или „запазване“).
  • сигурност: Разрешения (възможности) за WordPress edit_post) + REST еднократен случай.
  • AI повикване: сървър → сървър чрез wp_remote_post(), кратко време за изчакване, обработка на грешки.
  • Скрито: преходен процес, базиран на хеша на съдържанието, за да се избегне двойно плащане за едно и също генериране.
  • съхранение: резервно копие в post meta (ключ _bpcab_meta_descriptionСлед това можете да го изложите на предния край или да го картографирате към SEO плъгин.

Пълният код — стъпка по стъпка

Няколко думи за „кукичките“ преди кодирането. Едно кука е точка за разширение на WordPress. Има два вида:

  • действие : задейства код в даден момент (напр.: rest_api_init).
  • Филтър : променя стойност (напр. филтриране на съдържание).

Справка: Куки (действия и филтри).

Стъпка 1 – създайте специална мета публикация

Регистрираме „чист“ файл с метаданни, който е видим чрез REST (полезно за Gutenberg). Справка: register_post_meta().

<?php
/**
 * Plugin Name: BPCAB - IA Meta Description
 * Description: Génère des meta descriptions via IA depuis l'éditeur WordPress (WP 6.9.4+).
 * Author: BPCAB
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action( 'init', function () {
	register_post_meta(
		'',
		'_bpcab_meta_description',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => 'sanitize_text_field',
			'auth_callback'     => function () {
				return current_user_can( 'edit_posts' );
			},
		)
	);
} );

Стъпка 2 — Защитена REST крайна точка

Предоставяме вътрешен REST маршрут. Gutenberg може лесно да извика WordPress REST API, а вие запазвате API ключа на сървъра.

REST документация: REST API на WordPress.

add_action( 'rest_api_init', function () {
	register_rest_route(
		'bpcab/v1',
		'/meta-description',
		array(
			'methods'             => 'POST',
			'permission_callback' => function ( WP_REST_Request $request ) {
				$post_id = absint( $request->get_param( 'post_id' ) );
				if ( ! $post_id ) {
					return new WP_Error( 'bpcab_missing_post_id', 'post_id manquant.', array( 'status' => 400 ) );
				}
				return current_user_can( 'edit_post', $post_id );
			},
			'callback'            => 'bpcab_rest_generate_meta_description',
			'args'                => array(
				'post_id' => array(
					'type'     => 'integer',
					'required' => true,
				),
				'mode' => array(
					'type'    => 'string',
					'default' => 'draft',
					'enum'    => array( 'draft', 'save' ),
				),
			),
		)
	);
} );

function bpcab_rest_generate_meta_description( WP_REST_Request $request ) : WP_REST_Response {
	$post_id = absint( $request->get_param( 'post_id' ) );
	$mode    = (string) $request->get_param( 'mode' );

	$post = get_post( $post_id );
	if ( ! $post ) {
		return new WP_REST_Response(
			array( 'error' => 'Article introuvable.' ),
			404
		);
	}

	// On se base sur le contenu + titre. Vous pouvez adapter selon votre ligne éditoriale.
	$title   = (string) get_the_title( $post );
	$content = (string) $post->post_content;

	// Nettoyage basique : on retire les shortcodes et les balises.
	$content_plain = wp_strip_all_tags( strip_shortcodes( $content ) );
	$content_plain = trim( preg_replace( '/s+/', ' ', $content_plain ) );

	// Si l'article est vide (cas fréquent sur brouillon), on évite de payer un appel IA inutile.
	if ( mb_strlen( $content_plain ) < 80 ) {
		return new WP_REST_Response(
			array(
				'error' => 'Contenu trop court. Ajoutez un peu de texte avant de générer.',
			),
			400
		);
	}

	$result = bpcab_generate_meta_description_via_openai(
		array(
			'post_id'  => $post_id,
			'title'    => $title,
			'content'  => $content_plain,
			'language' => 'fr',
		)
	);

	if ( is_wp_error( $result ) ) {
		return new WP_REST_Response(
			array(
				'error'   => $result->get_error_message(),
				'details' => $result->get_error_data(),
			),
			500
		);
	}

	$meta_description = $result['meta_description'];

	if ( 'save' === $mode ) {
		update_post_meta( $post_id, '_bpcab_meta_description', $meta_description );
	}

	return new WP_REST_Response(
		array(
			'meta_description' => $meta_description,
			'saved'            => ( 'save' === $mode ),
		),
		200
	);
}

Стъпка 3 — Извикване на AI чрез wp_remote_post() + кеш транзиент

Кешът се създава с помощта на „подсказка“ (хеш) на заглавието и съдържанието. Ако регенерирате без да променяте статията, ще извлечете кеша.

Референтни преходни процеси: Преходни процеси на API.

function bpcab_generate_meta_description_via_openai( array $data ) {
	if ( ! defined( 'BPCAB_OPENAI_API_KEY' ) || ! BPCAB_OPENAI_API_KEY ) {
		return new WP_Error( 'bpcab_missing_api_key', 'Clé API manquante. Ajoutez BPCAB_OPENAI_API_KEY dans wp-config.php.' );
	}

	$post_id = absint( $data['post_id'] ?? 0 );
	$title   = (string) ( $data['title'] ?? '' );
	$content = (string) ( $data['content'] ?? '' );

	// Empreinte pour le cache : si le contenu change, on regénère.
	$cache_key_raw = $title . '|' . $content;
	$cache_key     = 'bpcab_md_' . md5( $cache_key_raw );

	$cached = get_transient( $cache_key );
	if ( is_array( $cached ) && ! empty( $cached['meta_description'] ) ) {
		return $cached;
	}

	// Prompt “contraignant” : court, sans guillemets, sans emojis, sans promesses.
	$system_instructions = 'Vous êtes un assistant SEO. Vous écrivez une meta description en français, factuelle, sans superlatifs inutiles.';
	$user_prompt         = "Générez une meta description SEO pour l'article suivant.n"
		. "- Longueur: 140 à 160 caractères (espaces inclus)n"
		. "- Une seule phrasen"
		. "- Pas de guillemetsn"
		. "- Pas de call-to-action du type "Cliquez" ou "Découvrez"n"
		. "- Doit résumer fidèlement le contenunn"
		. "Titre: {$title}nn"
		. "Contenu:n{$content}n";

	$body = array(
		'model' => 'gpt-4.1-mini',
		'input' => array(
			array(
				'role'    => 'system',
				'content' => $system_instructions,
			),
			array(
				'role'    => 'user',
				'content' => $user_prompt,
			),
		),
		// On limite la sortie : une meta description ne doit pas devenir un paragraphe.
		'max_output_tokens' => 120,
		// Température basse = plus stable.
		'temperature'       => 0.4,
	);

	$args = array(
		'headers' => array(
			'Content-Type'  => 'application/json',
			'Authorization' => 'Bearer ' . BPCAB_OPENAI_API_KEY,
		),
		'body'    => wp_json_encode( $body ),
		// Timeouts : si votre hébergeur est lent, augmentez à 20.
		'timeout' => 15,
	);

	$response = wp_remote_post( 'https://api.openai.com/v1/responses', $args );

	if ( is_wp_error( $response ) ) {
		return new WP_Error(
			'bpcab_http_error',
			'Erreur HTTP lors de l’appel à l’API IA.',
			array( 'wp_error' => $response->get_error_message() )
		);
	}

	$code = wp_remote_retrieve_response_code( $response );
	$raw  = wp_remote_retrieve_body( $response );

	if ( $code < 200 || $code >= 300 ) {
		return new WP_Error(
			'bpcab_api_non_200',
			'Réponse non valide de l’API IA.',
			array(
				'status_code' => $code,
				'body'        => $raw,
			)
		);
	}

	$json = json_decode( $raw, true );
	if ( ! is_array( $json ) ) {
		return new WP_Error(
			'bpcab_json_decode',
			'Impossible de décoder la réponse JSON de l’API.',
			array( 'body' => $raw )
		);
	}

	// Extraction robuste : selon les formats, le texte peut se trouver à plusieurs endroits.
	$text = bpcab_extract_text_from_openai_responses_api( $json );
	$text = is_string( $text ) ? $text : '';

	// Nettoyage : on veut une string sûre, sans HTML.
	$text = wp_strip_all_tags( $text );
	$text = sanitize_text_field( $text );
	$text = trim( preg_replace( '/s+/', ' ', $text ) );

	// Garde-fous : longueur + suppression guillemets (j’ai vu l’IA en ajouter malgré l’instruction).
	$text = str_replace( array( '"', '“', '”', '’' ), array( '', '', '', "'" ), $text );

	// Tronquage “propre” si ça dépasse (évite de casser un caractère multibyte).
	if ( mb_strlen( $text ) > 160 ) {
		$text = mb_substr( $text, 0, 160 );
		$text = rtrim( $text, " tnrx0B-–—,;:" );
	}

	// Si trop court, on préfère renvoyer quand même, mais vous pouvez forcer une régénération.
	if ( mb_strlen( $text ) < 80 ) {
		// Pas une erreur fatale : certains articles très simples donnent des descriptions courtes.
	}

	$result = array(
		'meta_description' => $text,
		'model'            => $json['model'] ?? 'unknown',
	);

	// Cache 30 jours. Ajustez : si vous modifiez souvent les articles, réduisez à 7 jours.
	set_transient( $cache_key, $result, 30 * DAY_IN_SECONDS );

	return $result;
}

/**
 * Extraction du texte depuis la Responses API.
 * On évite les dépendances : extraction “best effort”.
 */
function bpcab_extract_text_from_openai_responses_api( array $json ) : string {
	// Format courant : output[0].content[0].text
	if ( isset( $json['output'][0]['content'][0]['text'] ) && is_string( $json['output'][0]['content'][0]['text'] ) ) {
		return $json['output'][0]['content'][0]['text'];
	}

	// Fallback : concaténer tous les segments texte trouvés.
	$texts = array();

	if ( isset( $json['output'] ) && is_array( $json['output'] ) ) {
		foreach ( $json['output'] as $out ) {
			if ( empty( $out['content'] ) || ! is_array( $out['content'] ) ) {
				continue;
			}
			foreach ( $out['content'] as $c ) {
				if ( isset( $c['text'] ) && is_string( $c['text'] ) ) {
					$texts[] = $c['text'];
				}
			}
		}
	}

	return trim( implode( ' ', $texts ) );
}

Стъпка 4 — Добавяне на бутон в Gutenberg (JS) + REST nonce

Добавяме скрипт само в редактора. Използваме wp.apiFetch и т.н. минавам REST nonce чрез wp_localize_script.

Често срещан капан: поставяне на този JS код във фронтенд интерфейса (или в конструктор) и показване на маршрута без разрешения. Тук оставаме в администраторската област, а крайната точка проверява... edit_post.

add_action( 'enqueue_block_editor_assets', function () {
	$asset_handle = 'bpcab-ai-meta-description-editor';

	wp_enqueue_script(
		$asset_handle,
		plugins_url( 'bpcab-ai-meta-description-editor.js', __FILE__ ),
		array( 'wp-data', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-api-fetch', 'wp-plugins' ),
		'1.0.0',
		true
	);

	wp_localize_script(
		$asset_handle,
		'BPCAB_AIMD',
		array(
			'restUrl' => esc_url_raw( rest_url( 'bpcab/v1/meta-description' ) ),
			'nonce'   => wp_create_nonce( 'wp_rest' ),
		)
	);
} );

След това създайте JS файла заедно с mu-plugin. Проблем: mu-plugin „естествено“ няма папка с ресурси. Две възможности:

  • Вариант за начинаещи: поставете JS вътре wp-content/plugins/bpcab-ai-meta-description/ (класически плъгин) вместо mu-plugin.
  • опция за mu-плъгин: поставете JS на фиксиран път (напр.: /wp-content/mu-plugins/assets/) и използвайте ръчно създаден URL адрес.

За да избегнете препъване по пътеките, разделът „Пълен асемблиран код“ по-долу ви предоставя версия класически плъгин (По-лесен за зареждане на JS). mu-плъгинът остава много добър за сървърната страна, но за начинаещ, класическият плъгин означава по-малко изненади.

Ето го файлът bpcab-ai-meta-description-editor.js :

( function ( wp ) {
	const { registerPlugin } = wp.plugins;
	const { PluginDocumentSettingPanel } = wp.editPost;
	const { TextControl, Button, Notice } = wp.components;
	const { useSelect, useDispatch } = wp.data;
	const { useState } = wp.element;

	function AIMetaDescriptionPanel() {
		const postId = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostId(), [] );
		const currentMeta = useSelect( ( select ) => {
			const meta = select( 'core/editor' ).getEditedPostAttribute( 'meta' );
			return meta ? meta._bpcab_meta_description : '';
		}, [] );

		const { editPost } = useDispatch( 'core/editor' );

		const [ value, setValue ] = useState( currentMeta || '' );
		const [ isLoading, setIsLoading ] = useState( false );
		const [ error, setError ] = useState( '' );
		const [ info, setInfo ] = useState( '' );

		async function generate( mode ) {
			setError( '' );
			setInfo( '' );
			setIsLoading( true );

			try {
				const res = await wp.apiFetch( {
					url: BPCAB_AIMD.restUrl,
					method: 'POST',
					headers: {
						'X-WP-Nonce': BPCAB_AIMD.nonce,
					},
					data: {
						post_id: postId,
						mode: mode, // 'draft' ou 'save'
					},
				} );

				if ( res && res.meta_description ) {
					setValue( res.meta_description );
					editPost( { meta: { _bpcab_meta_description: res.meta_description } } );

					if ( res.saved ) {
						setInfo( 'Meta description générée et enregistrée.' );
					} else {
						setInfo( 'Meta description générée (non enregistrée). Cliquez sur “Enregistrer” si elle vous convient.' );
					}
				} else {
					setError( 'Réponse inattendue du serveur.' );
				}
			} catch ( e ) {
				// Message souvent vu : "You are probably offline." ou erreur REST.
				setError( e.message ? e.message : 'Erreur lors de l’appel REST.' );
			} finally {
				setIsLoading( false );
			}
		}

		return wp.element.createElement(
			PluginDocumentSettingPanel,
			{
				name: 'bpcab-ai-meta-description',
				title: 'Meta description (IA)',
				className: 'bpcab-ai-meta-description-panel',
			},
			error ? wp.element.createElement( Notice, { status: 'error', isDismissible: true }, error ) : null,
			info ? wp.element.createElement( Notice, { status: 'info', isDismissible: true }, info ) : null,
			wp.element.createElement( TextControl, {
				label: 'Meta description',
				help: 'Stockée dans la meta _bpcab_meta_description. Vous pouvez la copier dans votre plugin SEO si besoin.',
				value: value,
				onChange: ( v ) => {
					setValue( v );
					editPost( { meta: { _bpcab_meta_description: v } } );
				},
			} ),
			wp.element.createElement(
				'div',
				{ style: { display: 'flex', gap: '8px' } },
				wp.element.createElement( Button, { variant: 'secondary', isBusy: isLoading, disabled: isLoading, onClick: () => generate( 'draft' ) }, 'Générer (aperçu)' ),
				wp.element.createElement( Button, { variant: 'primary', isBusy: isLoading, disabled: isLoading, onClick: () => generate( 'save' ) }, 'Générer & enregistrer' )
			)
		);
	}

	registerPlugin( 'bpcab-ai-meta-description', {
		render: AIMetaDescriptionPanel,
	} );
} )( window.wp );

Класически капан: забравяне за зависимостта wp-plugins ou wp-edit-post в wp_enqueue_script()Резултат: бял екран в редактора и конзолна грешка от типа „Не може да се прочетат свойствата на неопределено“.

Пълният асемблиран код

Версия „готова за копиране и поставяне“ класически плъгин (препоръчва се тук за лесно зареждане на JS).

Дървовидна структура

  • wp-content/plugins/bpcab-ai-meta-description/bpcab-ai-meta-description.php
  • wp-content/plugins/bpcab-ai-meta-description/bpcab-ai-meta-description-editor.js

Пълен PHP файл

<?php
/**
 * Plugin Name: BPCAB - IA Meta Description
 * Description: Génération de meta descriptions via IA depuis l’éditeur (WP 6.9.4+, PHP 8.1+).
 * Version: 1.0.0
 * Author: BPCAB
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Enregistre la post meta qui stocke la meta description générée.
 */
add_action( 'init', function () {
	register_post_meta(
		'',
		'_bpcab_meta_description',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => 'sanitize_text_field',
			'auth_callback'     => function () {
				return current_user_can( 'edit_posts' );
			},
		)
	);
} );

/**
 * Route REST pour générer / enregistrer la meta description.
 */
add_action( 'rest_api_init', function () {
	register_rest_route(
		'bpcab/v1',
		'/meta-description',
		array(
			'methods'             => 'POST',
			'permission_callback' => function ( WP_REST_Request $request ) {
				$post_id = absint( $request->get_param( 'post_id' ) );
				if ( ! $post_id ) {
					return new WP_Error( 'bpcab_missing_post_id', 'post_id manquant.', array( 'status' => 400 ) );
				}
				return current_user_can( 'edit_post', $post_id );
			},
			'callback'            => 'bpcab_rest_generate_meta_description',
			'args'                => array(
				'post_id' => array(
					'type'     => 'integer',
					'required' => true,
				),
				'mode' => array(
					'type'    => 'string',
					'default' => 'draft',
					'enum'    => array( 'draft', 'save' ),
				),
			),
		)
	);
} );

function bpcab_rest_generate_meta_description( WP_REST_Request $request ) : WP_REST_Response {
	$post_id = absint( $request->get_param( 'post_id' ) );
	$mode    = (string) $request->get_param( 'mode' );

	$post = get_post( $post_id );
	if ( ! $post ) {
		return new WP_REST_Response( array( 'error' => 'Article introuvable.' ), 404 );
	}

	$title   = (string) get_the_title( $post );
	$content = (string) $post->post_content;

	$content_plain = wp_strip_all_tags( strip_shortcodes( $content ) );
	$content_plain = trim( preg_replace( '/s+/', ' ', $content_plain ) );

	if ( mb_strlen( $content_plain ) < 80 ) {
		return new WP_REST_Response(
			array( 'error' => 'Contenu trop court. Ajoutez du texte avant de générer.' ),
			400
		);
	}

	$result = bpcab_generate_meta_description_via_openai(
		array(
			'post_id'  => $post_id,
			'title'    => $title,
			'content'  => $content_plain,
			'language' => 'fr',
		)
	);

	if ( is_wp_error( $result ) ) {
		return new WP_REST_Response(
			array(
				'error'   => $result->get_error_message(),
				'details' => $result->get_error_data(),
			),
			500
		);
	}

	$meta_description = $result['meta_description'];

	if ( 'save' === $mode ) {
		update_post_meta( $post_id, '_bpcab_meta_description', $meta_description );

		/**
		 * Point d’extension : si vous voulez synchroniser vers un plugin SEO,
		 * vous pourrez accrocher un hook ici.
		 */
		do_action( 'bpcab_meta_description_saved', $post_id, $meta_description, $result );
	}

	return new WP_REST_Response(
		array(
			'meta_description' => $meta_description,
			'saved'            => ( 'save' === $mode ),
		),
		200
	);
}

function bpcab_generate_meta_description_via_openai( array $data ) {
	if ( ! defined( 'BPCAB_OPENAI_API_KEY' ) || ! BPCAB_OPENAI_API_KEY ) {
		return new WP_Error( 'bpcab_missing_api_key', 'Clé API manquante. Ajoutez BPCAB_OPENAI_API_KEY dans wp-config.php.' );
	}

	$title   = (string) ( $data['title'] ?? '' );
	$content = (string) ( $data['content'] ?? '' );

	$cache_key_raw = $title . '|' . $content;
	$cache_key     = 'bpcab_md_' . md5( $cache_key_raw );

	$cached = get_transient( $cache_key );
	if ( is_array( $cached ) && ! empty( $cached['meta_description'] ) ) {
		return $cached;
	}

	$system_instructions = 'Vous êtes un assistant SEO. Vous écrivez une meta description en français, factuelle, sans superlatifs inutiles.';
	$user_prompt         = "Générez une meta description SEO pour l'article suivant.n"
		. "- Longueur: 140 à 160 caractères (espaces inclus)n"
		. "- Une seule phrasen"
		. "- Pas de guillemetsn"
		. "- Pas de call-to-action du type "Cliquez" ou "Découvrez"n"
		. "- Doit résumer fidèlement le contenunn"
		. "Titre: {$title}nn"
		. "Contenu:n{$content}n";

	$body = array(
		'model' => 'gpt-4.1-mini',
		'input' => array(
			array(
				'role'    => 'system',
				'content' => $system_instructions,
			),
			array(
				'role'    => 'user',
				'content' => $user_prompt,
			),
		),
		'max_output_tokens' => 120,
		'temperature'       => 0.4,
	);

	$response = wp_remote_post(
		'https://api.openai.com/v1/responses',
		array(
			'headers' => array(
				'Content-Type'  => 'application/json',
				'Authorization' => 'Bearer ' . BPCAB_OPENAI_API_KEY,
			),
			'body'    => wp_json_encode( $body ),
			'timeout' => 15,
		)
	);

	if ( is_wp_error( $response ) ) {
		return new WP_Error(
			'bpcab_http_error',
			'Erreur HTTP lors de l’appel à l’API IA.',
			array( 'wp_error' => $response->get_error_message() )
		);
	}

	$code = wp_remote_retrieve_response_code( $response );
	$raw  = wp_remote_retrieve_body( $response );

	if ( $code < 200 || $code >= 300 ) {
		return new WP_Error(
			'bpcab_api_non_200',
			'Réponse non valide de l’API IA.',
			array(
				'status_code' => $code,
				'body'        => $raw,
			)
		);
	}

	$json = json_decode( $raw, true );
	if ( ! is_array( $json ) ) {
		return new WP_Error(
			'bpcab_json_decode',
			'Impossible de décoder la réponse JSON de l’API.',
			array( 'body' => $raw )
		);
	}

	$text = bpcab_extract_text_from_openai_responses_api( $json );
	$text = wp_strip_all_tags( (string) $text );
	$text = sanitize_text_field( $text );
	$text = trim( preg_replace( '/s+/', ' ', $text ) );
	$text = str_replace( array( '"', '“', '”', '’' ), array( '', '', '', "'" ), $text );

	if ( mb_strlen( $text ) > 160 ) {
		$text = mb_substr( $text, 0, 160 );
		$text = rtrim( $text, " tnrx0B-–—,;:" );
	}

	$result = array(
		'meta_description' => $text,
		'model'            => $json['model'] ?? 'unknown',
	);

	set_transient( $cache_key, $result, 30 * DAY_IN_SECONDS );

	return $result;
}

function bpcab_extract_text_from_openai_responses_api( array $json ) : string {
	if ( isset( $json['output'][0]['content'][0]['text'] ) && is_string( $json['output'][0]['content'][0]['text'] ) ) {
		return $json['output'][0]['content'][0]['text'];
	}

	$texts = array();

	if ( isset( $json['output'] ) && is_array( $json['output'] ) ) {
		foreach ( $json['output'] as $out ) {
			if ( empty( $out['content'] ) || ! is_array( $out['content'] ) ) {
				continue;
			}
			foreach ( $out['content'] as $c ) {
				if ( isset( $c['text'] ) && is_string( $c['text'] ) ) {
					$texts[] = $c['text'];
				}
			}
		}
	}

	return trim( implode( ' ', $texts ) );
}

/**
 * Script éditeur Gutenberg.
 */
add_action( 'enqueue_block_editor_assets', function () {
	$handle = 'bpcab-ai-meta-description-editor';

	wp_enqueue_script(
		$handle,
		plugins_url( 'bpcab-ai-meta-description-editor.js', __FILE__ ),
		array( 'wp-data', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-api-fetch', 'wp-plugins' ),
		'1.0.0',
		true
	);

	wp_localize_script(
		$handle,
		'BPCAB_AIMD',
		array(
			'restUrl' => esc_url_raw( rest_url( 'bpcab/v1/meta-description' ) ),
			'nonce'   => wp_create_nonce( 'wp_rest' ),
		)
	);
} );

Пълен JS файл

( function ( wp ) {
	const { registerPlugin } = wp.plugins;
	const { PluginDocumentSettingPanel } = wp.editPost;
	const { TextControl, Button, Notice } = wp.components;
	const { useSelect, useDispatch } = wp.data;
	const { useState } = wp.element;

	function AIMetaDescriptionPanel() {
		const postId = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostId(), [] );
		const currentMeta = useSelect( ( select ) => {
			const meta = select( 'core/editor' ).getEditedPostAttribute( 'meta' );
			return meta ? meta._bpcab_meta_description : '';
		}, [] );

		const { editPost } = useDispatch( 'core/editor' );

		const [ value, setValue ] = useState( currentMeta || '' );
		const [ isLoading, setIsLoading ] = useState( false );
		const [ error, setError ] = useState( '' );
		const [ info, setInfo ] = useState( '' );

		async function generate( mode ) {
			setError( '' );
			setInfo( '' );
			setIsLoading( true );

			try {
				const res = await wp.apiFetch( {
					url: BPCAB_AIMD.restUrl,
					method: 'POST',
					headers: {
						'X-WP-Nonce': BPCAB_AIMD.nonce,
					},
					data: {
						post_id: postId,
						mode: mode,
					},
				} );

				if ( res && res.meta_description ) {
					setValue( res.meta_description );
					editPost( { meta: { _bpcab_meta_description: res.meta_description } } );

					setInfo( res.saved ? 'Meta description générée et enregistrée.' : 'Meta description générée (non enregistrée).' );
				} else {
					setError( 'Réponse inattendue du serveur.' );
				}
			} catch ( e ) {
				setError( e.message ? e.message : 'Erreur lors de l’appel REST.' );
			} finally {
				setIsLoading( false );
			}
		}

		return wp.element.createElement(
			PluginDocumentSettingPanel,
			{
				name: 'bpcab-ai-meta-description',
				title: 'Meta description (IA)',
				className: 'bpcab-ai-meta-description-panel',
			},
			error ? wp.element.createElement( Notice, { status: 'error', isDismissible: true }, error ) : null,
			info ? wp.element.createElement( Notice, { status: 'info', isDismissible: true }, info ) : null,
			wp.element.createElement( TextControl, {
				label: 'Meta description',
				help: 'Astuce : relisez et ajustez. L’IA est un point de départ, pas une vérité.',
				value: value,
				onChange: ( v ) => {
					setValue( v );
					editPost( { meta: { _bpcab_meta_description: v } } );
				},
			} ),
			wp.element.createElement(
				'div',
				{ style: { display: 'flex', gap: '8px' } },
				wp.element.createElement( Button, { variant: 'secondary', isBusy: isLoading, disabled: isLoading, onClick: () => generate( 'draft' ) }, 'Générer (aperçu)' ),
				wp.element.createElement( Button, { variant: 'primary', isBusy: isLoading, disabled: isLoading, onClick: () => generate( 'save' ) }, 'Générer & enregistrer' )
			)
		);
	}

	registerPlugin( 'bpcab-ai-meta-description', {
		render: AIMetaDescriptionPanel,
	} );
} )( window.wp );

Обяснение на кода

Защо поле за мета публикация, а не директно поле за „SEO плъгин“?

Всеки SEO плъгин има свои собствени мета ключове. Като първо ги съхраните в _bpcab_meta_description, ВИЕ:

  • останат независими.
  • Можете да сменяте SEO плъгини, без да губите текста си.
  • След това можете да се свържете с Yoast/Rank Math/SEOPress чрез hook.

Защо REST + Gutenberg, а не „PHP бутон“?

Gutenberg е JavaScript приложение в администраторския панел. Ако искате адаптивен бутон, REST е най-стабилният подход. И най-важното е, че API ключът остава на сървърната страна.

Защо преходният кеш е от съществено значение

Без кеш, плащате:

  • с всяко щракване,
  • с всяко освежаване,
  • при всеки конфликт в черновата (тестване от двама автори).

С кеш, базиран на хеша на съдържанието, няма да плащате отново, докато текстът не се промени.

Защо „дезинфекцираме“ реакцията на изкуствения интелект?

Отговорът на ИИ е външно съдържание. Въпреки че рискът от инжектиране на HTML е нисък в този контекст, съм виждал да връща типографски кавички, нови редове и понякога тагове, ако подканата е лошо написана. Тук го форсираме:

  • wp_strip_all_tags() : премахва всички етикети,
  • sanitize_text_field() : почиства прашките,
  • контрол на дължината.

Цени и оптимизация на API

Цените зависят от модела и обема на токените (вход + изход). За мета описание:

  • Запис: заглавие + от 1000 до 3000 знака почистено съдържание,
  • Изход: ~160 знака.

На практика това е „малко“ искане. Реалният риск от разходи идва главно от:

  • от многократна регенерация (оттук и кешът),
  • от изпращането на пълната статия (оттук и почистването и идеята за ограничаване на изпратеното съдържание).

Конкретни стратегии

  • Изпрати само интрото (напр. първите 1500 знака): често достатъчно за обобщаване.
  • Модел „Мини“ : добро съотношение цена-качество за кратък текст.
  • Дълъг кеш (7 до 30 дни) + изчистване само ако съдържанието се промени.
  • Пакетно извън администратора : за миграция, направете WP-CLI (вариант по-долу) и стартирайте през нощта.

Разширени варианти и случаи на употреба

Вариант 1 — изпратете само въведението

Често получавам по-добри резултати, като изпращам въведението плюс H2 заглавията, вместо цялата статия. За начинаещи най-лесното нещо е да съкратят съдържанието, което изпращат.

// Remplacez $content_plain par une version limitée.
$content_plain = mb_substr( $content_plain, 0, 1500 );

Вариант 2 — Синхронизиране с SEO плъгин

Пример: запазвате вътрешните си мета данни, но ги копирате и в SEO плъгин чрез hook-а bpcab_meta_description_saved.

Забележка: Мета ключовете варират в зависимост от плъгина и неговата версия. Проверете документацията на плъгина или прегледайте таблицата. wp_postmeta в статия, където сте попълнили полето ръчно.

add_action( 'bpcab_meta_description_saved', function ( int $post_id, string $meta_description ) {
	// Exemple fictif : adaptez à votre plugin SEO après vérification.
	// update_post_meta( $post_id, '_yoast_wpseo_metadesc', $meta_description );
	// update_post_meta( $post_id, 'rank_math_description', $meta_description );
}, 10, 2 );

Вариант 3 — Съвместимост с Divi 5 / Elementor / Avada

Добра новина: генерирането се извършва от страната на WordPress (REST + post meta). Така че:

  • Диви 5 Ако редактирате статиите/страниците си в Gutenberg (или дори Divi), мета тагът се съхранява на ниво публикация. Divi не пречи на запазването на мета таговете на публикациите.
  • Elementor Същото е и тук. Elementor редактира съдържанието, но мета данните на публикацията остават достъпни. Можете да задействате генерирането от Gutenberg, след което да запазите мета данните за SEO показване.
  • Avada : същата логика. Строителят не се намесва в update_post_meta().

Ако използвате изключително редактор за изграждане и никога Gutenberg, все още можете да използвате този плъгин: генерирането се задейства чрез REST, така че можете да добавите малка страница с инструменти в администраторския панел (не е включена тук) или да стартирате масово генериране чрез WP-CLI.

Безопасност и най-добри практики

  • Никога не разкривайте API ключа от страна на клиента Няма директна заявка от браузъра към OpenAI. Винаги през вашия WordPress сървър.
  • REST разрешения : ние проверяваме current_user_can('edit_post', $post_id)Без него всеки влязъл в системата потребител би могъл да генерира (и да ви струва пари).
  • Ограничаване на скоростта Ако имате много автори, добавете ограничение на потребител (напр. 20 поколения/час). Без ограничение, един бутон, който е спам, може да бъде скъпоструващ.
  • санитарна обработка Отговорът на изкуствения интелект се почиства преди запазване. Дори ако е „просто текст“, третирайте го като външен вход.
  • Бисквитки Изпращате съдържание от статия на трета страна. Ако съдържанието ви съдържа лични данни (имена, имейли, препоръки), трябва да се погрижите за това (правно основание, информация, DPA според доставчика). Не генерирайте съдържание, базирано на сляпо, от потребителите.

Справка за сигурност на REST: REST API удостоверяване.

Просто ограничаване на скоростта (пример)

Ето един прост подход за преходно действие за всеки потребител. Той не замества истински WAF, но предотвратява неволна злоупотреба.

function bpcab_rate_limit_ok( int $user_id, int $limit = 20, int $window_seconds = 3600 ) {
	$key   = 'bpcab_rl_' . $user_id;
	$count = (int) get_transient( $key );

	if ( $count >= $limit ) {
		return false;
	}

	set_transient( $key, $count + 1, $window_seconds );
	return true;
}

Наричаш го в началото на bpcab_rest_generate_meta_description() и изпращате 429, ако е необходимо.

Как да тествате и отстранявате грешки

1) Активиране на лог файловете на WordPress

категория wp-config.php (в тестова среда, не в производствена), активирайте:

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

Грешките ще влязат в wp-content/debug.log.

2) Тествайте REST крайната точка без Gutenberg

Преди да заподозрете JavaScript, тествайте крайната точка с curlИзвлечете nonce от администраторския панел (или използвайте удостоверен REST инструмент), след което:

curl -X POST "https://votre-site.tld/wp-json/bpcab/v1/meta-description" 
  -H "Content-Type: application/json" 
  -H "X-WP-Nonce: VOTRE_NONCE" 
  -d '{"post_id":123,"mode":"draft"}'

3) Проверете отговора на OpenAI в случай на грешка

Когато API върне код, различен от 2xx, плъгинът връща body Подробно. На активен сайт избягвайте да показвате тези подробности на авторите (те могат да съдържат техническа информация). Запазете ги в лог файловете.

4) Капани, които често виждам

  • Кодът е поставен в functions.php на родителската тема: актуализация на темата = изгубен код.
  • Забравена скоба: фатална грешка, бял екран в администраторския панел.
  • PHP е твърде стар (7.4/8.0): стриктно типизиране и някои функции се държат различно.
  • Конфликт с плъгин за фрагмент: частично деактивиран фрагмент може да повреди редактора.
  • Административен кеш: променяте JS, но браузърът запазва старата версия. Извършете „твърдо презареждане“.

Ако това не проработи

Ето диагностична диаграма, базирана на реални случаи на поддръжка.

симптом Вероятна причина проверка Решение
Бутонът не се показва в редактора. Скриптът не е зареден или има лоши зависимости Конзола на браузъра (F12) + раздел „Мрежа“ Проверка enqueue_block_editor_assets, зависимости (wp-plugins, wp-edit-post) и пътят до JS файла
Грешка „Липсва API ключ“ Липсва константа или е направена печатна грешка wp-config.php ТЪРСИ BPCAB_OPENAI_API_KEY в wp-config.php Добави define() на правилното място, без скрити празнини
Грешка 401/403 от страната на AI ​​API Невалиден ключ, изтекъл ключ или разрешения за акаунт отговор details.body в REST отговора Генерирайте отново ключа, проверете фактурирането/квотите в API акаунта
Грешка 500 „Невалиден отговор на AI API“ Време за изчакване, квота или отговор, който не е JSON Консултирайте wp-content/debug.log увеличаване timeoutНамалете размера на съдържанието, което изпращате, проверете квотата си
Метаданните са генерирани, но не са запазени Използван е чернови режим или са недостатъчни права Проверка mode и капацитет edit_post Кликнете върху „Генериране и запазване“, проверете потребителската роля
Странен текст (кавички, две изречения, твърде дълго) Твърде либерален суфльор или твърде креативен модел Сравнете резултата с вашите ограничения Долна temperatureПринудително създаване на „изречение“, съкращаване до 160, премахване на кавички

Типични грешки в WordPress и решения

  • Неподходяща кука: ако сложите register_rest_route от init вместо rest_api_initМаршрутът може да не е наличен в точния момент.
  • Объркване между действие и филтър: rest_api_init е действие („правите“ нещо), а не филтър.
  • Постоянни връзки: REST API не зависи от постоянни връзки, но някои плъгини за сигурност го блокират. /wp-json/Тествайте URL адреса директно.
  • Тестване в производствена среда без запазване: Лошо копиран плъгин може да повреди администраторския панел. Винаги правете резервно копие и го тествайте в тестова среда.

ресурси

Често задавани въпроси

Google все още ли използва мета описанието?

Не винаги. Google може да пренапише фрагмента. Но доброто мета описание остава полезно: то често влияе на фрагмента, когато той съвпада точно със заявката, а е полезно и за други платформи (споделяне, предварителен преглед).

Защо да не се генерира автоматично всеки път, когато се запазва статия?

Защото ще плащате за AI извиквания в цикъл (автоматично запазване, ревизии, корекции). Предпочитам ясен бутон. Ако искате автоматични актуализации, правете ги само при първото „публикуване“ и с ограничение за кеш и честота.

Мога ли да използвам Anthropic, Mistral или Google вместо това?

Да. Запазете същата архитектура: вътрешен REST + wp_remote_post() + кеш + дезинфекция. Променя се само форматът на заявка/отговор.

Защо да съхранявате в мета данни на публикацията, а не в ACF поле?

Постмета слоят е нативният слой. ACF също съхранява данни в постмета слоя, но добавя конфигурационен слой. За мета описание, нативната версия е по-проста и по-преносима.

Съвместим ли е с Divi 5 / Elementor / Avada?

Да за съхранението. Панелът за генериране е в Gutenberg. Ако никога не използвате Gutenberg, добавете страница с инструменти (или WP-CLI), за да задействате генерирането.

Бутонът се завърта в цикъл („isLoading“) и не връща нищо.

Обикновено: крайната точка е блокирана от плъгин за сигурност, невалиден nonce или грешка 500 от страна на сървъра. Проверете мрежовата конзола (заявка към /wp-json/bpcab/v1/meta-description) и файлът debug.log.

Получавам съобщението „Вероятно сте офлайн.“ в Гутенберг

Това е общо съобщение от wp.apiFetchИстинската причина често е блокиран REST отговор (403), невалиден JSON или изтичане на времето за изчакване. Проверете отговора в раздела „Мрежа“.

Как да изчистя кеша, ако искам да наложа ново поколение?

Кешът е временен, базиран на хеша на заглавието и съдържанието. Ако промените съдържанието, хешът се променя и кешът вече не е валиден. Като алтернатива можете да намалите продължителността (7 дни) или да добавите параметър „force“ (не е включен тук).

Мога ли да генерирам данни за 1000 стари статии?

Да, но не ръчно от администраторския панел. Създайте WP-CLI скрипт или cron задание с ограничаване на скоростта и следете разходите. Препоръчвам да започнете с 20 статии, да проверите качеството и след това да разширите.

Защо мета описанието ми съдържа странни символи (—, …)?

Това е нормално: понякога изкуственият интелект използва типографска пунктуация. Кодът премахва кавичките, но запазва тиретата. Ако искате 100% ASCII, добавете нормализация (направете това внимателно на френски).

Мога ли да показвам това мета описание във фронтенд страницата, ако нямам SEO плъгин?

Да. Вашата тема може да чете get_post_meta( get_the_ID(), '_bpcab_meta_description', true ) и го инжектирайте в <meta name="description">Направете го правилно чрез wp_head и бягство с esc_attr()Ако искаш, мога да ти дам точния откъс според твоята тема.