Si vous avez déjà collé un shortcode dans une page… et obtenu une page blanche ou un “Erreur critique”, vous savez que le moindre détail compte. Ici, on va créer un shortcode WordPress (WordPress 6.9.4, PHP 8.1+) qui appelle une API d’IA côté serveur via wp_remote_post(), met la réponse en cache avec les Transients, et affiche un contenu propre (sanitisé) sur vos pages.


Le besoin / Le cas d’usage

Un shortcode est une petite “commande” que vous insérez dans l’éditeur WordPress, par exemple [mon_shortcode], et que WordPress remplace par du contenu généré dynamiquement. Le problème classique des blogueurs : produire des micro-contenus utiles (accroches, plans, FAQ, résumés, idées de titres) sans y passer 45 minutes à chaque fois.

Un shortcode IA devient intéressant quand vous voulez générer :

  • Une FAQ à partir d’un sujet (“FAQ sur la pose de parquet stratifié”).
  • Un résumé d’un article (à partir d’un extrait que vous fournissez).
  • Des idées de titres pour une page de vente, une recette, un tutoriel.
  • Une introduction adaptée à votre ton éditorial.

Ce que vous saurez implémenter à la fin : un shortcode du type [ai_content prompt="..."] qui appelle l’API OpenAI (sans SDK), gère les erreurs, met en cache les résultats, et reste compatible avec Divi 5, Elementor et Avada (puisqu’un shortcode fonctionne partout où WordPress interprète les shortcodes).

Résumé rapide

  • Vous stockez la clé API dans wp-config.php (jamais en dur dans le plugin).
  • Vous créez un mu-plugin (recommandé) qui enregistre le shortcode [ai_content].
  • Le shortcode appelle l’API via wp_remote_post() (HTTP côté serveur).
  • Vous mettez en cache avec Transients API pour réduire les coûts et accélérer l’affichage.
  • Vous sanitisez la sortie (HTML autorisé limité) et vous gérez les timeouts.
  • Vous ajoutez un rate limiting simple pour éviter les abus.

Quand utiliser l’IA pour ça

J’utilise ce pattern “shortcode IA” quand le contenu :

  • n’a pas besoin d’être parfait au mot près (ex : idées, brouillon, structure, variantes).
  • peut être mis en cache (ex : une FAQ identique pour tous les visiteurs).
  • est déclenché par vous (ou par un admin), pas par un formulaire public.
  • doit s’afficher dans une page builder : Divi 5, Elementor et Avada gèrent les shortcodes dans leurs modules/éléments texte.

Exemple concret : une page “Services” avec un bloc FAQ généré à partir d’un prompt stable. Vous le générez une fois, vous le cachez 7 jours, et vous ne payez pas l’API à chaque visite.

Quand ne PAS utiliser l’IA

Vous allez vous simplifier la vie (et économiser de l’argent) si vous n’utilisez pas l’IA dans ces cas :

  • Contenu 100% factuel (horaires, prix, spécifications). Une simple page ou un champ ACF fait mieux.
  • Contenu dépendant du visiteur (personnalisation par utilisateur) : vous risquez d’exploser les coûts et de créer des fuites de données.
  • Formulaire public “posez une question” sans garde-fous : vous allez vous faire spammer (et facturer).
  • Performance critique : un appel API en front peut ralentir. Même avec cache, le premier hit reste plus lent.
  • Vous pouvez le faire en SQL/PHP natif : ex. afficher les 5 derniers articles, filtrer des CPT, calculer une moyenne. Zéro IA, zéro coût.

Dans mon expérience, l’erreur n°1 est de déclencher l’IA sur chaque affichage de page sans cache. Sur un site un peu visité, ça devient vite incontrôlable côté coûts et latence.

Prérequis

Versions et environnement

  • WordPress 6.9.4+ (avril 2026) et PHP 8.1+.
  • Extension PHP cURL ou transport HTTP compatible (WordPress gère plusieurs transports, mais cURL est courant).
  • Accès FTP/SFTP ou accès fichier via l’hébergeur.

Comprendre “API” et “appel HTTP” (avant le code)

Une API est une interface qui vous permet de demander un service à une plateforme (ici : générer du texte). Techniquement, votre site envoie une requête HTTP (souvent en POST) vers une URL, avec un JSON contenant votre demande (prompt, modèle, paramètres). La plateforme répond avec un JSON contenant le texte généré.

