Si vous avez déjà reçu un email du type “Je n’ai pas trouvé l’info sur votre site, vous pouvez m’aider ?”, vous avez déjà le bon cas d’usage pour un mini-chat IA. Le but ici n’est pas de “remplacer” votre support, mais de donner une première réponse utile, basée sur le contenu de votre site, sans installer de plugin.


Le besoin / Le cas d’usage

Un chatbot IA “simple” sur WordPress sert surtout à absorber les questions répétitives : horaires, politique de remboursement, “où trouver tel article”, résumés d’un guide long, ou aide à la navigation (“je cherche un tuto sur les permaliens”). Sur un blog, j’ai souvent vu que 80% des demandes entrantes se résument à 10 questions.

Ce que l’IA apporte ici : une interface de chat minimaliste + une route AJAX WordPress sécurisée qui envoie la question à une API IA (ex. OpenAI) et renvoie une réponse exploitable. On reste volontairement sobre : pas de widget tiers, pas de SDK, pas de dépendances Composer.

À la fin, vous saurez :

  • Afficher un petit chat (HTML/CSS/JS) via un shortcode WordPress.
  • Envoyer la question à votre serveur WordPress (AJAX) avec un nonce (jeton anti-CSRF).
  • Appeler une API IA avec wp_remote_post(), gérer les erreurs/timeout, et mettre en cache les réponses avec les Transients.
  • Protéger votre clé API (stockée dans wp-config.php) et éviter de l’exposer côté navigateur.

Résumé rapide

  • Vous ajoutez une constante API dans wp-config.php (jamais dans le thème).
  • Vous créez un mu-plugin (plugin “must-use”) qui fournit un shortcode [ai_chatbot].
  • Le front (JS) appelle admin-ajax.php avec un nonce, pas l’API IA directement.
  • Le serveur appelle l’API via wp_remote_post(), timeout court, gestion d’erreurs propre.
  • Cache des réponses (transients) pour réduire les coûts.
  • Réponse assainie avant affichage (wp_kses()) pour limiter les risques XSS.

Quand utiliser l’IA pour ça

Utilisez ce type de chatbot si :

  • Vous recevez des questions répétitives et vous voulez une “première réponse” immédiate.
  • Votre contenu est déjà riche (FAQ, pages “À propos”, guides) et l’IA peut orienter l’utilisateur.
  • Vous acceptez qu’une réponse soit parfois imparfaite, et vous ajoutez un message clair (“réponse automatique”).
  • Vous voulez un chatbot léger, maîtrisé, sans dépendre d’un plugin opaque qui injecte des scripts externes partout.

Je le recommande souvent pour des blogs tutos, des sites vitrines, ou des niches où les questions sont très stables (ex. “comment réserver”, “délais”, “tarifs”).

Quand ne PAS utiliser l’IA

Évitez l’IA si une solution WordPress classique fait mieux, plus simple, et moins cher :

  • Recherche interne : commencez par améliorer la recherche (extraits, catégories, page “Plan du site”). Une IA peut coûter cher juste pour dire “utilisez la recherche”.
  • Support sensible (santé, juridique, finance) : le risque d’hallucination est réel. Dans ces cas, un formulaire de contact + base de connaissances est plus sûr.
  • Questions transactionnelles (commandes, comptes) : si la réponse dépend de données privées, ne bricolez pas un chat IA “simple”. Il faut une vraie auth, des autorisations, et un modèle de menaces.
  • Budget serré : si vous avez peu de trafic, ça va. Si vous avez 50 000 visites/jour, le coût peut exploser si vous n’avez pas de cache/rate limit.

Prérequis

Versions ciblées : WordPress 6.9.4 (avril 2026) et PHP 8.1+.

Une clé API IA : je prends OpenAI pour l’exemple car l’API est stable et bien documentée, mais l’architecture sera la même avec Anthropic/Mistral/Google.

Comprendre rapidement : API et appel HTTP

Une API est une interface qui vous permet de demander quelque chose à un service (ici : “réponds à cette question”). Techniquement, vous envoyez une requête HTTP (souvent en POST) avec du JSON, et vous recevez du JSON en retour.

Dans WordPress, on fait ça proprement via l’API HTTP : wp_remote_post() (et wp_remote_get()). C’est la méthode recommandée car elle gère les transports (cURL, streams), les proxies, et s’intègre avec WordPress.

Source officielle : WordPress HTTP API.

Où stocker la clé API (obligatoire)

Vous allez stocker la clé dans wp-config.php, pas dans un fichier de thème, et surtout pas dans du JavaScript. Si votre clé se retrouve côté navigateur, elle sera copiée en 30 secondes et utilisée à vos frais.

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

define('BPCAB_OPENAI_API_KEY', 'VOTRE_CLE_ICI');

Coûts : chaque message envoyé à l’API coûte de l’argent (au token). Même si c’est “quelques centimes”, ça peut grimper. On mettra donc du cache + un rate limit côté serveur.

Où coller le code

  • Option recommandée : un mu-plugin (chargé automatiquement). Chemin : wp-content/mu-plugins/.
  • Alternative : un plugin custom classique dans wp-content/plugins/.
  • À éviter : functions.php du thème (surtout si vous changez de thème, ou si un builder met à jour des fichiers).

