Si vous voyez des dizaines d’appels identiques à une API IA dans vos logs (ou votre facture), le problème vient rarement du modèle. Il vient presque toujours d’un cache absent, mal dimensionné, ou cassé par une clé de cache trop “naïve”.
Le besoin / Le cas d’usage
Sur WordPress 6.9.4 (avril 2026), l’intégration d’IA côté serveur est devenue banale : résumer un article, proposer des titres SEO, classer des commentaires, générer une meta description, produire un encart “À retenir”, etc. Le piège : vous appelez l’API à chaque affichage de page, à chaque refresh d’éditeur, ou à chaque requête AJAX. Résultat : latence, coûts, et parfois un blocage (quota/rate limit).
Le cache intelligent des réponses IA avec les Transients API règle trois problèmes concrets :
- Performance : vous évitez 300–1200 ms (souvent plus) de réseau par affichage.
- Coûts : vous ne payez pas 100 fois la même réponse générée pour un contenu inchangé.
- Stabilité : si l’API est temporairement indisponible, vous pouvez servir une réponse en cache (ou un fallback).
Exemples où ça change tout :
- Un blog avec un widget “Résumé IA” en sidebar sur chaque article.
- Un site média qui affiche des “points clés” IA sur les pages catégories (beaucoup de trafic).
- Un site avec Elementor/Divi/Avada où un module dynamique déclenche des rendus multiples (prévisualisation, édition, rendu front).
- Un intranet où des utilisateurs demandent des explications sur des documents internes (requêtes répétitives).
À la fin, vous saurez implémenter un mini-plugin qui :
- appelle une API IA via
wp_remote_post(), - met en cache la réponse avec
get_transient()/set_transient(), - invalide intelligemment le cache quand le contenu change,
- protège l’API key, gère les timeouts, et nettoie la sortie (sécurité).
Résumé rapide
- Utilisez un cache clé = hash basé sur (prompt + paramètres + contenu + version de votre logique).
- Stockez l’API key dans wp-config.php, jamais en dur, jamais côté navigateur.
- Mettez un timeout court (10–20s max) et gérez les erreurs
WP_Error. - Ajoutez un verrou (lock) pour éviter le “thundering herd” (100 requêtes simultanées).
- Nettoyez la réponse :
wp_kses_post()pour HTML,sanitize_text_field()pour texte court. - Préférez un cache “long” (heures/jours) + invalidation sur
save_postplutôt qu’un TTL trop court.
Quand utiliser l’IA pour ça
Le cache des réponses IA est pertinent quand :
- la réponse est déterministe ou quasi-déterministe (même entrée → réponse similaire),
- la réponse est réutilisée sur plusieurs affichages (front, RSS, AMP, prévisualisation, etc.),
- vous avez un trafic significatif ou des pics (newsletter, Google Discover),
- vous voulez stabiliser le rendu (éviter des variations de texte d’un refresh à l’autre),
- vous avez des contenus qui changent peu (articles publiés, pages evergreen).
Dans mon expérience, le meilleur ratio “gain vs complexité” est : résumés, extractions de points clés, meta descriptions, catégorisation, FAQ générée à partir d’un article.
Quand ne PAS utiliser l’IA
Ne cachez pas (ou évitez l’IA) quand :
- la réponse dépend de données ultra-volatiles (cours de bourse, stock temps réel) : vous aurez des incohérences.
- vous avez besoin de personnalisation par utilisateur (ex : coaching) : le cache doit alors intégrer l’ID utilisateur, ce qui explose le nombre d’entrées.
- un simple calcul PHP/SQL fait mieux : ex. “nombre d’articles dans une catégorie”, “articles similaires” (taxonomies), “extrait” (WordPress le fait déjà).
- vous générez du contenu à la volée pour l’éditeur seulement : préférez un bouton “Générer” (action explicite) plutôt qu’un rendu automatique.
Anti-pattern que je vois souvent : un shortcode qui appelle l’IA à chaque rendu, placé dans un template global Divi/Elementor. Le site devient lent, et l’auteur ne comprend pas pourquoi “ça rame même quand je ne fais rien”.
Prérequis
Environnement visé :
- WordPress 6.9.4+
- PHP 8.1+
- HTTPS sortant autorisé (cURL/streams selon votre hébergeur)
Une clé API IA (exemples) :
- OpenAI (Responses API) : API Reference – Responses
- Anthropic : Anthropic Docs
- Mistral : Mistral Docs
Stockage de la clé dans wp-config.php (pas dans le plugin) :
<?php
// wp-config.php
// Clé API OpenAI (exemple). Ne la commitez jamais.
define('BPCAB_OPENAI_API_KEY', 'sk-...');
// Optionnel : version de votre logique IA pour invalider les caches en masse.
define('BPCAB_AI_CACHE_VERSION', '2026-04-12-1');
Sources officielles utiles :
- Transients API : developer.wordpress.org – Transients
- HTTP API : developer.wordpress.org – HTTP API
- Sanitization/Escaping : developer.wordpress.org – Sanitizing
- PHP hash : php.net – hash()
- WP-Cron : developer.wordpress.org – WP-Cron
Architecture de la solution
Flux (schéma textuel) :
WordPress (shortcode / bloc / module builder) → construit un prompt stable → calcule une clé de cache → get_transient()
→ (hit) renvoie la réponse cache
→ (miss) pose un lock transient → wp_remote_post() vers l’API IA → parse JSON → sanitize → set_transient() → renvoie la réponse
Étapes techniques, et pourquoi elles comptent :
- Clé de cache : si vous utilisez seulement
$post_id, vous casserez le cache dès que vous changez un paramètre (modèle, température, consignes). J’utilise un hash de tout ce qui influence la réponse. - TTL : un résumé d’article peut tenir 7 jours, voire 30, si vous invalidez sur
save_post. Un TTL trop court = vous payez quand même. - Lock : sans verrou, un pic de trafic sur un cache expiré déclenche 50 appels IA simultanés (thundering herd).
- Sanitization : les modèles peuvent produire du HTML, des liens, ou du texte inattendu. Vous devez filtrer.
- Timeout + retry raisonnable : un timeout de 60s peut saturer PHP-FPM sous charge.
Le code complet — étape par étape
1) Créer un mu-plugin (recommandé) ou un plugin
Pour éviter qu’un thème enfant ou un plugin de snippets “casse” votre site, j’installe souvent ce type de code en mu-plugin (chargé automatiquement).
Créez : wp-content/mu-plugins/bpcab-ai-transient-cache.php. Si le dossier n’existe pas, créez-le.
2) Un shortcode qui affiche un résumé IA d’un article
On part d’un cas simple et rentable : un shortcode [ai_resume] qui résume le contenu de l’article en cours. Le rendu sera identique tant que l’article et les paramètres n’ont pas changé.
2.1 Construire un prompt stable + paramètres
<?php
// Étape 2.1 : construire un prompt stable (pas de données inutiles, pas de variations)
function bpcab_build_summary_prompt( WP_Post $post ): array {
// On évite les variations : pas de dates "aujourd'hui", pas d'IDs aléatoires.
$title = wp_strip_all_tags( get_the_title( $post ) );
$content = wp_strip_all_tags( $post->post_content );
// Limite simple pour éviter d'envoyer un roman à l'API.
// Pour des articles très longs, préférez un chunking (voir variantes).
$content = mb_substr( $content, 0, 12000 );
$system = "Vous êtes un assistant éditorial. Produisez un résumé factuel et neutre.";
$user = "Titre : {$title}nnTexte :n{$content}nnConsignes :n- 5 à 7 phrases maximumn- pas de listesn- pas de jargonn- en français";
return [
'system' => $system,
'user' => $user,
];
}
2.2 Fabriquer une clé de cache “intelligente”
La clé doit changer si :
- le contenu change,
- vos consignes changent,
- vous changez de modèle,
- vous déployez une nouvelle version de votre logique.
<?php
// Étape 2.2 : clé de cache basée sur un hash de toutes les entrées pertinentes
function bpcab_ai_cache_key( array $payload, array $opts = [] ): string {
$version = defined('BPCAB_AI_CACHE_VERSION') ? (string) BPCAB_AI_CACHE_VERSION : 'v1';
$defaults = [
'provider' => 'openai',
'model' => 'gpt-4.1-mini', // Exemple : adaptez selon votre compte
'temp' => 0.2,
];
$opts = array_merge( $defaults, $opts );
// Attention : sérialisation stable. On trie pour éviter des variations d'ordre.
ksort( $payload );
ksort( $opts );
$raw = wp_json_encode(
[
'v' => $version,
'payload' => $payload,
'opts' => $opts,
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
// Hash court mais robuste.
$hash = hash( 'sha256', (string) $raw );
// Préfixe court : la longueur maximale d'une clé de transient est limitée.
return 'bpcab_ai_' . substr( $hash, 0, 32 );
}
2.3 Mettre un lock pour éviter les appels simultanés
Le lock est un transient séparé, avec un TTL court (ex : 30–60 secondes). Si un process le pose, les autres attendent (ou servent un cache périmé si vous implémentez “stale-while-revalidate”).
<?php
// Étape 2.3 : lock anti "thundering herd"
function bpcab_ai_lock_key( string $cache_key ): string {
return $cache_key . '_lock';
}
function bpcab_ai_try_lock( string $cache_key, int $ttl = 45 ): bool {
$lock_key = bpcab_ai_lock_key( $cache_key );
// add_option-like n'existe pas pour transients. On fait simple :
// si lock déjà présent, on refuse.
if ( false !== get_transient( $lock_key ) ) {
return false;
}
set_transient( $lock_key, 1, $ttl );
return true;
}
function bpcab_ai_unlock( string $cache_key ): void {
delete_transient( bpcab_ai_lock_key( $cache_key ) );
}
2.4 Appeler l’API IA avec wp_remote_post()
Exemple OpenAI Responses API. Pas de SDK, pas de Composer. Vous pouvez adapter à Anthropic/Mistral ensuite.
<?php
// Étape 2.4 : appel OpenAI via HTTP API WordPress
function bpcab_openai_responses_call( array $prompt, array $opts = [] ): array {
if ( ! defined('BPCAB_OPENAI_API_KEY') || ! BPCAB_OPENAI_API_KEY ) {
return [
'ok' => false,
'error' => 'Clé API manquante. Définissez BPCAB_OPENAI_API_KEY dans wp-config.php.',
];
}
$defaults = [
'model' => 'gpt-4.1-mini',
'temperature' => 0.2,
'max_output_tokens' => 220,
'timeout' => 20,
];
$opts = array_merge( $defaults, $opts );
$body = [
'model' => $opts['model'],
'input' => [
[
'role' => 'system',
'content' => [
[ 'type' => 'input_text', 'text' => (string) $prompt['system'] ],
],
],
[
'role' => 'user',
'content' => [
[ 'type' => 'input_text', 'text' => (string) $prompt['user'] ],
],
],
],
'temperature' => (float) $opts['temperature'],
'max_output_tokens' => (int) $opts['max_output_tokens'],
];
$args = [
'method' => 'POST',
'timeout' => (int) $opts['timeout'],
'headers' => [
'Authorization' => 'Bearer ' . BPCAB_OPENAI_API_KEY,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $body ),
];
$response = wp_remote_post( 'https://api.openai.com/v1/responses', $args );
if ( is_wp_error( $response ) ) {
return [
'ok' => false,
'error' => $response->get_error_message(),
];
}
$code = (int) wp_remote_retrieve_response_code( $response );
$raw = (string) wp_remote_retrieve_body( $response );
if ( $code < 200 || $code >= 300 ) {
return [
'ok' => false,
'error' => 'HTTP ' . $code . ' : ' . mb_substr( $raw, 0, 500 ),
];
}
$data = json_decode( $raw, true );
if ( ! is_array( $data ) ) {
return [
'ok' => false,
'error' => 'JSON invalide renvoyé par l’API.',
];
}
// Extraction "robuste" : l'API Responses peut renvoyer des structures variables.
// On tente d'extraire du texte en parcourant output.
$text = '';
if ( isset( $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 $c ) {
if ( is_array( $c ) && isset( $c['type'], $c['text'] ) && $c['type'] === 'output_text' ) {
$text .= (string) $c['text'];
}
}
}
}
$text = trim( $text );
if ( $text === '' ) {
return [
'ok' => false,
'error' => 'Réponse vide ou non parsable.',
];
}
return [
'ok' => true,
'text' => $text,
'raw' => $data,
];
}
2.5 Mettre en cache avec Transients API + fallback
On applique :
- cache TTL “long” (ex : 7 jours),
- lock court,
- sanitization,
- fallback si l’API échoue (afficher un extrait standard au lieu de casser la page).
<?php
// Étape 2.5 : get_or_set transient + lock + sanitation
function bpcab_ai_get_summary_for_post( int $post_id ): string {
$post = get_post( $post_id );
if ( ! $post instanceof WP_Post ) {
return '';
}
$prompt = bpcab_build_summary_prompt( $post );
$opts = [
'provider' => 'openai',
'model' => 'gpt-4.1-mini',
'temp' => 0.2,
];
$cache_key = bpcab_ai_cache_key(
[
'post_id' => (int) $post_id,
'post_modified' => (string) $post->post_modified_gmt,
'prompt' => $prompt,
],
$opts
);
$cached = get_transient( $cache_key );
if ( is_string( $cached ) && $cached !== '' ) {
return $cached;
}
// Lock : si quelqu'un est déjà en train de générer, on évite un second appel.
$locked = bpcab_ai_try_lock( $cache_key, 45 );
if ( ! $locked ) {
// Stratégie simple : on renvoie un fallback immédiat.
// Variante possible : attendre 200-400ms puis retenter get_transient().
return bpcab_ai_fallback_excerpt( $post );
}
try {
$result = bpcab_openai_responses_call(
$prompt,
[
'model' => $opts['model'],
'temperature' => $opts['temp'],
'max_output_tokens' => 220,
'timeout' => 20,
]
);
if ( ! $result['ok'] ) {
// On log en debug, mais on ne casse pas le front.
if ( defined('WP_DEBUG') && WP_DEBUG ) {
error_log( '[BPCAB AI] Erreur API: ' . (string) $result['error'] );
}
return bpcab_ai_fallback_excerpt( $post );
}
// Nettoyage : ici on veut du texte simple.
$text = sanitize_text_field( $result['text'] );
// Cache : 7 jours. Ajustez selon votre fréquence de mise à jour.
set_transient( $cache_key, $text, 7 * DAY_IN_SECONDS );
return $text;
} finally {
// Très important : libérer le lock même si exception.
bpcab_ai_unlock( $cache_key );
}
}
function bpcab_ai_fallback_excerpt( WP_Post $post ): string {
// Fallback sans IA : extrait WP (ou tronquage du contenu).
$excerpt = has_excerpt( $post ) ? $post->post_excerpt : wp_trim_words( wp_strip_all_tags( $post->post_content ), 40 );
return sanitize_text_field( $excerpt );
}
2.6 Exposer via un shortcode
<?php
// Étape 2.6 : shortcode [ai_resume] utilisable dans Divi/Elementor/Avada
add_shortcode( 'ai_resume', function( $atts ) {
if ( is_admin() && ! wp_doing_ajax() ) {
// Évite des appels inattendus dans certains contextes d'admin.
return '';
}
$post_id = get_the_ID();
if ( ! $post_id ) {
return '';
}
$summary = bpcab_ai_get_summary_for_post( (int) $post_id );
// Sortie HTML minimale.
return '<div class="ai-resume">' . esc_html( $summary ) . '</div>';
} );
3) Invalidation du cache quand l’article change
Les transients n’ont pas de “tags” natifs. Si vous ne stockez pas les clés quelque part, vous ne pouvez pas facilement supprimer “tous les caches de ce post”.
Approche que j’utilise : une version de post (un “bump”) stockée en post meta, incluse dans la clé de cache. Quand le post est sauvegardé, on incrémente cette version. Ça invalide automatiquement toutes les entrées associées, sans avoir à les lister.
<?php
// Étape 3 : "bump" de version de cache par post
function bpcab_ai_get_post_cache_bump( int $post_id ): int {
$bump = (int) get_post_meta( $post_id, '_bpcab_ai_bump', true );
return max( 1, $bump );
}
function bpcab_ai_increment_post_cache_bump( int $post_id ): void {
$bump = bpcab_ai_get_post_cache_bump( $post_id );
update_post_meta( $post_id, '_bpcab_ai_bump', $bump + 1 );
}
add_action( 'save_post', function( $post_id, $post, $update ) {
// Sécurités classiques : autosave, révisions, permissions.
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! $post instanceof WP_Post ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Invalidation logique : on bump la version.
bpcab_ai_increment_post_cache_bump( (int) $post_id );
}, 10, 3 );
Ensuite, intégrez ce bump à la clé de cache (modifiez l’appel dans bpcab_ai_get_summary_for_post()) :
<?php
// À intégrer dans bpcab_ai_get_summary_for_post() lors de la construction de la clé :
$bump = bpcab_ai_get_post_cache_bump( (int) $post_id );
$cache_key = bpcab_ai_cache_key(
[
'post_id' => (int) $post_id,
'bump' => $bump,
'post_modified' => (string) $post->post_modified_gmt,
'prompt' => $prompt,
],
$opts
);
Le code assemblé complet
Copiez-collez ce fichier tel quel dans wp-content/mu-plugins/bpcab-ai-transient-cache.php. Vérifiez bien que votre serveur charge les mu-plugins (c’est standard), et que PHP 8.1+ est actif.
<?php
/**
* Plugin Name: BPCAB — Cache IA via Transients (OpenAI)
* Description: Exemple WordPress 6.9.4+ : cache intelligent des réponses IA avec Transients API + lock + invalidation par bump.
* Author: Votre équipe
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Construit un prompt stable pour résumer un article.
*/
function bpcab_build_summary_prompt( WP_Post $post ): array {
$title = wp_strip_all_tags( get_the_title( $post ) );
$content = wp_strip_all_tags( $post->post_content );
// Limite volontaire pour réduire coûts et latence.
$content = mb_substr( $content, 0, 12000 );
$system = "Vous êtes un assistant éditorial. Produisez un résumé factuel et neutre.";
$user = "Titre : {$title}nnTexte :n{$content}nnConsignes :n- 5 à 7 phrases maximumn- pas de listesn- pas de jargonn- en français";
return [
'system' => $system,
'user' => $user,
];
}
/**
* Retourne un "bump" par post pour invalider le cache sans avoir à supprimer des transients.
*/
function bpcab_ai_get_post_cache_bump( int $post_id ): int {
$bump = (int) get_post_meta( $post_id, '_bpcab_ai_bump', true );
return max( 1, $bump );
}
/**
* Incrémente le bump, ce qui invalide toutes les clés dépendantes.
*/
function bpcab_ai_increment_post_cache_bump( int $post_id ): void {
$bump = bpcab_ai_get_post_cache_bump( $post_id );
update_post_meta( $post_id, '_bpcab_ai_bump', $bump + 1 );
}
add_action( 'save_post', function( $post_id, $post, $update ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! $post instanceof WP_Post ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// À chaque sauvegarde, on invalide logiquement les caches IA liés au post.
bpcab_ai_increment_post_cache_bump( (int) $post_id );
}, 10, 3 );
/**
* Fabrique une clé de cache stable et courte.
*/
function bpcab_ai_cache_key( array $payload, array $opts = [] ): string {
$version = defined('BPCAB_AI_CACHE_VERSION') ? (string) BPCAB_AI_CACHE_VERSION : 'v1';
$defaults = [
'provider' => 'openai',
'model' => 'gpt-4.1-mini',
'temp' => 0.2,
];
$opts = array_merge( $defaults, $opts );
ksort( $payload );
ksort( $opts );
$raw = wp_json_encode(
[
'v' => $version,
'payload' => $payload,
'opts' => $opts,
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
$hash = hash( 'sha256', (string) $raw );
return 'bpcab_ai_' . substr( $hash, 0, 32 );
}
/**
* Clé de lock associée à une clé de cache.
*/
function bpcab_ai_lock_key( string $cache_key ): string {
return $cache_key . '_lock';
}
/**
* Essaie de poser un lock (transient court).
*/
function bpcab_ai_try_lock( string $cache_key, int $ttl = 45 ): bool {
$lock_key = bpcab_ai_lock_key( $cache_key );
if ( false !== get_transient( $lock_key ) ) {
return false;
}
set_transient( $lock_key, 1, $ttl );
return true;
}
/**
* Libère le lock.
*/
function bpcab_ai_unlock( string $cache_key ): void {
delete_transient( bpcab_ai_lock_key( $cache_key ) );
}
/**
* Fallback sans IA.
*/
function bpcab_ai_fallback_excerpt( WP_Post $post ): string {
$excerpt = has_excerpt( $post ) ? $post->post_excerpt : wp_trim_words( wp_strip_all_tags( $post->post_content ), 40 );
return sanitize_text_field( $excerpt );
}
/**
* Appel OpenAI (Responses API) via wp_remote_post().
*/
function bpcab_openai_responses_call( array $prompt, array $opts = [] ): array {
if ( ! defined('BPCAB_OPENAI_API_KEY') || ! BPCAB_OPENAI_API_KEY ) {
return [
'ok' => false,
'error' => 'Clé API manquante. Définissez BPCAB_OPENAI_API_KEY dans wp-config.php.',
];
}
$defaults = [
'model' => 'gpt-4.1-mini',
'temperature' => 0.2,
'max_output_tokens' => 220,
'timeout' => 20,
];
$opts = array_merge( $defaults, $opts );
$body = [
'model' => $opts['model'],
'input' => [
[
'role' => 'system',
'content' => [
[ 'type' => 'input_text', 'text' => (string) $prompt['system'] ],
],
],
[
'role' => 'user',
'content' => [
[ 'type' => 'input_text', 'text' => (string) $prompt['user'] ],
],
],
],
'temperature' => (float) $opts['temperature'],
'max_output_tokens' => (int) $opts['max_output_tokens'],
];
$args = [
'method' => 'POST',
'timeout' => (int) $opts['timeout'],
'headers' => [
'Authorization' => 'Bearer ' . BPCAB_OPENAI_API_KEY,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $body ),
];
$response = wp_remote_post( 'https://api.openai.com/v1/responses', $args );
if ( is_wp_error( $response ) ) {
return [
'ok' => false,
'error' => $response->get_error_message(),
];
}
$code = (int) wp_remote_retrieve_response_code( $response );
$raw = (string) wp_remote_retrieve_body( $response );
if ( $code < 200 || $code >= 300 ) {
return [
'ok' => false,
'error' => 'HTTP ' . $code . ' : ' . mb_substr( $raw, 0, 500 ),
];
}
$data = json_decode( $raw, true );
if ( ! is_array( $data ) ) {
return [
'ok' => false,
'error' => 'JSON invalide renvoyé par l’API.',
];
}
$text = '';
if ( isset( $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 $c ) {
if ( is_array( $c ) && isset( $c['type'], $c['text'] ) && $c['type'] === 'output_text' ) {
$text .= (string) $c['text'];
}
}
}
}
$text = trim( $text );
if ( $text === '' ) {
return [
'ok' => false,
'error' => 'Réponse vide ou non parsable.',
];
}
return [
'ok' => true,
'text' => $text,
'raw' => $data,
];
}
/**
* Retourne le résumé IA d'un post, avec cache transient + lock + fallback.
*/
function bpcab_ai_get_summary_for_post( int $post_id ): string {
$post = get_post( $post_id );
if ( ! $post instanceof WP_Post ) {
return '';
}
$prompt = bpcab_build_summary_prompt( $post );
$opts = [
'provider' => 'openai',
'model' => 'gpt-4.1-mini',
'temp' => 0.2,
];
$bump = bpcab_ai_get_post_cache_bump( (int) $post_id );
$cache_key = bpcab_ai_cache_key(
[
'post_id' => (int) $post_id,
'bump' => $bump,
'post_modified' => (string) $post->post_modified_gmt,
'prompt' => $prompt,
],
$opts
);
$cached = get_transient( $cache_key );
if ( is_string( $cached ) && $cached !== '' ) {
return $cached;
}
$locked = bpcab_ai_try_lock( $cache_key, 45 );
if ( ! $locked ) {
return bpcab_ai_fallback_excerpt( $post );
}
try {
$result = bpcab_openai_responses_call(
$prompt,
[
'model' => $opts['model'],
'temperature' => $opts['temp'],
'max_output_tokens' => 220,
'timeout' => 20,
]
);
if ( ! $result['ok'] ) {
if ( defined('WP_DEBUG') && WP_DEBUG ) {
error_log( '[BPCAB AI] Erreur API: ' . (string) $result['error'] );
}
return bpcab_ai_fallback_excerpt( $post );
}
$text = sanitize_text_field( $result['text'] );
// TTL : 7 jours. Si votre contenu change peu, vous pouvez monter à 30 jours.
set_transient( $cache_key, $text, 7 * DAY_IN_SECONDS );
return $text;
} finally {
bpcab_ai_unlock( $cache_key );
}
}
/**
* Shortcode utilisable dans Gutenberg, Divi 5, Elementor, Avada.
*/
add_shortcode( 'ai_resume', function( $atts ) {
if ( is_admin() && ! wp_doing_ajax() ) {
return '';
}
$post_id = get_the_ID();
if ( ! $post_id ) {
return '';
}
$summary = bpcab_ai_get_summary_for_post( (int) $post_id );
return '<div class="ai-resume">' . esc_html( $summary ) . '</div>';
} );
Explication du code
Pourquoi la clé de cache est un hash (et pas juste post_id)
Si vous cachez seulement par $post_id, vous allez servir une ancienne réponse quand :
- vous changez le modèle (ex : “mini” → “standard”),
- vous modifiez les consignes (ton, longueur),
- vous changez la température,
- vous corrigez un bug de parsing.
Le hash de payload + opts + version évite ces collisions. La constante BPCAB_AI_CACHE_VERSION sert de “bouton rouge” : vous la changez, tout est invalidé automatiquement (sans supprimer quoi que ce soit).
Pourquoi un “bump” en post meta
Les transients n’ont pas de mécanisme natif pour “supprimer tous les transients liés à ce post”. J’ai souvent vu des devs tenter un DELETE FROM wp_options WHERE option_name LIKE ... en prod. Mauvaise idée : vous pouvez supprimer des options non liées, et vous créez un risque de perf.
Le bump évite ça : vous ne supprimez rien, vous changez juste la version incluse dans la clé. Les vieux transients expirent naturellement.
Pourquoi un lock transient
Sans lock, un cache manqué sur une page populaire déclenche un effet domino. Avec 30 requêtes simultanées, vous faites 30 appels IA, vous payez 30 fois, et vous augmentez le risque de rate limit.
Le lock ici est volontairement simple. Ce n’est pas un mutex parfait (il y a une petite fenêtre de course), mais en pratique ça réduit drastiquement les doublons.
Pourquoi sanitize_text_field() (et quand préférer wp_kses_post())
Pour un résumé, je veux du texte pur. sanitize_text_field() enlève les tags et normalise. Si votre IA doit renvoyer du HTML (par exemple une liste <ul>), utilisez plutôt wp_kses_post(), puis échappez ce que vous affichez au bon endroit.
Pièges réalistes
- Copier le code au mauvais endroit : un fichier mu-plugin doit être directement dans
mu-plugins, pas dans un sous-dossier (sauf loader). - Oublier un point-virgule : un mu-plugin cassé casse tout le site. Testez d’abord en staging.
- Hook inadapté : si vous déclenchez l’appel IA sur
init“pour préchauffer”, vous allez appeler l’API sur chaque page, même sans besoin. - Conflit avec un cache de page : si vous affichez un résumé personnalisé par utilisateur, un cache full-page servira la mauvaise version.
- Snippet plugin : certains plugins de snippets chargent tard ou isolent le scope. En cas de bug, passez en plugin/mu-plugin.
Coûts API et optimisation
Le coût dépend du fournisseur, du modèle, et des tokens. Sans figer des tarifs (ils changent), vous pouvez raisonner en ordre de grandeur :
- Un résumé court d’article : souvent 500 à 2 000 tokens d’entrée + 100 à 300 tokens de sortie, selon longueur.
- Sans cache, un widget sur une page à 10 000 vues/mois = 10 000 appels. Avec cache (7 jours) et contenu stable, vous tombez à quelques dizaines.
Stratégies concrètes pour réduire la facture
- Cache long + invalidation : c’est la stratégie la plus rentable.
- Modèles “mini” pour tâches simples (résumés, meta description). Gardez les modèles plus lourds pour l’édition (action manuelle).
- Réduisez l’entrée : tronquez intelligemment, supprimez le boilerplate, évitez d’envoyer le HTML complet.
- Batch (si votre API le permet) : générer 20 résumés en back-office, pas à l’affichage.
- Pré-génération sur publication : générez au moment du
save_post(mais attention à l’expérience éditeur et aux timeouts).
Optimisation côté WordPress
- Si vous avez Redis/Memcached, les transients peuvent être stockés en object cache (meilleur). Sinon, ils tombent dans
wp_options. - Évitez des transients trop nombreux et trop longs si vous n’avez pas d’object cache. Un bump limite l’explosion, mais pas totalement.
Variantes et cas d’usage avancés
Variante 1 : “stale-while-revalidate” (servir le cache périmé)
Pour des pages très fréquentées, je préfère parfois servir une réponse “périmée” plutôt qu’un fallback. Pour ça, stockez :
- un transient “data” long (ex : 30 jours),
- un transient “fresh_until” court (ex : 24h) pour décider quand régénérer.
Au-delà du cadre de ce snippet, mais le pattern est : si “data” existe et lock échoue, servez “data”.
Variante 2 : cache par langue (multilingue)
Si vous utilisez Polylang/WPML, incluez la langue dans le payload :
// Exemple : ajoutez la langue dans le payload de bpcab_ai_cache_key()
$lang = function_exists('determine_locale') ? determine_locale() : get_locale();
$cache_key = bpcab_ai_cache_key(
[
// ...
'lang' => (string) $lang,
],
$opts
);
Variante 3 : intégration Divi 5 / Elementor / Avada
Divi 5
Le plus simple : utilisez le shortcode [ai_resume] dans un module “Code” ou “Texte” (selon configuration). Divi peut rendre plusieurs fois en builder ; c’est précisément pour ça que le cache est utile.
Elementor
Utilisez un widget “Shortcode” et collez [ai_resume]. J’ai souvent vu Elementor déclencher des rendus en preview + front : sans cache, vous doublez les appels.
Avada (Fusion Builder)
Ajoutez un élément “Shortcode” avec [ai_resume]. Si vous utilisez des layouts globaux, vérifiez que le résumé n’est pas placé sur des pages où il n’a pas de contexte de post (ex : page d’accueil statique).
Variante 4 : mise en cache d’un JSON (ex : classification)
Pour une classification (catégorie, score, tags), faites renvoyer du JSON strict, puis validez et stockez un tableau encodé.
<?php
// Exemple : stocker un tableau en transient (JSON)
$data = [
'tags' => [ 'wordpress', 'seo' ],
'score' => 0.82,
];
set_transient( $cache_key, wp_json_encode( $data ), 30 * DAY_IN_SECONDS );
// À la lecture :
$cached = get_transient( $cache_key );
$arr = is_string( $cached ) ? json_decode( $cached, true ) : null;
if ( ! is_array( $arr ) ) {
$arr = [];
}
Sécurité et bonnes pratiques
Ne jamais exposer la clé API côté client
Pas de JavaScript qui appelle l’API IA directement depuis le navigateur. Même si vous “masquez” la clé, elle fuit. Faites toujours l’appel côté serveur via wp_remote_post().
Valider les entrées (surtout si l’utilisateur fournit du texte)
- Limitez la taille :
mb_substr(),wp_trim_words(). - Nettoyez :
sanitize_textarea_field()ousanitize_text_field(). - Ajoutez des nonces si vous exposez une action via AJAX : developer.wordpress.org – Nonces
Rate limiting (simple et efficace)
Si vous offrez une fonctionnalité “Demander à l’IA” à vos visiteurs, mettez un rate limit par IP ou par utilisateur avec un transient compteur.
<?php
// Exemple minimal : 20 requêtes / 10 minutes par IP
function bpcab_ai_rate_limit_ok( string $bucket, int $limit = 20, int $window = 600 ): bool {
$key = 'bpcab_rl_' . substr( hash('sha256', $bucket ), 0, 24 );
$cur = (int) get_transient( $key );
if ( $cur >= $limit ) {
return false;
}
// Incrément simple (non atomique, mais suffisant pour beaucoup de sites)
set_transient( $key, $cur + 1, $window );
return true;
}
RGPD / données personnelles
Si vous envoyez du contenu utilisateur (commentaires, formulaires), vous envoyez potentiellement des données personnelles à un tiers. Documentez-le, minimisez les données, et évitez d’envoyer des emails, numéros, adresses. Dans les projets sensibles, je filtre avant en remplaçant des patterns (emails/téléphones) et je garde une trace de consentement si nécessaire.
Sanitization de la sortie IA
- Texte court :
sanitize_text_field()+esc_html()à l’affichage. - HTML autorisé :
wp_kses_post()+ affichage sans double échappement. - Liens : vérifiez
esc_url()si vous reconstruisez des balises<a>.
Comment tester et déboguer
1) Activez WP_DEBUG en staging
<?php
// wp-config.php (staging)
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
2) Testez d’abord sans cache (temporairement)
Quand je débogue, je commente temporairement le get_transient() et le set_transient() pour vérifier que l’appel API fonctionne. Ensuite je réactive le cache.
3) Inspectez la réponse HTTP
Si vous obtenez une erreur, logguez :
- le code HTTP,
- les 300–500 premiers caractères du body (pas plus, pour éviter de logguer des données sensibles),
- le temps d’exécution (si vous mesurez).
4) Vérifiez que votre hébergeur autorise les requêtes sortantes
Sur certains hébergements, les requêtes sortantes sont filtrées. Le symptôme typique : cURL error 28 (timeout) ou refus réseau.
5) Vérifiez l’object cache
Si vous utilisez Redis/Memcached, les transients seront plus efficaces. Sinon, ils vont dans la DB. Pour comprendre le comportement, voyez la doc Transients : developer.wordpress.org – Transients.
Si ça ne marche pas
Voici les pannes que je croise le plus souvent, et comment les isoler.
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Le résumé n’apparaît jamais | Shortcode rendu hors boucle / pas de post ID | Testez sur une page “Article” standard, logguez get_the_ID() |
Adapter le code pour accepter post_id en attribut, ou utiliser get_queried_object_id() |
| Temps de chargement très long | Timeout trop haut, API lente, pas de cache hit | Log du temps, vérifiez si get_transient() hit |
Réduire timeout (15–20s), augmenter TTL, pré-générer |
| Erreur HTTP 401/403 | Clé invalide / permissions / projet mal configuré | Regardez le body renvoyé par l’API | Regénérer la clé, vérifier le compte et les restrictions |
| Erreur HTTP 429 | Quota ou rate limit | Logs + dashboard fournisseur | Mettre lock + cache, rate limiting côté WP, backoff |
| Cache “ne marche pas” (toujours miss) | Clé change à chaque fois (données instables) | Logguez la clé + le payload | Retirer timestamps, trier les tableaux, stabiliser le prompt |
| Le cache ne se met jamais à jour après modification | Bump non incrémenté (hook save_post non déclenché) | Vérifiez autosave/révision, permissions, type de post | Ajuster conditions, ajouter hook sur custom post type si besoin |
| Erreur fatale après ajout du fichier | Syntaxe PHP (point-virgule, accolade), PHP trop ancien | Consultez wp-content/debug.log ou logs serveur |
Corriger la syntaxe, vérifier PHP 8.1+, tester en staging |
Problème classique : code collé dans functions.php
Si votre thème se met à jour, vous perdez le code. Si vous faites une erreur, vous cassez le front. Passez en plugin ou mu-plugin. C’est plus propre, et ça se désactive facilement.
Problème classique : conflit de cache de page
Si vous avez un cache full-page (plugin, Varnish, Cloudflare), vous pouvez voir des comportements “bizarres” :
- vous changez le post, mais vous voyez l’ancien résumé : c’est le cache de page, pas le transient,
- vous testez en navigation privée et ça “marche”, car vous contournez un cache navigateur/CDN.
Solution : purgez le cache de page à la publication, ou servez le résumé uniquement sur les pages où c’est nécessaire.
Ressources
- Transients API (WordPress)
- HTTP API (wp_remote_post, timeouts, headers)
- Sanitizing Data (WordPress Security)
- Nonces (WordPress)
- OpenAI Responses API reference
- PHP hash()
- Code source WordPress (GitHub mirror)
FAQ
Quelle durée (TTL) choisir pour un transient de réponse IA ?
Pour un résumé d’article publié : 7 à 30 jours marche bien si vous invalidez sur save_post via un bump. Pour une page souvent modifiée : 6 à 24 heures, ou bump uniquement.
Est-ce que les transients sont stockés en base de données ?
Oui, si vous n’avez pas d’object cache persistant. Avec Redis/Memcached, ils peuvent être stockés en mémoire. Référence : Transients API.
Pourquoi mon cache “rate” alors que je ne change rien ?
La clé change probablement à chaque requête. Les causes typiques : vous incluez une date (“aujourd’hui”), un ID de session, un tableau non trié, ou un contenu HTML qui varie (espaces, attributs).
Puis-je stocker du HTML généré par l’IA en transient ?
Oui, mais filtrez avec wp_kses_post() (ou une whitelist stricte) avant stockage, puis affichez sans double échappement. Si vous faites esc_html() sur du HTML, vous verrez les balises au lieu du rendu.
Dois-je utiliser une option au lieu d’un transient ?
Pour des réponses IA, le transient est plus adapté : expiration native, sémantique de cache. Une option est utile si vous voulez un stockage “permanent” et géré manuellement.
Comment éviter de remplir la table wp_options si je n’ai pas Redis ?
Réduisez le nombre de variations de clés (prompt stable, paramètres stables), augmentez le TTL, et invalidez via bump plutôt que de générer des clés multiples. Si vous avez énormément de pages, envisagez un object cache persistant.
Le lock transient est-il fiable à 100% ?
Non. C’est un garde-fou pragmatique. Pour une atomicité stricte, il faut un stockage avec opérations atomiques (Redis) ou une table dédiée. Mais dans la majorité des sites WordPress, ce lock réduit déjà fortement les doublons.
Je veux générer les résumés à la publication, pas à l’affichage. C’est mieux ?
Souvent oui pour le front. Mais attention : l’éditeur peut subir un délai à la sauvegarde, et vous pouvez heurter des timeouts. Une approche robuste est de planifier une tâche WP-Cron qui génère après publication : WP-Cron.
Est-ce compatible avec Divi 5, Elementor et Avada ?
Oui : le shortcode fonctionne partout. Le point d’attention, c’est le builder qui peut rendre plusieurs fois en preview. Justement, le cache évite de payer plusieurs appels.
Comment ajouter un attribut post_id au shortcode ?
Ajoutez shortcode_atts() et autorisez post_id. Faites très attention à ne pas permettre à n’importe qui de forcer des résumés d’IDs non publics si votre site a des contenus privés.
Que faire si l’API renvoie 429 (rate limit) ?
Ajoutez un rate limiting côté WordPress, augmentez le TTL, mettez un lock, et servez un cache périmé si possible. Sur les gros sites, je mets aussi un backoff (attente) et je pré-génère en batch.