Dans WordPress, on fait ça proprement avec wp_remote_post() (API HTTP WordPress) plutôt que file_get_contents(). C’est plus compatible et ça gère mieux les erreurs.

Clé API : où la mettre (wp-config.php)

Vous avez besoin d’une clé API (ex : OpenAI). Attention : chaque appel coûte de l’argent. Une clé exposée publiquement peut être utilisée par un tiers, et vous payez.

Ajoutez ceci dans wp-config.php, idéalement juste au-dessus de la ligne “That’s all, stop editing!” :

<?php
// Clé API OpenAI (ne jamais committer ce fichier dans un dépôt public)
define( 'BPCAB_OPENAI_API_KEY', 'COLLEZ_VOTRE_CLE_API_ICI' );

// Optionnel : vous pouvez forcer un modèle par défaut
define( 'BPCAB_OPENAI_MODEL', 'gpt-4.1-mini' );

Ne mettez jamais cette clé dans un shortcode, une page, un widget, ou du JavaScript. Côté navigateur, elle serait visible.

Où coller le code WordPress (mu-plugin recommandé)

Pour un débutant, le plus robuste est un mu-plugin (Must-Use Plugin) : il se charge automatiquement, même si vous changez de thème. Créez le fichier :

  • wp-content/mu-plugins/bpcab-ai-shortcode.php

Si le dossier mu-plugins n’existe pas, créez-le. Source officielle : Must Use Plugins.


Architecture de la solution

Voici le flux, tel qu’il se passe en coulisses :

Éditeur (Divi/Elementor/Avada/WP) 
  → shortcode [ai_content prompt="..."]
    → PHP: validation + rate limit
      → cache (get_transient)
        → si cache OK : afficher
        → sinon :
          → wp_remote_post() vers API IA
            → décodage JSON
              → nettoyage (wp_kses_post)
                → set_transient()
                  → afficher

Pourquoi ces étapes

  • Validation : éviter qu’un visiteur injecte un prompt dangereux ou trop long.
  • Rate limit : éviter 200 appels IA sur une même IP (ou par page) en 2 minutes.
  • Cache : réduire les coûts + accélérer le site.
  • Timeout : éviter de bloquer PHP si l’API est lente.
  • Nettoyage de la sortie : vous ne devez pas afficher du HTML “libre” renvoyé par un service externe.

Le code complet — étape par étape

On va construire un shortcode [ai_content] avec des attributs simples :

  • prompt : votre demande (obligatoire)
  • format : text (par défaut) ou html
  • cache : durée en secondes (par défaut 86400 = 24h)

Étape 1 — enregistrer le shortcode

Un shortcode se déclare avec add_shortcode(). La fonction callback doit retourner une chaîne (pas faire echo). Doc officielle : add_shortcode().

<?php
// Sécurité : empêcher l'accès direct au fichier.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Enregistre le shortcode au chargement de WordPress.
 * Hook = action : un point d'accroche exécuté à un moment donné.
 */
add_action( 'init', function () {
	add_shortcode( 'ai_content', 'bpcab_ai_content_shortcode' );
} );

Étape 2 — écrire le callback du shortcode (validation + cache)

Vous allez voir deux fonctions WordPress importantes :

  • Transients API : get_transient() / set_transient() pour mettre en cache. Doc : Transients API.
  • Sanitization : sanitize_text_field() pour nettoyer une entrée simple. Doc : Sanitizing data.
/**
 * Shortcode: [ai_content prompt="..." format="text|html" cache="86400"]
 *
 * @param array $atts Attributs du shortcode.
 * @return string HTML affichable.
 */