Doc officielle mu-plugins : Must-Use Plugins.

Architecture de la solution

Voici le flux, en clair :

  • Le visiteur ouvre une page contenant [ai_chatbot].
  • Le shortcode affiche l’UI (une petite fenêtre) + charge un JS.
  • Quand l’utilisateur envoie un message, le JS fait un POST vers admin-ajax.php avec :
    • le message
    • un nonce WordPress (anti-requête forgée)
  • WordPress reçoit la requête via un hook AJAX, valide le nonce, applique un rate limit, puis appelle l’API IA via wp_remote_post().
  • WordPress assainit la réponse, la met en cache (transient), puis renvoie du JSON au navigateur.
  • Le navigateur affiche la réponse.

Pourquoi passer par AJAX WordPress (et pas appeler l’API IA en JS)

Parce que votre clé doit rester côté serveur. Si vous appelez OpenAI directement depuis le navigateur, vous devrez exposer la clé… et vous perdez le contrôle (coût, abus, scraping).

Le code complet — étape par étape

On va construire un mu-plugin qui :

  • ajoute un shortcode [ai_chatbot]
  • enqueue CSS/JS proprement
  • crée une action AJAX pour les utilisateurs connectés et non connectés
  • appelle l’API OpenAI
  • met en cache les réponses

Étape 1 — Créer le mu-plugin

Créez le dossier wp-content/mu-plugins/ s’il n’existe pas, puis créez le fichier :

wp-content/mu-plugins/bpcab-ai-chatbot.php

<?php
/**
 * Plugin Name: BPCAB — Chatbot IA simple (sans plugin)
 * Description: Ajoute un chatbot IA minimal via shortcode, avec appel serveur (wp_remote_post), cache transient, et sécurité de base.
 * Author: Vous
 * Version: 1.0.0
 */

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

define('BPCAB_CHATBOT_VERSION', '1.0.0');

/**
 * Petit helper : récupère l'IP (approx) pour rate limiting.
 * Attention : derrière un proxy/CDN, l'IP peut être celle du proxy si mal configuré.
 */
function bpcab_get_client_ip(): string {
	$keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
	foreach ($keys as $key) {
		if (!empty($_SERVER[$key])) {
			$ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
			// X-Forwarded-For peut contenir plusieurs IP : on prend la première.
			if (str_contains($ip, ',')) {
				$parts = explode(',', $ip);
				$ip = trim($parts[0]);
			}
			return $ip;
		}
	}
	return '0.0.0.0';
}

Étape 2 — Shortcode + chargement CSS/JS

Un shortcode est une balise entre crochets (ex. [ai_chatbot]) que WordPress remplace par du HTML. C’est pratique avec Divi 5, Elementor et Avada : tous savent insérer un shortcode (module “Code”, widget “Shortcode”, etc.).

/**
 * Enqueue des assets uniquement si le shortcode est présent sur la page.
 */
function bpcab_maybe_enqueue_assets(): void {
	if (!is_singular()) {
		return;
	}

	global $post;
	if (!$post instanceof WP_Post) {
		return;
	}

	if (!has_shortcode($post->post_content, 'ai_chatbot')) {
		return;
	}

	$handle = 'bpcab-ai-chatbot';

	wp_register_style(
		$handle,
		plugins_url('bpcab-ai-chatbot.css', __FILE__),
		array(),
		BPCAB_CHATBOT_VERSION
	);

	wp_register_script(
		$handle,
		plugins_url('bpcab-ai-chatbot.js', __FILE__),
		array(),
		BPCAB_CHATBOT_VERSION,
		true
	);

	wp_enqueue_style($handle);
	wp_enqueue_script($handle);

	// Données côté JS (sans clé API !)
	wp_localize_script($handle, 'BPCAB_CHATBOT', array(
		'ajaxUrl' => admin_url('admin-ajax.php'),
		'nonce'   => wp_create_nonce('bpcab_chatbot_nonce'),
		'maxLen'  => 400,
	));
}
add_action('wp_enqueue_scripts', 'bpcab_maybe_enqueue_assets');

/**
 * Shortcode [ai_chatbot]
 */
function bpcab_ai_chatbot_shortcode(): string {
	// HTML volontairement simple, facile à styliser.
	ob_start();
	?>
	<div class="bpcab-chatbot" data-bpcab-chatbot="1">
		<div class="bpcab-chatbot__header">
			<strong>Assistant</strong>
			<span class="bpcab-chatbot__status" aria-live="polite">Prêt</span>
		</div>

		<div class="bpcab-chatbot__messages" role="log" aria-live="polite"></div>

		<form class="bpcab-chatbot__form">
			<input class="bpcab-chatbot__input" type="text" name="message" placeholder="Posez votre question…" autocomplete="off" />
			<button class="bpcab-chatbot__send" type="submit">Envoyer</button>
		</form>

		<p class="bpcab-chatbot__hint">
			<em>Réponse automatique. Ne partagez pas d’informations sensibles.</em>
		</p>
	</div>
	<?php
	return (string) ob_get_clean();
}
add_shortcode('ai_chatbot', 'bpcab_ai_chatbot_shortcode');

Piège fréquent : coller ce code dans le mauvais fichier (un “snippet” de builder, ou un plugin de snippets qui s’exécute trop tard). Avec un mu-plugin, vous évitez beaucoup de surprises.

Étape 3 — Créer les fichiers CSS/JS

Dans le même dossier mu-plugins, créez :

  • bpcab-ai-chatbot.css
  • bpcab-ai-chatbot.js

CSS minimal (vous pourrez l’adapter au thème/builder) :

.bpcab-chatbot{
	max-width: 520px;
	border: 1px solid rgba(0,0,0,.12);
	border-radius: 12px;
	overflow: hidden;
	background: #fff;
}

.bpcab-chatbot__header{
	display:flex;
	justify-content: space-between;
	align-items:center;
	padding: 12px 14px;
	background: #111827;
	color: #fff;
}

.bpcab-chatbot__messages{
	padding: 12px 14px;
	min-height: 220px;
	max-height: 360px;
	overflow:auto;
	background: #f9fafb;
}

.bpcab-chatbot__msg{
	margin: 0 0 10px 0;
	padding: 10px 12px;
	border-radius: 10px;
	line-height: 1.35;
}

.bpcab-chatbot__msg--user{
	background: #dbeafe;
}

.bpcab-chatbot__msg--bot{
	background: #fff;
	border: 1px solid rgba(0,0,0,.08);
}

.bpcab-chatbot__form{
	display:flex;
	gap: 8px;
	padding: 12px 14px;
	border-top: 1px solid rgba(0,0,0,.08);
	background: #fff;
}

.bpcab-chatbot__input{
	flex:1;
	padding: 10px 12px;
	border: 1px solid rgba(0,0,0,.18);
	border-radius: 10px;
}

.bpcab-chatbot__send{
	padding: 10px 14px;
	border-radius: 10px;
	border: 0;
	background: #111827;
	color: #fff;
	cursor: pointer;
}

.bpcab-chatbot__hint{
	padding: 0 14px 14px 14px;
	margin: 0;
	color: #6b7280;
	font-size: 13px;
}

JS minimal : envoi AJAX vers WordPress, affichage des messages, gestion de l’état.

(function () {
	function qs(root, sel) { return root.querySelector(sel); }

	function addMsg(messagesEl, text, who) {
		var p = document.createElement('p');
		p.className = 'bpcab-chatbot__msg bpcab-chatbot__msg--' + who;
		p.textContent = text;
		messagesEl.appendChild(p);
		messagesEl.scrollTop = messagesEl.scrollHeight;
	}

	function setStatus(root, text) {
		var s = qs(root, '.bpcab-chatbot__status');
		if (s) s.textContent = text;
	}

	function initChatbot(root) {
		var form = qs(root, '.bpcab-chatbot__form');
		var input = qs(root, '.bpcab-chatbot__input');
		var messages = qs(root, '.bpcab-chatbot__messages');

		if (!form || !input || !messages) return;

		addMsg(messages, "Bonjour ! Posez votre question, je vais essayer de vous orienter.", "bot");

		form.addEventListener('submit', function (e) {
			e.preventDefault();

			var msg = (input.value || '').trim();
			if (!msg) return;

			if (msg.length > (window.BPCAB_CHATBOT?.maxLen || 400)) {
				addMsg(messages, "Message trop long. Essayez de faire plus court.", "bot");
				return;
			}

			addMsg(messages, msg, "user");
			input.value = '';
			setStatus(root, 'Réflexion…');

			var data = new FormData();
			data.append('action', 'bpcab_chatbot_ask');
			data.append('nonce', window.BPCAB_CHATBOT?.nonce || '');
			data.append('message', msg);

			fetch(window.BPCAB_CHATBOT?.ajaxUrl || '', {
				method: 'POST',
				credentials: 'same-origin',
				body: data
			})
			.then(function (r) { return r.json(); })
			.then(function (payload) {
				if (!payload || !payload.success) {
					var err = payload?.data?.message || "Erreur côté serveur.";
					addMsg(messages, err, "bot");
					setStatus(root, 'Erreur');
					return;
				}
				addMsg(messages, payload.data.answer, "bot");
				setStatus(root, 'Prêt');
			})
			.catch(function () {
				addMsg(messages, "Impossible de contacter le serveur. Réessayez.", "bot");
				setStatus(root, 'Hors ligne');
			});
		});
	}

	document.addEventListener('DOMContentLoaded', function () {
		document.querySelectorAll('[data-bpcab-chatbot="1"]').forEach(initChatbot);
	});
})();

Piège fréquent : “le JS ne se charge pas”. Dans 90% des cas, c’est un mauvais chemin plugins_url(), ou un cache (plugin/CDN) qui sert une vieille version. Changez la version BPCAB_CHATBOT_VERSION pour forcer le rechargement, ou videz le cache.

Étape 4 — Endpoint AJAX + nonce + rate limit

Un hook WordPress est un point d’accroche. Il en existe deux types :

  • action : “faites quelque chose ici” (ex. wp_enqueue_scripts).
  • filtre : “modifiez cette valeur” (ex. the_content).

Ici, on utilise des actions AJAX : wp_ajax_* (connecté) et wp_ajax_nopriv_* (non connecté).

/**
 * Rate limit très simple : X requêtes par minute par IP.
 * Ce n'est pas une protection parfaite, mais ça évite les abus basiques.
 */