function bpcab_ai_content_shortcode( $atts ) {
	$atts = shortcode_atts(
		array(
			'prompt' => '',
			'format' => 'text',
			'cache'  => 86400, // 24h
		),
		$atts,
		'ai_content'
	);

	// Validation basique
	$prompt_raw = (string) $atts['prompt'];
	$prompt_raw = trim( $prompt_raw );

	if ( $prompt_raw === '' ) {
		return '<div class="bpcab-ai-error">Prompt manquant. Exemple : [ai_content prompt="Donnez 5 titres SEO..."]</div>';
	}

	// Limiter la taille du prompt (évite les abus et les coûts)
	if ( strlen( $prompt_raw ) > 1200 ) {
		return '<div class="bpcab-ai-error">Prompt trop long (max 1200 caractères).</div>';
	}

	$format = ( $atts['format'] === 'html' ) ? 'html' : 'text';
	$cache_ttl = absint( $atts['cache'] );
	if ( $cache_ttl < 60 ) {
		$cache_ttl = 60; // minimum 1 minute
	}
	if ( $cache_ttl > 30 * DAY_IN_SECONDS ) {
		$cache_ttl = 30 * DAY_IN_SECONDS; // maximum 30 jours
	}

	// Nettoyage du prompt (on le garde en texte, pas de HTML)
	$prompt = sanitize_text_field( $prompt_raw );

	// Cache key stable (dépend du prompt + format + langue)
	$cache_key = 'bpcab_ai_' . md5( $prompt . '|' . $format . '|' . get_locale() );

	$cached = get_transient( $cache_key );
	if ( is_string( $cached ) && $cached !== '' ) {
		return $cached;
	}

	// Rate limit simple (par IP) pour éviter les abus en front
	$rate_ok = bpcab_ai_rate_limit_ok();
	if ( ! $rate_ok ) {
		return '<div class="bpcab-ai-error">Trop de requêtes. Réessayez dans une minute.</div>';
	}

	// Appel API IA
	$result = bpcab_ai_generate_via_openai( $prompt, $format );

	if ( is_wp_error( $result ) ) {
		// Ne pas exposer trop de détails en front
		return '<div class="bpcab-ai-error">Impossible de générer le contenu pour le moment.</div>';
	}

	// Mise en cache
	set_transient( $cache_key, $result, $cache_ttl );

	return $result;
}

Étape 3 — rate limiting (simple, mais efficace)

Ce rate limit n’est pas “anti-bot militaire”, mais il évite l’erreur fréquente : un shortcode placé sur une page très visitée + cache désactivé = facture API qui grimpe. On limite à quelques appels par minute et par IP.

/**
 * Rate limit très simple : X requêtes / minute / IP.
 * Retourne true si on autorise, false sinon.
 */
function bpcab_ai_rate_limit_ok() {
	$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : '';
	$ip = preg_replace( '/[^0-9a-fA-F:.]/', '', $ip );

	// Si pas d'IP (rare), on autorise mais on pourrait aussi refuser.
	if ( $ip === '' ) {
		return true;
	}

	$key = 'bpcab_ai_rl_' . md5( $ip );
	$data = get_transient( $key );

	if ( ! is_array( $data ) ) {
		$data = array(
			'count' => 0,
		);
	}

	$data['count'] = isset( $data['count'] ) ? (int) $data['count'] : 0;
	$data['count']++;

	// Autoriser 6 requêtes par minute et par IP
	$limit = 6;

	set_transient( $key, $data, MINUTE_IN_SECONDS );

	return ( $data['count'] <= $limit );
}

Étape 4 — appel à l’API OpenAI via wp_remote_post()

Ici on utilise wp_remote_post() (API HTTP WordPress). Doc : wp_remote_post().

Points pratiques :

  • On met un timeout raisonnable (ex : 20s). Sans ça, certains hébergements bloquent longtemps.
  • On vérifie le code HTTP et on gère les JSON invalides.
  • On nettoie la sortie : wp_kses_post() si HTML, sinon esc_html().
/**
 * Génère du contenu via OpenAI.
 *
 * @param string $prompt Prompt utilisateur (déjà nettoyé).
 * @param string $format 'text' ou 'html'.
 * @return string|WP_Error HTML prêt à afficher ou WP_Error.
 */