function bpcab_rate_limit_or_die(string $ip, int $limit = 10, int $window_seconds = 60): void {
	$key = 'bpcab_rl_' . md5($ip);
	$hits = (int) get_transient($key);

	if ($hits >= $limit) {
		wp_send_json_error(array(
			'message' => 'Trop de requêtes. Attendez une minute et réessayez.'
		), 429);
	}

	set_transient($key, $hits + 1, $window_seconds);
}

/**
 * Handler AJAX : reçoit la question et renvoie une réponse IA.
 */
function bpcab_ajax_chatbot_ask(): void {
	// Vérification nonce (anti-CSRF)
	$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
	if (!wp_verify_nonce($nonce, 'bpcab_chatbot_nonce')) {
		wp_send_json_error(array('message' => 'Requête refusée (nonce invalide).'), 403);
	}

	$ip = bpcab_get_client_ip();
	bpcab_rate_limit_or_die($ip, 10, 60);

	$message = isset($_POST['message']) ? (string) wp_unslash($_POST['message']) : '';
	$message = trim($message);

	if ($message === '') {
		wp_send_json_error(array('message' => 'Message vide.'), 400);
	}

	if (mb_strlen($message) > 400) {
		wp_send_json_error(array('message' => 'Message trop long (max 400 caractères).'), 400);
	}

	$answer = bpcab_get_ai_answer($message);

	if (is_wp_error($answer)) {
		wp_send_json_error(array(
			'message' => $answer->get_error_message()
		), 500);
	}

	wp_send_json_success(array(
		'answer' => $answer,
	));
}
add_action('wp_ajax_bpcab_chatbot_ask', 'bpcab_ajax_chatbot_ask');
add_action('wp_ajax_nopriv_bpcab_chatbot_ask', 'bpcab_ajax_chatbot_ask');

Étape 5 — Appel OpenAI via wp_remote_post() + cache transient

On va :

  • Créer une clé de cache basée sur la question (hash).
  • Mettre un TTL (ex. 24h) pour éviter de repayer la même réponse.
  • Ajouter un timeout court (ex. 20s) pour éviter que PHP “reste accroché”.
  • Assainir la réponse (on autorise un sous-ensemble HTML très limité).
/**
 * Appelle l'API IA (OpenAI) et renvoie une réponse texte.
 * IMPORTANT : la clé API doit être définie dans wp-config.php via BPCAB_OPENAI_API_KEY
 */
function bpcab_get_ai_answer(string $user_message) {
	if (!defined('BPCAB_OPENAI_API_KEY') || BPCAB_OPENAI_API_KEY === '') {
		return new WP_Error('bpcab_missing_key', 'Clé API manquante. Ajoutez BPCAB_OPENAI_API_KEY dans wp-config.php.');
	}

	// Cache : même question => même réponse pendant 24h
	$cache_key = 'bpcab_ai_' . md5(mb_strtolower(trim($user_message)));
	$cached = get_transient($cache_key);
	if (is_string($cached) && $cached !== '') {
		return $cached;
	}

	/**
	 * Prompt système : court, orienté "site".
	 * Ici, on ne fait PAS encore de RAG (recherche dans vos contenus).
	 * On limite la responsabilité : pas de conseils médicaux/juridiques.
	 */
	$system = "Vous êtes un assistant pour un site WordPress. Répondez en français, de façon courte et pratique. "
		. "Si la question demande des infos sensibles (santé/juridique/finance), refusez et conseillez de contacter le propriétaire du site. "
		. "Si vous ne savez pas, dites-le et proposez une piste (ex: consulter la page FAQ).";

	/**
	 * API OpenAI (Chat Completions style Responses API selon évolution).
	 * En avril 2026, l'API a évolué plusieurs fois. Le principe reste :
	 * - endpoint HTTPS
	 * - JSON en entrée
	 * - JSON en sortie
	 *
	 * Si votre compte OpenAI recommande un endpoint différent, adaptez l'URL et le parsing.
	 * Référez-vous à la doc officielle.
	 */
	$endpoint = 'https://api.openai.com/v1/chat/completions';

	$body = array(
		'model' => 'gpt-4.1-mini',
		'temperature' => 0.2,
		'messages' => array(
			array('role' => 'system', 'content' => $system),
			array('role' => 'user', 'content' => $user_message),
		),
	);

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

	$response = wp_remote_post($endpoint, $args);

	if (is_wp_error($response)) {
		return new WP_Error('bpcab_http_error', 'Erreur HTTP : ' . $response->get_error_message());
	}

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

	if ($code < 200 || $code >= 300) {
		// On évite d'afficher tout le raw (peut contenir des détails techniques).
		return new WP_Error('bpcab_api_error', 'API IA indisponible (code ' . $code . '). Vérifiez votre clé/quota.');
	}

	$data = json_decode($raw, true);
	if (!is_array($data)) {
		return new WP_Error('bpcab_json_error', 'Réponse API illisible (JSON invalide).');
	}

	// Parsing "chat.completions"
	$text = $data['choices'][0]['message']['content'] ?? '';
	$text = is_string($text) ? trim($text) : '';

	if ($text === '') {
		return new WP_Error('bpcab_empty_answer', 'Réponse vide de l’API IA.');
	}

	/**
	 * Assainissement :
	 * - on autorise un peu de HTML basique (liens, strong, em, br)
	 * - on supprime le reste
	 */
	$allowed = array(
		'a' => array(
			'href' => true,
			'target' => true,
			'rel' => true,
		),
		'strong' => array(),
		'em' => array(),
		'br' => array(),
	);

	$safe = wp_kses($text, $allowed);

	// Cache 24h
	set_transient($cache_key, $safe, DAY_IN_SECONDS);

	return $safe;
}