function bpcab_ai_generate_via_openai( $prompt, $format = 'text' ) {
	if ( ! defined( 'BPCAB_OPENAI_API_KEY' ) || BPCAB_OPENAI_API_KEY === '' ) {
		return new WP_Error( 'bpcab_no_api_key', 'Clé API manquante dans wp-config.php.' );
	}

	$model = defined( 'BPCAB_OPENAI_MODEL' ) && BPCAB_OPENAI_MODEL ? BPCAB_OPENAI_MODEL : 'gpt-4.1-mini';

	// Consigne système : très utile pour stabiliser la sortie
	$system = 'Vous êtes un assistant de rédaction. Répondez en français. Contenu utile, concret, sans blabla.';

	// On demande du HTML simple si format=html (ça reste à nettoyer côté WordPress)
	$user = $prompt;
	if ( $format === 'html' ) {
		$user .= "nnRetournez du HTML simple (p, ul, ol, li, strong, em). Pas de scripts, pas de styles inline.";
	}

	$body = array(
		'model' => $model,
		'input' => array(
			array(
				'role' => 'system',
				'content' => $system,
			),
			array(
				'role' => 'user',
				'content' => $user,
			),
		),
		// Paramètres raisonnables pour du contenu “site”
		'temperature' => 0.6,
		'max_output_tokens' => 450,
	);

	$args = array(
		'headers' => array(
			'Authorization' => 'Bearer ' . BPCAB_OPENAI_API_KEY,
			'Content-Type'  => 'application/json',
		),
		'body'    => wp_json_encode( $body ),
		'timeout' => 20,
	);

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

	if ( is_wp_error( $response ) ) {
		// Exemple réel : cURL error 28 (timeout)
		error_log( '[BPCAB AI] Erreur HTTP: ' . $response->get_error_message() );
		return $response;
	}

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

	if ( $code < 200 || $code >= 300 ) {
		// On log le body pour diagnostiquer (clé invalide, quota, etc.)
		error_log( '[BPCAB AI] HTTP ' . $code . ' Body: ' . $raw );
		return new WP_Error( 'bpcab_bad_status', 'Statut HTTP inattendu: ' . $code );
	}

	$data = json_decode( $raw, true );
	if ( ! is_array( $data ) ) {
		error_log( '[BPCAB AI] JSON invalide: ' . $raw );
		return new WP_Error( 'bpcab_bad_json', 'Réponse JSON invalide.' );
	}

	/*
	 * Extraction du texte :
	 * La structure exacte peut évoluer. On prévoit plusieurs chemins.
	 */
	$text = '';

	// Chemin courant (souvent présent) : output[0].content[0].text
	if ( isset( $data['output'][0]['content'][0]['text'] ) && is_string( $data['output'][0]['content'][0]['text'] ) ) {
		$text = $data['output'][0]['content'][0]['text'];
	}

	// Fallback : output_text (souvent pratique)
	if ( $text === '' && isset( $data['output_text'] ) && is_string( $data['output_text'] ) ) {
		$text = $data['output_text'];
	}

	$text = trim( (string) $text );

	if ( $text === '' ) {
		error_log( '[BPCAB AI] Réponse vide. Body: ' . $raw );
		return new WP_Error( 'bpcab_empty', 'Réponse IA vide.' );
	}

	// Nettoyage sortie
	if ( $format === 'html' ) {
		// Autoriser seulement le HTML “post” standard
		$clean = wp_kses_post( $text );
	} else {
		// Texte : on échappe en HTML, puis on remet des sauts de ligne lisibles
		$clean = nl2br( esc_html( $text ) );
	}

	// Enveloppe HTML pour styliser facilement
	return '<div class="bpcab-ai-content">' . $clean . '</div>';
}

Note : j’ai volontairement ajouté des error_log(). Sur un site réel, ces logs sont souvent ce qui vous sauve quand l’API répond “quota exceeded” ou quand votre hébergeur coupe les requêtes sortantes.


Le code assemblé complet

Copiez-collez tout ce fichier dans wp-content/mu-plugins/bpcab-ai-shortcode.php. Ensuite, vérifiez que la constante BPCAB_OPENAI_API_KEY est bien définie dans wp-config.php.

<?php
/**
 * Plugin Name: BPCAB - Shortcode IA (OpenAI)
 * Description: Ajoute le shortcode [ai_content] qui génère du contenu via une API IA côté serveur, avec cache et protections.
 * Author: BPCAB
 * Version: 1.0.0
 *
 * À placer dans: wp-content/mu-plugins/bpcab-ai-shortcode.php
 */

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

add_action( 'init', function () {
	add_shortcode( 'ai_content', 'bpcab_ai_content_shortcode' );
} );

/**
 * Shortcode: [ai_content prompt="..." format="text|html" cache="86400"]
 */
function bpcab_ai_content_shortcode( $atts ) {
	$atts = shortcode_atts(
		array(
			'prompt' => '',
			'format' => 'text',
			'cache'  => 86400,
		),
		$atts,
		'ai_content'
	);

	$prompt_raw = trim( (string) $atts['prompt'] );

	if ( $prompt_raw === '' ) {
		return '<div class="bpcab-ai-error">Prompt manquant. Exemple : [ai_content prompt="Donnez 5 titres SEO sur..."]</div>';
	}

	if ( strlen( $prompt_raw ) > 1200 ) {
		return '<div class="bpcab-ai-error">Prompt trop long (max 1200 caractères).</div>';
	}

	$format = ( $atts['format'] === 'html' ) ? 'html' : 'text';

	$cache_ttl = absint( $atts['cache'] );
	if ( $cache_ttl < 60 ) {
		$cache_ttl = 60;
	}
	if ( $cache_ttl > 30 * DAY_IN_SECONDS ) {
		$cache_ttl = 30 * DAY_IN_SECONDS;
	}

	$prompt = sanitize_text_field( $prompt_raw );

	$cache_key = 'bpcab_ai_' . md5( $prompt . '|' . $format . '|' . get_locale() );
	$cached = get_transient( $cache_key );

	if ( is_string( $cached ) && $cached !== '' ) {
		return $cached;
	}

	if ( ! bpcab_ai_rate_limit_ok() ) {
		return '<div class="bpcab-ai-error">Trop de requêtes. Réessayez dans une minute.</div>';
	}

	$result = bpcab_ai_generate_via_openai( $prompt, $format );

	if ( is_wp_error( $result ) ) {
		error_log( '[BPCAB AI] WP_Error: ' . $result->get_error_code() . ' - ' . $result->get_error_message() );
		return '<div class="bpcab-ai-error">Impossible de générer le contenu pour le moment.</div>';
	}

	set_transient( $cache_key, $result, $cache_ttl );

	return $result;
}

/**
 * Rate limit très simple : 6 requêtes / minute / IP.
 */
function bpcab_ai_rate_limit_ok() {
	$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : '';
	$ip = preg_replace( '/[^0-9a-fA-F:.]/', '', $ip );

	if ( $ip === '' ) {
		return true;
	}

	$key  = 'bpcab_ai_rl_' . md5( $ip );
	$data = get_transient( $key );

	if ( ! is_array( $data ) ) {
		$data = array( 'count' => 0 );
	}

	$data['count'] = isset( $data['count'] ) ? (int) $data['count'] : 0;
	$data['count']++;

	set_transient( $key, $data, MINUTE_IN_SECONDS );

	return ( $data['count'] <= 6 );
}

/**
 * Appel OpenAI via wp_remote_post() (sans SDK).
 */
function bpcab_ai_generate_via_openai( $prompt, $format = 'text' ) {
	if ( ! defined( 'BPCAB_OPENAI_API_KEY' ) || BPCAB_OPENAI_API_KEY === '' ) {
		return new WP_Error( 'bpcab_no_api_key', 'Clé API manquante dans wp-config.php.' );
	}

	$model = defined( 'BPCAB_OPENAI_MODEL' ) && BPCAB_OPENAI_MODEL ? BPCAB_OPENAI_MODEL : 'gpt-4.1-mini';

	$system = 'Vous êtes un assistant de rédaction. Répondez en français. Contenu utile, concret, sans blabla.';
	$user   = $prompt;

	if ( $format === 'html' ) {
		$user .= "nnRetournez du HTML simple (p, ul, ol, li, strong, em). Pas de scripts, pas de styles inline.";
	}

	$body = array(
		'model' => $model,
		'input' => array(
			array(
				'role' => 'system',
				'content' => $system,
			),
			array(
				'role' => 'user',
				'content' => $user,
			),
		),
		'temperature'       => 0.6,
		'max_output_tokens' => 450,
	);

	$args = array(
		'headers' => array(
			'Authorization' => 'Bearer ' . BPCAB_OPENAI_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 ) ) {
		error_log( '[BPCAB AI] Erreur HTTP: ' . $response->get_error_message() );
		return $response;
	}

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

	if ( $code < 200 || $code >= 300 ) {
		error_log( '[BPCAB AI] HTTP ' . $code . ' Body: ' . $raw );
		return new WP_Error( 'bpcab_bad_status', 'Statut HTTP inattendu: ' . $code );
	}

	$data = json_decode( $raw, true );
	if ( ! is_array( $data ) ) {
		error_log( '[BPCAB AI] JSON invalide: ' . $raw );
		return new WP_Error( 'bpcab_bad_json', 'Réponse JSON invalide.' );
	}

	$text = '';

	if ( isset( $data['output'][0]['content'][0]['text'] ) && is_string( $data['output'][0]['content'][0]['text'] ) ) {
		$text = $data['output'][0]['content'][0]['text'];
	}

	if ( $text === '' && isset( $data['output_text'] ) && is_string( $data['output_text'] ) ) {
		$text = $data['output_text'];
	}

	$text = trim( (string) $text );

	if ( $text === '' ) {
		error_log( '[BPCAB AI] Réponse vide. Body: ' . $raw );
		return new WP_Error( 'bpcab_empty', 'Réponse IA vide.' );
	}

	if ( $format === 'html' ) {
		$clean = wp_kses_post( $text );
	} else {
		$clean = nl2br( esc_html( $text ) );
	}

	return '<div class="bpcab-ai-content">' . $clean . '</div>';
}