Deux erreurs que je vois souvent :

  • Oublier un point-virgule dans wp-config.php ou dans le mu-plugin : résultat, écran blanc (fatal error). Activez WP_DEBUG en staging, pas en production.
  • Copier un vieux tutoriel qui utilise une ancienne structure d’API ou un modèle obsolète. Ici, vous avez une base WordPress 6.9.4 + PHP 8.1, et vous adaptez uniquement l’endpoint si OpenAI change sa recommandation.

Le code assemblé complet

Ci-dessous, le fichier complet mu-plugin. Vous devrez aussi créer les fichiers CSS/JS (montrés plus haut). Ne mettez pas la clé dans ce fichier.

<?php
/**
 * Plugin Name: BPCAB — Chatbot IA simple (sans plugin)
 * Description: Chatbot IA minimal via shortcode, AJAX WordPress, appel OpenAI avec wp_remote_post(), cache transient, sécurité de base.
 * Author: Vous
 * Version: 1.0.0
 */

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

define('BPCAB_CHATBOT_VERSION', '1.0.0');

function bpcab_get_client_ip(): string {
	$keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
	foreach ($keys as $key) {
		if (!empty($_SERVER[$key])) {
			$ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
			if (str_contains($ip, ',')) {
				$parts = explode(',', $ip);
				$ip = trim($parts[0]);
			}
			return $ip;
		}
	}
	return '0.0.0.0';
}

function bpcab_maybe_enqueue_assets(): void {
	if (!is_singular()) {
		return;
	}

	global $post;
	if (!$post instanceof WP_Post) {
		return;
	}

	if (!has_shortcode($post->post_content, 'ai_chatbot')) {
		return;
	}

	$handle = 'bpcab-ai-chatbot';

	wp_register_style(
		$handle,
		plugins_url('bpcab-ai-chatbot.css', __FILE__),
		array(),
		BPCAB_CHATBOT_VERSION
	);

	wp_register_script(
		$handle,
		plugins_url('bpcab-ai-chatbot.js', __FILE__),
		array(),
		BPCAB_CHATBOT_VERSION,
		true
	);

	wp_enqueue_style($handle);
	wp_enqueue_script($handle);

	wp_localize_script($handle, 'BPCAB_CHATBOT', array(
		'ajaxUrl' => admin_url('admin-ajax.php'),
		'nonce'   => wp_create_nonce('bpcab_chatbot_nonce'),
		'maxLen'  => 400,
	));
}
add_action('wp_enqueue_scripts', 'bpcab_maybe_enqueue_assets');

function bpcab_ai_chatbot_shortcode(): string {
	ob_start();
	?>
	<div class="bpcab-chatbot" data-bpcab-chatbot="1">
		<div class="bpcab-chatbot__header">
			<strong>Assistant</strong>
			<span class="bpcab-chatbot__status" aria-live="polite">Prêt</span>
		</div>

		<div class="bpcab-chatbot__messages" role="log" aria-live="polite"></div>

		<form class="bpcab-chatbot__form">
			<input class="bpcab-chatbot__input" type="text" name="message" placeholder="Posez votre question…" autocomplete="off" />
			<button class="bpcab-chatbot__send" type="submit">Envoyer</button>
		</form>

		<p class="bpcab-chatbot__hint">
			<em>Réponse automatique. Ne partagez pas d’informations sensibles.</em>
		</p>
	</div>
	<?php
	return (string) ob_get_clean();
}
add_shortcode('ai_chatbot', 'bpcab_ai_chatbot_shortcode');

function bpcab_rate_limit_or_die(string $ip, int $limit = 10, int $window_seconds = 60): void {
	$key = 'bpcab_rl_' . md5($ip);
	$hits = (int) get_transient($key);

	if ($hits >= $limit) {
		wp_send_json_error(array(
			'message' => 'Trop de requêtes. Attendez une minute et réessayez.'
		), 429);
	}

	set_transient($key, $hits + 1, $window_seconds);
}