Explication du code

Shortcode : ce que WordPress exécute

add_shortcode('ai_content', ...) dit à WordPress : “quand tu vois [ai_content], appelle cette fonction et remplace le shortcode par le HTML retourné”. Un piège courant : faire echo dans le callback. Ça marche parfois, puis ça casse un builder ou un cache. Retournez une chaîne, toujours.

Pourquoi le cache est indispensable

get_transient() récupère la version déjà générée. Si elle existe, on ne contacte pas l’API. Sur un site un peu visité, c’est la différence entre :

  • 1 appel IA par jour
  • et 10 000 appels IA par jour (et là, vous le verrez sur la facture).

Pourquoi on sanitise prompt et réponse

Entrée : sanitize_text_field() enlève des caractères et balises indésirables. Ce n’est pas “anti-tout”, mais ça évite les prompts injectés avec du HTML ou des choses bizarres.

Sortie : même si l’IA “promet” de renvoyer du HTML propre, vous devez filtrer :

  • wp_kses_post() garde un HTML similaire à celui autorisé dans un article WordPress, et supprime scripts/attributs dangereux.
  • Si vous affichez du texte, esc_html() empêche l’injection HTML.

Pourquoi un timeout de 20 secondes

Sans timeout, vous risquez des pages qui chargent indéfiniment si l’API est lente. Le timeout de 20s est un compromis. Sur certains hébergements mutualisés, j’ai vu des requêtes sortantes “ralenties” : sans timeout, PHP reste bloqué.

Pourquoi on log les erreurs

Les erreurs typiques ne se voient pas toujours en front (surtout si vous affichez un message générique). Les logs (error_log) vous donnent :

  • HTTP 401 : clé invalide
  • HTTP 429 : quota/rate limit côté fournisseur
  • HTTP 500 : incident côté API
  • JSON invalide : proxy, WAF, HTML injecté par un filtre

Coûts API et optimisation

Les tarifs varient selon le modèle et le fournisseur, et changent régulièrement. Prenez l’habitude de vérifier la page de pricing du fournisseur avant de mettre en production. Pour OpenAI : OpenAI Pricing.

Estimation simple (ordre de grandeur)

Un shortcode de FAQ “courte” peut consommer (selon modèle et réglages) quelques centaines à quelques milliers de tokens en entrée+sortie. Sans donner un chiffre “gravé dans le marbre”, retenez ceci :

  • Sans cache : coût proportionnel au trafic (dangereux).
  • Avec cache 24h : coût proportionnel au nombre de prompts uniques et au nombre de mises à jour.

Optimisations qui marchent vraiment

  • Cache long (7 à 30 jours) pour des blocs stables (FAQ, résumé de page fixe).
  • Modèle “mini” pour des tâches simples (idées de titres, listes). Gardez les gros modèles pour des contenus plus sensibles.
  • Limiter max_output_tokens : ça plafonne la longueur (et le coût).
  • Éviter les prompts dynamiques en front. Si le prompt contient la date, l’URL, ou des variables, vous multipliez les clés de cache.

Variantes et cas d’usage avancés

Variante 1 — générer une FAQ en HTML (prête pour une page)

Dans une page (éditeur WordPress ou builder), utilisez :

[ai_content format="html" cache="604800" prompt="Créez une FAQ (6 questions/réponses) sur l'entretien d'un vélo électrique. Style clair, phrases courtes."]

Le format HTML est pratique dans Divi 5 (module Texte), Elementor (widget Éditeur de texte) et Avada (élément Texte). Si votre builder n’affiche pas le rendu, vérifiez que l’option “Exécuter les shortcodes” est activée dans l’élément concerné (certains modules ont un toggle).

Variante 2 — shortcode “admin only” (éviter l’IA en public)

Si vous voulez que le shortcode ne fonctionne que pour les admins (utile en phase de rédaction), ajoutez ce check au début du callback :

// Exemple : limiter aux admins connectés
if ( ! current_user_can( 'manage_options' ) ) {
	return '';
}

Ça évite qu’un visiteur déclenche une génération (et donc un coût) si le cache expire.

Variante 3 — adapter à Anthropic ou Mistral

Le principe reste identique : wp_remote_post(), headers, JSON, extraction du texte, cache. Vous changez l’URL et le format de payload.

Je conseille de garder la même structure (fonction “generate”, transients, nettoyage) et de faire une fonction par fournisseur. Le jour où vous migrez, vous ne cassez pas vos shortcodes.

Compatibilité Divi 5 / Elementor / Avada : pièges réels

  • Divi 5 : certains modules mettent en cache le rendu côté builder. Si vous modifiez le prompt, videz le cache Divi et rechargez le builder.
  • Elementor : en mode éditeur, Elementor peut afficher un rendu “différent” du front. Testez toujours sur la page publiée.
  • Avada : si vous utilisez un système de cache agressif, la première génération peut être “capturée” et servie longtemps. Ajustez TTL et purge.

Sécurité et bonnes pratiques

Ne jamais exposer la clé API côté client

Évitez absolument :

  • un appel API en JavaScript depuis le navigateur
  • une clé dans un shortcode
  • une clé dans un thème ou plugin commité sur GitHub

La clé doit rester côté serveur (wp-config.php), et l’appel doit se faire en PHP.

Limiter les entrées et éviter les prompts “visiteur”

Le prompt est une entrée. Même si vous le nettoyez, un visiteur peut vous coûter de l’argent en boucle. Pour un site public, préférez :

  • des prompts fixes (dans le shortcode)
  • ou un formulaire réservé aux membres, avec nonce, quotas, et logs.

RGPD : ne pas envoyer n’importe quoi

Si vous envoyez à une API externe :

  • des emails, noms, adresses, données de santé
  • ou des données de compte

… vous devez cadrer ça (base légale, information, DPA si nécessaire). Pour un shortcode de contenu éditorial, gardez des prompts “génériques” et non personnels.

Sanitization de sortie : pourquoi wp_kses_post() est un minimum

Le HTML renvoyé par un modèle peut contenir des balises inattendues. wp_kses_post() enlève l’essentiel du dangereux. Doc : wp_kses_post().

Ne modifiez jamais le core

Je le précise parce que je l’ai déjà vu : ne collez pas ce code dans wp-includes ou un fichier WordPress. Utilisez un mu-plugin ou un plugin. Les mises à jour WordPress écrasent le core.


Comment tester et déboguer

1) Activez WP_DEBUG (sur un environnement de test)

Dans wp-config.php :

<?php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

Le log sera dans wp-content/debug.log. Doc officielle : Debugging in WordPress.

2) Testez avec un prompt très simple

Exemple :

[ai_content prompt="Donnez 3 idées de titres pour un article sur le compostage en appartement."]

Si ça échoue, inutile de tester des prompts complexes. Faites d’abord fonctionner “le tuyau” HTTP.

3) Vérifiez les erreurs HTTP dans les logs

Regardez debug.log :

  • 401/403 : clé invalide, clé non autorisée, ou restrictions côté compte
  • 429 : quota dépassé
  • 500/503 : incident fournisseur
  • cURL error 28 : timeout

4) Testez le cache

Rechargez la page plusieurs fois : après le premier appel, ça doit être instantané (ou presque). Si ce n’est pas le cas :

  • votre objet cache peut être désactivé, mais les transients DB doivent quand même marcher
  • ou votre code génère une clé différente à chaque fois (prompt variable, espaces, etc.)

Si ça ne marche pas

Voici un tableau de diagnostic basé sur des problèmes que j’ai réellement vus chez des blogueurs (surtout quand le code vient d’un vieux tutoriel ou d’un plugin de snippets).