function bpcab_get_ai_answer(string $user_message) {
	if (!defined('BPCAB_OPENAI_API_KEY') || BPCAB_OPENAI_API_KEY === '') {
		return new WP_Error('bpcab_missing_key', 'Clé API manquante. Ajoutez BPCAB_OPENAI_API_KEY dans wp-config.php.');
	}

	$cache_key = 'bpcab_ai_' . md5(mb_strtolower(trim($user_message)));
	$cached = get_transient($cache_key);
	if (is_string($cached) && $cached !== '') {
		return $cached;
	}

	$system = "Vous êtes un assistant pour un site WordPress. Répondez en français, de façon courte et pratique. "
		. "Si la question demande des infos sensibles (santé/juridique/finance), refusez et conseillez de contacter le propriétaire du site. "
		. "Si vous ne savez pas, dites-le et proposez une piste (ex: consulter la page FAQ).";

	$endpoint = 'https://api.openai.com/v1/chat/completions';

	$body = array(
		'model' => 'gpt-4.1-mini',
		'temperature' => 0.2,
		'messages' => array(
			array('role' => 'system', 'content' => $system),
			array('role' => 'user', 'content' => $user_message),
		),
	);

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

	$response = wp_remote_post($endpoint, $args);

	if (is_wp_error($response)) {
		return new WP_Error('bpcab_http_error', 'Erreur HTTP : ' . $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 new WP_Error('bpcab_api_error', 'API IA indisponible (code ' . $code . '). Vérifiez votre clé/quota.');
	}

	$data = json_decode($raw, true);
	if (!is_array($data)) {
		return new WP_Error('bpcab_json_error', 'Réponse API illisible (JSON invalide).');
	}

	$text = $data['choices'][0]['message']['content'] ?? '';
	$text = is_string($text) ? trim($text) : '';

	if ($text === '') {
		return new WP_Error('bpcab_empty_answer', 'Réponse vide de l’API IA.');
	}

	$allowed = array(
		'a' => array('href' => true, 'target' => true, 'rel' => true),
		'strong' => array(),
		'em' => array(),
		'br' => array(),
	);

	$safe = wp_kses($text, $allowed);

	set_transient($cache_key, $safe, DAY_IN_SECONDS);

	return $safe;
}

function bpcab_ajax_chatbot_ask(): void {
	$nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
	if (!wp_verify_nonce($nonce, 'bpcab_chatbot_nonce')) {
		wp_send_json_error(array('message' => 'Requête refusée (nonce invalide).'), 403);
	}

	$ip = bpcab_get_client_ip();
	bpcab_rate_limit_or_die($ip, 10, 60);

	$message = isset($_POST['message']) ? (string) wp_unslash($_POST['message']) : '';
	$message = trim($message);

	if ($message === '') {
		wp_send_json_error(array('message' => 'Message vide.'), 400);
	}

	if (mb_strlen($message) > 400) {
		wp_send_json_error(array('message' => 'Message trop long (max 400 caractères).'), 400);
	}

	$answer = bpcab_get_ai_answer($message);

	if (is_wp_error($answer)) {
		wp_send_json_error(array(
			'message' => $answer->get_error_message()
		), 500);
	}

	wp_send_json_success(array('answer' => $answer));
}
add_action('wp_ajax_bpcab_chatbot_ask', 'bpcab_ajax_chatbot_ask');
add_action('wp_ajax_nopriv_bpcab_chatbot_ask', 'bpcab_ajax_chatbot_ask');

Explication du code

Pourquoi un mu-plugin

Un mu-plugin se charge automatiquement, sans activation, et il est moins “fragile” qu’un snippet collé dans un thème. Dans mon expérience, quand un débutant met le code dans functions.php et change ensuite de thème (ou met à jour un thème parent), le chatbot disparaît.

Pourquoi wp_localize_script()

On s’en sert ici pour passer au JS :

  • l’URL AJAX (admin-ajax.php)
  • un nonce
  • une limite de longueur

Ça évite de “hardcoder” l’URL et ça marche même si WordPress est installé dans un sous-dossier.

Doc : wp_localize_script().

Nonce : ce que ça protège réellement

Un nonce WordPress empêche un autre site de déclencher des requêtes à votre place via le navigateur de votre visiteur (attaque CSRF). Ça ne remplace pas un système d’authentification, mais pour un chatbot public, c’est un minimum.

Doc : Nonces WordPress.

Cache Transients : pourquoi c’est vital

Les Transients sont un cache clé/valeur avec expiration. Si un visiteur demande “Quels sont vos horaires ?” 200 fois dans le mois, vous ne voulez pas payer 200 appels IA. Vous payez 1 appel, puis vous servez la réponse du cache.

Doc : Transients API.

Assainissement de la réponse

Une API IA peut renvoyer du texte inattendu (voire du HTML). Si vous injectez ça tel quel dans le DOM, vous ouvrez la porte à des problèmes XSS. Ici :

  • on autorise seulement a, strong, em, br
  • tout le reste est supprimé

Fonction utilisée : wp_kses(). Doc : wp_kses().

Coûts API et optimisation

Les prix exacts dépendent du modèle et de votre contrat. Ce qui compte pour estimer :

  • le nombre de messages par mois
  • la longueur moyenne des questions et des réponses (tokens)
  • le taux de cache hit (combien de questions identiques reviennent)

Estimation “terrain” (ordre de grandeur)

Sur un chatbot simple “orientation”, j’observe souvent :

  • Question : 20 à 60 tokens
  • Réponse : 60 à 200 tokens
  • Total : 100 à 300 tokens par interaction

Si vous avez 2 000 interactions/mois, ça fait 200 000 à 600 000 tokens/mois. Selon le modèle, ça peut rester raisonnable… ou non. Le cache et un modèle “mini” font une énorme différence.

Optimisations immédiates

  • Cache 24h (déjà en place). Pour une FAQ stable, vous pouvez monter à 7 jours.
  • Réponses plus courtes : ajoutez dans le prompt “répondez en 5 phrases max”.
  • Température basse : moins de variations, plus de cache hit.
  • Limiter la longueur des messages côté JS + côté PHP (déjà en place). Toujours double-barrière.

Variantes et cas d’usage avancés

Variante 1 — Ajouter un “contexte site” (sans RAG)

Sans faire de recherche dans vos articles, vous pouvez fournir un mini-contexte statique (ex. vos horaires, votre URL FAQ). Pratique pour un site vitrine.

// Exemple : ajoutez ceci avant $body dans bpcab_get_ai_answer()
$site_context = "Contexte du site :n"
	. "- FAQ : https://example.com/faq/n"
	. "- Contact : https://example.com/contact/n"
	. "- Horaires : lun-ven 9h-18hn";

$body['messages'] = array(
	array('role' => 'system', 'content' => $system . "nn" . $site_context),
	array('role' => 'user', 'content' => $user_message),
);

Variante 2 — Mode “résumé d’article” via un attribut de shortcode

Vous pouvez autoriser [ai_chatbot mode="resume"] et envoyer à l’IA l’extrait de la page courante. Attention : plus de tokens = plus de coût.

// Exemple (incomplet) : détecter un attribut et injecter le contenu
function bpcab_ai_chatbot_shortcode($atts = array()): string {
	$atts = shortcode_atts(array('mode' => 'chat'), $atts, 'ai_chatbot');

	// ... même HTML qu'avant, mais vous pourriez ajouter data-mode
	// <div data-mode="chat"> etc.
	// Ce snippet est volontairement partiel : il faut adapter le JS pour envoyer le mode.
	return '...';
}

Je le signale clairement : ce snippet n’est pas complet. Pour que ça marche, il faut aussi modifier le JS (envoyer mode) et le handler AJAX (changer le prompt selon le mode).

Variante 3 — Intégration Divi 5 / Elementor / Avada

  • Divi 5 : ajoutez un module “Code” ou “Shortcode” et collez [ai_chatbot]. Si Divi minifie/concatène, videz son cache statique si le CSS/JS ne se met pas à jour.
  • Elementor : widget “Shortcode” → [ai_chatbot]. Si vous utilisez l’optimisation d’assets d’Elementor, vérifiez que le script n’est pas différé de façon agressive (sinon DOMContentLoaded peut arriver avant l’injection : dans ce cas, initialisez aussi sur elementor/frontend/init).
  • Avada : élément “Shortcode” dans Fusion Builder. Si vous avez une mise en cache Avada, purge après ajout des fichiers CSS/JS.

Sécurité et bonnes pratiques

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

Règle simple : la clé reste dans wp-config.php. Le navigateur ne doit jamais la voir. Même “cachée” dans un bundle JS, elle est récupérable.

Valider et limiter les entrées

  • Longueur max côté JS (UX) + côté PHP (sécurité).
  • Refuser les messages vides.
  • Rate limit par IP (déjà en place). Pour aller plus loin, vous pouvez aussi limiter par session/cookie.

Assainir les sorties

Affichez la réponse en texte (comme dans le JS avec textContent) ou assainissez fortement si vous autorisez du HTML. Ici, on fait les deux : on assainit côté serveur et on affiche en texte côté client. C’est volontairement “parano”.

RGPD : données envoyées à un tiers

Si un visiteur tape des données personnelles, vous les envoyez potentiellement à un prestataire (OpenAI). Prévoyez :

  • un message clair (“Ne partagez pas d’informations sensibles.”)
  • une mention dans votre politique de confidentialité
  • un paramétrage du fournisseur (rétention, opt-out, etc.) selon votre contrat

Ne testez pas en production sans sauvegarde

Un mu-plugin avec une erreur PHP casse tout le site (car il est chargé automatiquement). Travaillez d’abord sur une copie/staging. Si vous devez intervenir en production, gardez un accès FTP/SSH pour supprimer le fichier en cas d’écran blanc.

Comment tester et déboguer

Activer les logs proprement

Dans un environnement de test, activez :

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

Doc : Debugging dans WordPress.

Vérifier la requête AJAX

  • Ouvrez les DevTools navigateur → onglet Réseau → filtrez sur admin-ajax.php.
  • Regardez le statut HTTP (200/403/429/500) et la réponse JSON.
  • Si vous voyez une page HTML au lieu de JSON, c’est souvent une erreur PHP fatale (ou un plugin de sécurité qui intercepte).

Tester l’API IA “à vide”

Avant d’intégrer le JS, vous pouvez appeler bpcab_get_ai_answer() dans un contexte admin (temporairement) et logger la réponse. Ne laissez pas ce code en place.

Si ça ne marche pas

Symptôme Cause probable Vérification Solution
Rien ne s’affiche sur la page Shortcode absent ou pas interprété Vérifiez que la page contient [ai_chatbot] (pas dans un bloc “Code” qui échappe les crochets) Utilisez un bloc “Shortcode” (Elementor) / module Shortcode (Divi/Avada)
Le chat s’affiche mais le bouton ne répond pas JS non chargé (cache, chemin, conflit) DevTools → Console (erreurs) + Réseau (fichier JS 404?) Vérifiez plugins_url(), purge cache, incrémentez BPCAB_CHATBOT_VERSION
Réponse “nonce invalide” Nonce expiré, cache page agressif, ou JS reçoit un vieux nonce Regardez la valeur BPCAB_CHATBOT.nonce dans la console Réduisez le cache sur pages avec chat, ou régénérez le nonce au chargement (approche plus avancée)
Erreur 429 “Trop de requêtes” Rate limit trop strict ou tests répétés Reproduisez en navigation privée / autre IP Augmentez la fenêtre/limite, ou limitez aux utilisateurs connectés
Erreur “API IA indisponible (code 401/403)” Clé invalide, clé non chargée, permissions Vérifiez BPCAB_OPENAI_API_KEY dans wp-config.php (sans espaces) Regénérez la clé, vérifiez la facturation/quota côté fournisseur
Erreur “JSON invalide” Réponse API tronquée (proxy, WAF), ou endpoint changé Logguez $raw en staging (attention données) Ajustez l’endpoint/format selon la doc officielle, augmentez le timeout si nécessaire
Écran blanc après ajout du mu-plugin Erreur PHP (parenthèse, point-virgule), PHP trop ancien Consultez wp-content/debug.log ou logs serveur Corrigez la syntaxe, vérifiez PHP 8.1+, supprimez le fichier via FTP si besoin

Problèmes réalistes et correctifs rapides

  • Code collé dans le mauvais endroit : si vous l’avez mis dans un “Custom Code” d’un builder, il peut être filtré/échappé. Passez par mu-plugin.
  • Hook inadapté : si vous enqueuez le JS sur init, vous risquez de le charger trop tôt/mal. Ici on utilise wp_enqueue_scripts.
  • Conflit de cache : certains caches mettent en cache le HTML contenant le nonce. Résultat : nonce périmé pour tout le monde. Solution : exclure la page du cache, ou mettre en place une route qui renvoie un nonce “frais” (variante avancée).
  • Permaliens : pas directement lié, mais j’ai déjà vu des sites où admin_url() est filtré par un plugin de sécurité mal configuré. Si AJAX ne répond pas, testez l’URL /wp-admin/admin-ajax.php directement dans le navigateur.

Ressources

FAQ

Est-ce vraiment “sans plugin” si j’ajoute un mu-plugin ?

Vous n’installez pas un plugin tiers depuis wordpress.org, mais techniquement vous ajoutez bien du code sous forme de plugin. C’est volontaire : c’est la façon la plus propre de garder le code indépendant du thème.

Puis-je mettre le code dans functions.php ?

Oui, mais je ne le conseille pas. Vous risquez de perdre le chatbot lors d’un changement de thème, et un builder peut compliquer le debug. Si vous le faites quand même, utilisez un thème enfant.

Pourquoi utiliser admin-ajax.php plutôt que l’API REST ?

Les deux marchent. Pour un débutant, AJAX WordPress est rapide à mettre en place. Si vous voulez une approche plus moderne, passez à une route REST avec register_rest_route() et des permissions callbacks. Le cœur (appel wp_remote_post(), cache, sécurité) reste identique.

Le nonce casse avec mon plugin de cache : je fais quoi ?

Excluez la page du cache (solution simple), ou implémentez un endpoint qui renvoie un nonce à la demande, puis mettez à jour le JS pour le récupérer. Sur des sites très cachés (CDN agressif), c’est un classique.

Comment limiter le chatbot à certaines pages ?

Le shortcode vous donne déjà ce contrôle : vous ne l’ajoutez que là où vous en avez besoin. Et le code n’enqueue le JS/CSS que si le shortcode est présent.

Comment empêcher l’IA de raconter n’importe quoi ?

Vous ne pouvez pas le garantir à 100%. Réduisez le risque :

  • réponses courtes
  • température basse
  • message clair “si vous ne savez pas, dites-le”
  • rediriger vers votre FAQ/contact

Puis-je afficher des liens cliquables dans la réponse ?

Oui, mais faites-le prudemment. Dans ce tutoriel, on assainit avec wp_kses() en autorisant a. Côté JS, on affiche avec textContent (donc pas de HTML). Si vous voulez des liens cliquables, il faut afficher en HTML (innerHTML) et redoubler de prudence (assainissement strict, ajout automatique de rel="noopener nofollow").

Est-ce compatible Divi 5 / Elementor / Avada ?

Oui : utilisez un module/widget “Shortcode” et collez [ai_chatbot]. Le code est indépendant du thème. Les seuls soucis viennent généralement du cache/minification : purge après ajout des fichiers.

Pourquoi vous limitez à 400 caractères ?

Pour éviter les prompts énormes, réduire les coûts, et limiter l’abus. Vous pouvez augmenter, mais faites-le en connaissance de cause (tokens, latence, facture).

Comment changer le modèle IA ?

Modifiez la valeur model dans $body. Gardez un modèle “mini” pour un chatbot d’orientation. Si vous passez à plus gros, surveillez les coûts et la latence.

Je reçois “Erreur HTTP : cURL error 28”

C’est un timeout. Vérifiez :

  • sortie réseau autorisée depuis l’hébergement (firewall)
  • DNS
  • augmentez timeout (ex. 30) si votre serveur est lent

Si vous êtes sur un hébergement mutualisé verrouillé, c’est fréquent.