Symptôme Cause probable Vérification Solution
Page blanche / “Erreur critique” Erreur PHP (point-virgule manquant, accolade en trop) Consultez wp-content/debug.log ou le log serveur Corrigez la syntaxe, collez le code dans un mu-plugin (pas dans l’éditeur de thème en prod)
Le shortcode s’affiche tel quel: [ai_content …] Shortcodes non interprétés dans le module builder Testez dans l’éditeur WordPress natif, ou un bloc “Shortcode” Dans Divi/Elementor/Avada, utilisez un module/élément qui exécute les shortcodes
“Impossible de générer le contenu” Clé API absente ou invalide Log: HTTP 401/403 Vérifiez define('BPCAB_OPENAI_API_KEY', ...) dans wp-config.php
Ça marche une fois, puis plus rien Rate limit déclenché Testez depuis une autre IP / attendez 1 minute Augmentez la limite ou restreignez le shortcode aux admins
Le site est lent au premier affichage Appel API en front + latence réseau Mesurez TTFB, regardez la waterfall Augmentez le cache TTL, pré-générez en admin, ou passez par un cron
Résultat “bizarre” ou HTML cassé L’IA renvoie du HTML non prévu Regardez le body brut loggé Forcer format="text" ou limiter strictement les balises autorisées
Rien ne s’affiche après changement de prompt Cache du builder / cache plugin / CDN Désactivez temporairement le cache, purge CDN Videz caches (plugin, serveur, Divi/Elementor), réduisez TTL pendant les tests
Erreur “Call to undefined function …” Code collé au mauvais endroit ou exécuté trop tôt Vérifiez que le fichier est bien dans mu-plugins Utilisez add_action('init', ...) et un mu-plugin, pas un fichier isolé
Erreur liée à PHP PHP trop ancien (hébergement) Outils > Santé du site Passez en PHP 8.1+ (recommandé), sinon adaptez (mais ce guide cible 8.1+)

Pièges fréquents (à éviter)

  • Copier le code dans functions.php du thème parent : à la prochaine mise à jour du thème, vous perdez tout. Utilisez un thème enfant ou mieux : mu-plugin.
  • Tester en production sans sauvegarde : une simple erreur de syntaxe peut bloquer l’admin. Faites un test sur staging.
  • Utiliser un vieux snippet trouvé sur un forum : beaucoup utilisent d’anciens endpoints ou des fonctions obsolètes. Ici, le code cible WordPress 6.9.4 et PHP 8.1+.
  • Oublier de purger le cache : vous croyez que “ça ne marche pas”, mais vous voyez une version mise en cache.

Ressources


FAQ

1) Est-ce que ce shortcode fonctionne dans Divi 5, Elementor et Avada ?

Oui, parce qu’un shortcode est interprété par WordPress. La seule nuance : certains modules/éléments n’exécutent pas les shortcodes automatiquement. Utilisez un module “Texte/Éditeur” qui supporte les shortcodes, ou un élément dédié “Shortcode”.

2) Pourquoi utiliser un mu-plugin plutôt que functions.php ?

Parce que functions.php dépend du thème. J’ai souvent vu des sites perdre leurs snippets après un changement de thème. Un mu-plugin reste actif quoi qu’il arrive.

3) Puis-je mettre la clé API dans une option WordPress (base de données) ?

Techniquement oui, mais je le déconseille aux débutants. wp-config.php est plus simple et évite d’exposer la clé via export de base ou écran d’options mal protégé.

4) Pourquoi mon contenu change à chaque affichage ?

Sans cache, l’IA peut répondre différemment (température) et vous payez à chaque fois. Avec ce code, le cache stabilise le rendu pendant la durée TTL.

5) Comment forcer une régénération si le cache est actif ?

Deux options simples :

  • réduisez temporairement cache="60" pendant vos tests
  • modifiez légèrement le prompt (ça change la clé de cache)

6) Est-ce que je peux générer du contenu long (1000+ mots) ?

Oui, mais augmentez max_output_tokens et attendez-vous à plus de latence et plus de coûts. Pour du long contenu, je préfère un outil en back-office (génération à la demande) plutôt qu’un shortcode en front.

7) Pourquoi ne pas appeler l’API en JavaScript ?

Parce que vous exposeriez la clé API au navigateur. Quelqu’un pourrait la récupérer et faire des appels à vos frais. Le serveur (PHP) est le bon endroit.

8) J’obtiens une erreur 429, que faire ?

429 signifie “trop de requêtes” ou “quota dépassé”. Vérifiez votre quota côté fournisseur. Augmentez le TTL de cache, réduisez le trafic qui déclenche l’IA, et gardez le rate limit côté WordPress.

9) Est-ce que ce code respecte WordPress 6.9.4 et PHP 8.1+ ?

Oui. Il utilise l’API HTTP WordPress, les Transients, et des fonctions de sécurité standards. Évitez de le faire tourner sur une version PHP plus ancienne : vous allez cumuler bugs et comportements différents.

10) Puis-je stocker le résultat dans l’article au lieu de l’afficher dynamiquement ?

Oui, mais ce n’est plus un simple shortcode. Il faut une action côté admin (bouton “Générer et insérer”) qui écrit dans post_content. Si vous voulez, je peux vous proposer une variante avec un endpoint REST sécurisé (nonce + capability) et insertion dans l’éditeur.