Si vous avez déjà ajouté “juste un petit bouton” sur une page WordPress, puis fini avec 200 Ko de JavaScript, une dépendance à un framework et un cache qui ne se vide jamais, vous avez rencontré le vrai problème : l’interactivité côté client se paye vite en complexité.
Depuis plusieurs versions, WordPress propose une voie plus “core-friendly” : l’Interactivity API. L’idée est simple sur le papier : déclarer des comportements côté client directement dans votre markup (via data-wp-*), avec un store réactif géré par WordPress, sans React/Vue/Svelte dans votre thème.
Le problème / Le besoin
Besoin concret : rendre un composant front (ex. “like”, “compteur”, “filtre”, “accordéon”, “copier dans le presse-papiers”, “chargement progressif”) interactif sans embarquer un framework, tout en restant compatible avec l’écosystème WordPress (cache, thèmes, block themes, page builders, sécurité).
À la fin, vous saurez :
- Créer un petit plugin qui expose un bloc (ou un rendu HTML utilisable partout) avec Interactivity API.
- Gérer un état côté client (store) et des actions (handlers) sans dépendances externes.
- Ajouter un endpoint AJAX/REST sécurisé pour persister l’état (ex. compteur de likes).
- Déboguer les cas réels : mauvais enqueue, cache agressif, hooks mal choisis, conflits builder.
Résumé rapide
- On code un plugin “Like Button” (compteur + toggle) basé sur Interactivity API, compatible WordPress 6.9.4+ et PHP 8.1+.
- Le markup utilise
data-wp-interactive,data-wp-on--click,data-wp-text,data-wp-class--*. - Le store JS gère l’état local (optimistic UI), puis synchronise via REST API.
- Côté PHP : endpoint REST
/wp-json/bpcab/v1/like, nonce REST, permissions, sanitization/escaping. - On fournit une variante shortcode + intégrations Divi 5 / Elementor / Avada.
Quand utiliser cette solution
- Vous voulez une interactivité légère (toggle, compteur, UI state) sans bundle React.
- Vous livrez un thème ou plugin destiné à des sites variés (builders, cache, CDN) et vous voulez réduire les risques de conflits JS.
- Vous avez besoin d’un rendu SSR (HTML généré par PHP) qui reste utilisable sans JS, puis “s’améliore” avec JS (progressive enhancement).
- Vous voulez rester dans les patterns WordPress (enqueue, REST, nonces) et éviter un “mini-SPA” dans un coin.
Quand ne PAS utiliser cette solution
- Vous construisez une application riche type SPA (routing, formulaires complexes, état global massif). Un framework (ou au moins une architecture plus lourde) sera plus adapté.
- Vous devez supporter des navigateurs très anciens (Interactivity API suit les standards modernes ; vérifiez vos contraintes).
- Votre interactivité est déjà fournie proprement par un plugin (ex. WooCommerce blocks, facettes dédiées). Réinventer la roue coûte cher en maintenance.
- Vous avez un besoin temps réel (WebSocket, présence, collaboration). L’Interactivity API n’est pas un substitut à une couche realtime.
Prérequis / avant de commencer
- WordPress 6.9.4 (avril 2026) minimum, PHP 8.1+.
- Un environnement de test (local ou staging). Évitez de tester ce genre de code directement en production : j’ai déjà vu un endpoint REST mal protégé se faire marteler en quelques minutes.
- Un accès aux permaliens (réglages) pour vérifier la REST API et régénérer si besoin.
- Outils utiles :
- Query Monitor pour inspecter hooks/REST/perfs.
- DevTools navigateur (Network + Console).
Sources officielles à garder sous la main :
- Interactivity API (Block Editor Handbook)
- REST API Handbook
- register_rest_route()
- wp_enqueue_script()
- PHP filter/validation (php.net)
Pour le suivi core : quand vous tombez sur un comportement “bizarre” (notamment en interaction avec le cache ou l’éditeur), vérifiez les tickets sur core.trac.wordpress.org et les PR sur github.com/WordPress/gutenberg.
L’approche naïve (et pourquoi l’éviter)
La version que je vois encore souvent : un bouton avec un onclick inline, un fetch vers admin-ajax.php, pas de nonce, et un script chargé partout sur le site.
<?php
// Exemple à NE PAS copier : inline JS, pas de nonce, pas de contrôle de permission.
echo '<button onclick="likePost(' . get_the_ID() . ')">Like</button>';
?>
<script>
function likePost(postId){
fetch('/wp-admin/admin-ajax.php?action=like&post_id=' + postId)
.then(r => r.json())
.then(console.log);
}
</script>
Ce qui se passe en coulisses :
- Sécurité : endpoint sans nonce = CSRF trivial. Et si vous ne validez pas
post_id, vous ouvrez la porte à des écritures arbitraires. - Performance : script injecté partout, impossible à contrôler finement, et difficile à mettre en cache correctement.
- Maintenance : le jour où vous devez rendre le composant accessible (ARIA), testable, ou compatible builder, vous repartez de zéro.
- DX : les handlers inline deviennent ingérables dès que vous avez 2 variantes de composant.
La bonne approche — tutoriel pas à pas
On va construire un plugin minimal, propre, avec un composant “Like” qui fonctionne :
- Sans JS : le bouton s’affiche (et vous pouvez décider d’un fallback).
- Avec JS : toggle instantané + compteur mis à jour, puis synchronisation REST.
Étape 1 — Créer le plugin
Créez un dossier wp-content/plugins/bpcab-interactivity-like/ avec :
bpcab-interactivity-like.phpassets/like.js
Étape 2 — Définir le rendu HTML “interactive”
L’Interactivity API s’appuie sur des attributs data-wp-* dans le markup. Le plus important : data-wp-interactive qui “attache” un namespace de store à un sous-arbre DOM.
On va rendre un bouton + un compteur. Le bouton :
- déclenche une action via
data-wp-on--click - affiche un texte dynamique via
data-wp-text - toggle une classe CSS via
data-wp-class--is-liked
Étape 3 — Charger le script correctement (enqueue ciblé)
Le piège classique : charger le JS sur toutes les pages. Ici, on charge le script uniquement si le contenu contient notre shortcode (ou si notre bloc est rendu). Pour rester simple et fiable, on va :
- fournir un shortcode
[bpcab_like] - détecter sa présence via
has_shortcode()au moment opportun
Note : sur des sites très builder (Elementor/Divi), le contenu peut être généré différemment. Je vous donne des variantes plus bas.
Étape 4 — Exposer un endpoint REST sécurisé
On évite admin-ajax.php pour ce cas. La REST API est plus propre, cache-friendly, et mieux outillée (codes HTTP, JSON, auth). On crée une route :
POST /wp-json/bpcab/v1/likeavecpost_idetdelta(+1 ou -1)
On protège avec :
- Nonce REST (
wp_create_nonce( 'wp_rest' )) - Validation stricte des paramètres
- Permissions : ici, on autorise les visiteurs anonymes mais on limite l’impact (delta ±1, post existant, type public). Pour une vraie persistance “par utilisateur”, il faudrait un modèle d’identité (user id, cookie signé, ou stockage serveur plus strict).
Étape 5 — Store Interactivity (optimistic UI + rollback)
Le pattern que j’utilise souvent :
- On met à jour l’UI immédiatement (optimistic)
- On envoie la requête REST
- Si ça échoue, on rollback l’état et on loggue proprement
Code complet
Fichier 1 : plugin PHP
Créez wp-content/plugins/bpcab-interactivity-like/bpcab-interactivity-like.php.
<?php
/**
* Plugin Name: BPCAB Interactivity Like
* Description: Exemple avancé Interactivity API : bouton Like sans framework, avec synchronisation REST.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
* Author: BPCAB
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class BPCAB_Interactivity_Like_Plugin {
private const SCRIPT_HANDLE = 'bpcab-like-interactivity';
private const REST_NAMESPACE = 'bpcab/v1';
private const REST_ROUTE = '/like';
public function hooks(): void {
add_action( 'init', [ $this, 'register_shortcode' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'maybe_enqueue_assets' ] );
add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
}
public function register_shortcode(): void {
add_shortcode( 'bpcab_like', [ $this, 'render_shortcode' ] );
}
/**
* Rend le composant Like.
* Utilisable dans un article, une page, un widget texte, ou via do_shortcode().
*/
public function render_shortcode( array $atts = [] ): string {
$atts = shortcode_atts(
[
'post_id' => 0,
'label_like' => 'J’aime',
'label_unlike' => 'Je n’aime plus',
],
$atts,
'bpcab_like'
);
$post_id = (int) $atts['post_id'];
if ( $post_id <= 0 ) {
$post_id = get_the_ID() ? (int) get_the_ID() : 0;
}
if ( $post_id <= 0 ) {
return '';
}
$post = get_post( $post_id );
if ( ! $post || 'publish' !== $post->post_status ) {
return '';
}
// Compteur stocké en post meta. Pour un site à fort trafic, préférez une table dédiée ou un agrégat asynchrone.
$likes = (int) get_post_meta( $post_id, '_bpcab_likes', true );
if ( $likes < 0 ) {
$likes = 0;
}
// État initial "liked" : ici, on ne persiste pas par utilisateur.
// On part sur false. Pour une vraie UX, vous pouvez lire un cookie signé ou une table user/post.
$initial_liked = false;
// Données initiales injectées dans le DOM (JSON) pour hydrater le store côté client.
$initial_state = [
'postId' => $post_id,
'likes' => $likes,
'liked' => $initial_liked,
'labels' => [
'like' => (string) $atts['label_like'],
'unlike' => (string) $atts['label_unlike'],
],
];
// Escaping : on encode en JSON, puis on échappe pour attribut HTML.
$initial_state_json = wp_json_encode( $initial_state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
if ( false === $initial_state_json ) {
return '';
}
// Markup Interactivity API.
// data-wp-interactive="bpcab/like" : namespace du store côté client.
// data-wp-context : contexte initial (state) accessible via le store.
$html = '<div class="bpcab-like"';
$html .= ' data-wp-interactive="bpcab/like"';
$html .= ' data-wp-context="' . esc_attr( $initial_state_json ) . '"';
$html .= '>';
$html .= '<button type="button" class="bpcab-like__button"';
$html .= ' data-wp-on--click="actions.toggle"';
$html .= ' data-wp-class--is-liked="state.liked"';
$html .= '>';
$html .= '<span class="bpcab-like__label" data-wp-text="state.liked ? state.labels.unlike : state.labels.like"></span>';
$html .= '</button>';
$html .= '<span class="bpcab-like__count" aria-live="polite">';
$html .= '<strong data-wp-text="state.likes">' . esc_html( (string) $likes ) . '</strong>';
$html .= '</span>';
$html .= '</div>';
return $html;
}
/**
* Enqueue conditionnel : on charge le JS seulement si le shortcode est présent.
* Attention : sur certaines pages builder, le contenu n'est pas dans post_content.
*/
public function maybe_enqueue_assets(): void {
if ( is_admin() ) {
return;
}
$post = get_post();
if ( ! $post ) {
return;
}
// Détection simple : shortcode présent dans post_content.
// Variante builder plus bas.
if ( ! has_shortcode( (string) $post->post_content, 'bpcab_like' ) ) {
return;
}
$asset_url = plugin_dir_url( __FILE__ ) . 'assets/like.js';
$asset_path = plugin_dir_path( __FILE__ ) . 'assets/like.js';
$ver = file_exists( $asset_path ) ? (string) filemtime( $asset_path ) : '1.0.0';
// Dépendances : le runtime Interactivity est fourni par WordPress (paquet @wordpress/interactivity).
// Le handle exact peut évoluer selon les versions ; en pratique WP expose les scripts nécessaires
// quand vous utilisez l'Interactivity API dans les blocs.
//
// Ici, on reste robuste : pas de dépendance forcée, mais on s'appuie sur le fait que WP charge
// le runtime interactivity quand le markup data-wp-interactive est présent.
wp_enqueue_script(
self::SCRIPT_HANDLE,
$asset_url,
[],
$ver,
[
'in_footer' => true,
'strategy' => 'defer',
]
);
// Données globales minimales : endpoint REST + nonce.
// Le nonce 'wp_rest' est le standard pour REST API.
wp_add_inline_script(
self::SCRIPT_HANDLE,
'window.BPCAB_LIKE = ' . wp_json_encode(
[
'restUrl' => esc_url_raw( rest_url( self::REST_NAMESPACE . self::REST_ROUTE ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
],
JSON_UNESCAPED_SLASHES
) . ';',
'before'
);
// Un peu de CSS minimal via inline (vous pouvez le sortir dans un fichier).
$css = '.bpcab-like{display:flex;gap:.6rem;align-items:center}.bpcab-like__button.is-liked{font-weight:700}.bpcab-like__count{opacity:.85}';
wp_register_style( 'bpcab-like-inline', false, [], $ver );
wp_enqueue_style( 'bpcab-like-inline' );
wp_add_inline_style( 'bpcab-like-inline', $css );
}
public function register_rest_routes(): void {
register_rest_route(
self::REST_NAMESPACE,
self::REST_ROUTE,
[
'methods' => 'POST',
'callback' => [ $this, 'rest_like' ],
'permission_callback' => [ $this, 'rest_permission' ],
'args' => [
'post_id' => [
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
'validate_callback' => function ( $value ) {
return is_numeric( $value ) && (int) $value > 0;
},
],
'delta' => [
'type' => 'integer',
'required' => true,
'sanitize_callback' => function ( $value ) {
$value = (int) $value;
if ( 1 === $value ) {
return 1;
}
if ( -1 === $value ) {
return -1;
}
return 0;
},
'validate_callback' => function ( $value ) {
$value = (int) $value;
return 1 === $value || -1 === $value;
},
],
],
]
);
}
/**
* Permission : on accepte les visiteurs anonymes, mais on exige un nonce REST valide.
* Si vous voulez limiter aux utilisateurs connectés : return is_user_logged_in();
*/
public function rest_permission( WP_REST_Request $request ): bool|WP_Error {
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! $nonce ) {
return new WP_Error( 'bpcab_like_missing_nonce', 'Nonce manquant.', [ 'status' => 403 ] );
}
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error( 'bpcab_like_invalid_nonce', 'Nonce invalide.', [ 'status' => 403 ] );
}
return true;
}
public function rest_like( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$post_id = (int) $request->get_param( 'post_id' );
$delta = (int) $request->get_param( 'delta' );
$post = get_post( $post_id );
if ( ! $post || 'publish' !== $post->post_status ) {
return new WP_Error( 'bpcab_like_invalid_post', 'Article introuvable.', [ 'status' => 404 ] );
}
// Optionnel : limiter aux post types publics.
$post_type_obj = get_post_type_object( (string) $post->post_type );
if ( ! $post_type_obj || ! $post_type_obj->public ) {
return new WP_Error( 'bpcab_like_forbidden_type', 'Type de contenu non autorisé.', [ 'status' => 403 ] );
}
// Mise à jour robuste.
// Note : update_post_meta n'est pas atomique. Sur très fort trafic, vous pouvez avoir des collisions.
// Pour un compteur critique, utilisez une table custom + requête SQL atomique, ou un système de queue.
$current = (int) get_post_meta( $post_id, '_bpcab_likes', true );
$new = $current + $delta;
if ( $new < 0 ) {
$new = 0;
}
update_post_meta( $post_id, '_bpcab_likes', $new );
return new WP_REST_Response(
[
'postId' => $post_id,
'likes' => $new,
],
200
);
}
}
add_action(
'plugins_loaded',
static function (): void {
( new BPCAB_Interactivity_Like_Plugin() )->hooks();
}
);
Fichier 2 : script Interactivity
Créez wp-content/plugins/bpcab-interactivity-like/assets/like.js.
/* global BPCAB_LIKE */
(function () {
'use strict';
/**
* Interactivity API :
* On enregistre un store "bpcab/like" qui expose state/actions.
* Le state initial provient de data-wp-context (injecté côté PHP).
*
* Référence : https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/
*/
const { store, getContext } = window.wp && window.wp.interactivity ? window.wp.interactivity : {};
if (!store || !getContext) {
// Cas réel : le runtime interactivity n'est pas chargé (mauvais enqueue, optimisation agressive, plugin cache).
// On évite de casser la page.
return;
}
async function postLike({ postId, delta }) {
const res = await fetch(BPCAB_LIKE.restUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': BPCAB_LIKE.nonce
},
body: JSON.stringify({ post_id: postId, delta })
});
if (!res.ok) {
const text = await res.text().catch(() => '');
const err = new Error('REST error ' + res.status + ' ' + res.statusText);
err.details = text;
throw err;
}
return await res.json();
}
store('bpcab/like', {
state: {
get postId() {
return getContext().postId;
},
get likes() {
return getContext().likes;
},
get liked() {
return getContext().liked;
},
get labels() {
return getContext().labels || { like: 'J’aime', unlike: 'Je n’aime plus' };
}
},
actions: {
/**
* Toggle optimiste :
* - on inverse liked
* - on ajuste likes (+1/-1)
* - on synchronise REST
* - rollback en cas d'erreur
*/
async toggle() {
const ctx = getContext();
const previousLiked = !!ctx.liked;
const previousLikes = Number.isFinite(ctx.likes) ? ctx.likes : 0;
const nextLiked = !previousLiked;
const delta = nextLiked ? 1 : -1;
// Mise à jour optimiste.
ctx.liked = nextLiked;
ctx.likes = Math.max(0, previousLikes + delta);
try {
const data = await postLike({ postId: ctx.postId, delta });
// On resynchronise avec la vérité serveur.
if (data && typeof data.likes === 'number') {
ctx.likes = data.likes;
}
} catch (e) {
// Rollback.
ctx.liked = previousLiked;
ctx.likes = previousLikes;
// Log discret. En prod, vous pouvez envoyer vers un logger.
// Ne faites pas d'alert() agressif.
console.error('[bpcab/like] Échec synchronisation', e);
}
}
}
});
})();
Explication du code
Logique générale (simple)
Le PHP rend un HTML “normal” (un bouton + un compteur) et y ajoute :
data-wp-interactive="bpcab/like"pour dire à WordPress : “ce sous-arbre utilise le store bpcab/like”.data-wp-context="...json..."pour injecter l’état initial (postId, likes, liked, labels).data-wp-on--click="actions.toggle"pour connecter le clic à une action JS.data-wp-text="..."pour afficher une valeur calculée depuis le state.
Le JS enregistre un store “bpcab/like”. Quand l’utilisateur clique, on modifie le contexte (réactif), puis on appelle la REST API pour persister le compteur.
Précisions techniques (hooks, sécurité, intégration WP 6.9.4)
add_shortcode(): pratique pour un exemple portable. En production, vous ferez souvent un bloc dynamique (render_callback) pour un contrôle plus fin dans l’éditeur.wp_enqueue_scripts: on enqueue côté front. Le point sensible est la détection de présence. Si votre builder ne met pas le shortcode danspost_content, le script ne se chargera pas. Je couvre les variantes plus bas.- REST API :
register_rest_route()expose la route.permission_callbackvérifie le nonceX-WP-Nonce(CSRF).- Validation stricte de
deltaà ±1. Sans ça, vous verrez des gens posterdelta=999999dès que l’endpoint est public.
- Race conditions : un compteur en post meta n’est pas atomique. Sous fort trafic, deux requêtes simultanées peuvent écraser une valeur. Pour un blog classique c’est acceptable ; pour un média à gros volume, je passe sur table custom + requête atomique (ou un buffer/queue).
Pourquoi data-wp-context plutôt que wp_localize_script par instance
wp_localize_script est global (par handle), ce qui devient pénible avec 10 composants sur la même page. Ici, chaque instance porte son propre JSON. C’est beaucoup plus scalable quand vous avez une boucle d’articles (ex. page catégorie avec 20 “likes”).
Variantes et cas d’usage
Variante 1 — Plusieurs boutons sur une page (boucle) sans JS global complexe
Vous pouvez mettre [bpcab_like post_id="123"] dans une boucle, ou générer le HTML via PHP. Chaque composant a son propre data-wp-context. Le store est partagé, mais le contexte est par instance : c’est précisément l’intérêt.
Variante 2 — Stocker “liked” par utilisateur (connecté) au lieu d’un simple compteur
Pattern que j’utilise :
- Si l’utilisateur est connecté : stocker dans user meta une liste (ou table relationnelle) des posts likés.
- Le endpoint REST vérifie
is_user_logged_in()et utiliseget_current_user_id(). - Le compteur agrégé reste en post meta (ou table), mais l’état “liked” devient fiable.
Attention : une liste en user meta peut grossir. Sur gros sites, table relationnelle dédiée (user_id, post_id, created_at) + index.
Variante 3 — Mode “sans persistance” (UI only) pour accordéons/onglets
Si votre besoin est purement UI (accordéon, tabs), supprimez l’endpoint REST et gardez uniquement :
data-wp-on--clickpour changer un booléen dans le contextedata-wp-class--is-open/data-wp-textpour refléter l’état
Dans mon expérience, c’est la meilleure porte d’entrée : zéro backend, zéro sécurité, et vous validez votre pipeline d’enqueue/compatibilité cache.
Compatibilité Divi 5 / Elementor / Avada
Le point qui casse le plus souvent : le script ne se charge pas parce que votre contenu final ne contient pas le shortcode dans post_content.
Divi 5
Divi peut stocker des layouts dans des meta, et le rendu final n’est pas toujours détectable via has_shortcode( $post->post_content ). Deux options robustes :
- Enqueue “large” uniquement sur les pages où vous savez que le module apparaît (ex. via condition template).
- Ou détecter la présence du markup rendu au moment du rendu (buffer) — plus fragile.
Approche pragmatique que j’ai souvent utilisée : ajouter un petit réglage “forcer l’enqueue” via un filtre.
<?php
// À mettre dans votre plugin (ou un mu-plugin) si Divi ne déclenche pas has_shortcode().
add_filter( 'bpcab_like_force_enqueue', function( bool $force ): bool {
if ( function_exists( 'et_theme_builder_get_template_layouts' ) ) {
// Indice que Divi est actif. À affiner selon votre contexte.
return true;
}
return $force;
}, 10, 1 );
Puis adaptez maybe_enqueue_assets() pour appliquer ce filtre (si vous voulez cette variante, faites-le proprement). Je ne l’ai pas inclus par défaut pour garder le plugin minimal.
Elementor
Avec Elementor, le contenu est souvent rendu depuis des meta. Deux solutions propres :
- Créer un widget Elementor dédié qui rend le markup (et enregistre une dépendance script via l’API Elementor).
- Ou enqueuer sur les pages Elementor via une condition (détection plugin + meta).
Si vous partez sur la détection simple :
<?php
// Exemple : détecter une page construite avec Elementor.
$is_elementor = (bool) get_post_meta( get_the_ID(), '_elementor_edit_mode', true );
Avada (Fusion Builder)
Avada a ses propres shortcodes et un pipeline de rendu. Le plus fiable : utiliser le shortcode [bpcab_like] directement dans un bloc/élément “Code” ou “Shortcode”.
Si Avada minifie/concatène agressivement, vérifiez que :
- le script n’est pas déplacé en
headsansdefer - le runtime Interactivity n’est pas “retiré” par une optimisation
Vérifications après mise en place
- Sur une page contenant
[bpcab_like], ouvrez DevTools > Network :- vérifiez que
assets/like.jsest bien chargé (status 200) - vérifiez que
POST /wp-json/bpcab/v1/likeretourne 200 au clic
- vérifiez que
- DevTools > Console :
- aucune erreur
window.wp.interactivityundefined
- aucune erreur
- Côté WordPress :
- le meta
_bpcab_likesse met à jour (vous pouvez inspecter via WP-CLI ou un plugin de meta).
- le meta
Tableau de diagnostic rapide
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Le bouton s’affiche mais ne réagit pas | Runtime Interactivity non chargé ou JS non chargé | Console : window.wp.interactivity existe ? Network : like.js chargé ? |
Corriger l’enqueue, désactiver optimisation JS, vérifier builder |
| Erreur 403 sur la route REST | Nonce manquant/invalide | Network : header X-WP-Nonce présent ? |
Vérifier wp_add_inline_script, cache HTML, CDN qui strippe headers |
| Le compteur “saute” en arrière | Rollback après erreur REST | Console : log [bpcab/like] Échec synchronisation |
Vérifier REST URL, permaliens, WAF, CORS, plugins sécurité |
| Le compteur est incohérent sous charge | Race condition sur post meta | Simuler clics simultanés (k6/ab) et comparer | Table custom + update atomique, ou agrégation asynchrone |
Si ça ne marche pas
- Vérifiez où vous avez collé le code : ce plugin doit être dans
wp-content/plugins/...et activé. J’ai déjà vu le fichier PHP collé dans le thème, puis “ça marche sur une page et pas l’autre”. - Vérifiez PHP : en PHP < 8.1, vous pouvez avoir des erreurs de typage. Regardez
wp-content/debug.logsiWP_DEBUGest activé. - Vérifiez les permaliens : si la REST API renvoie 404, allez dans Réglages > Permaliens et enregistrez (sans changer). C’est un classique sur des migrations.
- Désactivez temporairement l’optimisation JS (cache/minify) : j’ai souvent croisé le runtime Interactivity “cassé” par une concaténation qui change l’ordre de chargement.
- Testez le endpoint REST au curl :
curl -i -X POST "https://example.com/wp-json/bpcab/v1/like"
-H "Content-Type: application/json"
-H "X-WP-Nonce: VOTRE_NONCE"
--data '{"post_id":123,"delta":1}'
Si vous n’avez pas de nonce sous la main, testez au moins que la route existe (elle doit répondre 403 “Nonce manquant”, ce qui est bon signe).
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
Uncaught TypeError: Cannot destructure property 'store' ... |
window.wp.interactivity absent (runtime non chargé) |
Vérifier enqueue, désactiver minify, s’assurer que WP charge les scripts interactivity |
| Le script ne se charge jamais | has_shortcode() ne détecte pas (builder, rendu via meta) |
Ajouter une condition builder (Elementor/Divi/Avada) ou enqueuer via widget/module dédié |
403 Nonce invalide |
HTML mis en cache avec un nonce expiré, ou page servie via cache full-page | Exclure la page du cache, ou générer le nonce via endpoint public, ou limiter aux users connectés |
SyntaxError: Unexpected token dans data-wp-context |
JSON mal encodé/échappé, guillemets cassés | Toujours passer par wp_json_encode puis esc_attr |
| Fatal error après copier-coller | Parenthèse/point-virgule manquant, ou fichier placé dans le mauvais dossier | Valider avec un linter, activer WP_DEBUG sur staging, corriger la syntaxe |
| Le compteur devient négatif | Delta non validé ou double clic rapide | Valider delta à ±1, clamp à 0 côté serveur et côté client |
| Conflit avec un plugin de sécurité/WAF | Blocage des POST REST ou headers | Whitelister la route /wp-json/bpcab/v1/like, vérifier logs du WAF |
| Vous utilisez un ancien snippet trouvé en 2023 | API/handles changés, pratiques obsolètes | Cibler WP 6.9.4+, s’appuyer sur la doc Interactivity actuelle |
Conseils sécurité, performance et maintenance
Sécurité
- Nonce REST obligatoire si votre endpoint modifie des données. Sans nonce, CSRF.
- Validez les paramètres : ici
deltaest strictement ±1. C’est une barrière simple mais efficace. - Réfléchissez à l’abus : un compteur public peut être manipulé. Si la métrique a une valeur business, ajoutez :
- rate limiting (au niveau reverse proxy/WAF)
- stockage par utilisateur (connecté) ou cookie signé
- détection bot
Performance
- Enqueue conditionnel : évitez de charger
like.jspartout. - Évitez les requêtes REST inutiles : vous pouvez ajouter un debounce si vous autorisez multi-clics (ici on toggle, donc stable).
- Fort trafic : post meta + update non atomique. Si vous avez des pics, passez à une table custom et une mise à jour SQL atomique (ou un agrégat différé).
Maintenance
- Gardez le store dans un namespace clair (
bpcab/like), évitez les noms génériques (app). - Évitez de coupler votre logique à un builder. Faites une couche d’intégration (widget/module) si nécessaire.
- Surveillez les évolutions Interactivity API via Gutenberg (PR) et le handbook. Les changements arrivent d’abord côté Gutenberg, puis sont mergés en core.
Ressources
- Interactivity API (developer.wordpress.org)
- REST API Handbook
- register_rest_route() (référence)
- wp_create_nonce() (référence)
- wp_verify_nonce() (référence)
- WordPress Core Trac (suivi bugs)
- Repo Gutenberg (PR Interactivity)
- filter_var() (php.net)
- Forums support WordPress.org (cas réels)
FAQ
Est-ce que l’Interactivity API remplace React dans Gutenberg ?
Non. Gutenberg utilise toujours React pour l’éditeur. L’Interactivity API vise surtout l’interactivité front-end des blocs/composants, avec un modèle plus léger et plus déclaratif.
Pourquoi mon window.wp.interactivity est undefined ?
En pratique, c’est soit un problème d’enqueue (script chargé trop tôt / pas chargé), soit une optimisation JS qui casse l’ordre, soit un builder qui génère le contenu sans déclencher vos conditions. Désactivez temporairement minify/defer “agressifs” et vérifiez Network.
Est-ce compatible avec un cache full-page ?
Oui pour l’affichage. Pour la persistance, attention au nonce : si la page HTML est servie depuis un cache et contient un nonce expiré, vos POST REST vont échouer (403). Sur des sites très cachés, je préfère générer le nonce via un endpoint public (lecture seule) ou limiter la fonctionnalité aux utilisateurs connectés.
Pourquoi utiliser REST plutôt que admin-ajax ?
REST fournit une sémantique HTTP plus propre, des réponses JSON standard, et s’intègre mieux aux outils modernes. admin-ajax reste valable, mais il devient vite un “fourre-tout” difficile à sécuriser et à profiler.
Peut-on faire un bloc Gutenberg au lieu d’un shortcode ?
Oui, et c’est souvent préférable. Le cœur du pattern reste identique : rendu SSR + attributs data-wp-* + store. Je pars sur shortcode ici pour que vous puissiez tester en 5 minutes, y compris dans des builders.
Comment gérer plusieurs compteurs différents (likes, bookmarks, votes) ?
Utilisez des namespaces distincts (bpcab/like, bpcab/bookmark) ou un store unique paramétré par contexte. Évitez un store “monolithique” si vos composants sont indépendants.
Le compteur en post meta est-il fiable ?
Pour un blog classique : oui. Pour un trafic élevé : non, vous aurez des collisions. Passez à une table custom et une mise à jour atomique, ou utilisez une stratégie d’agrégation asynchrone.
Comment tester proprement ce code ?
Je fais généralement :
- Staging + WP_DEBUG log
- Test manuel : clics rapides, navigation, cache navigateur
- Test REST : curl (403 attendu sans nonce, 200 avec nonce)
- Test compat cache : activer le cache, vérifier expiration nonce
Pourquoi ne pas mettre le nonce dans data-wp-context ?
Vous pouvez, mais vous allez dupliquer une valeur sensible dans chaque instance. Je préfère un objet global minimal, puis un contexte par composant pour l’état UI. Si vous avez des pages très longues, ça réduit la taille HTML.
Est-ce que ça marche dans un thème classique (non FSE) ?
Oui. L’Interactivity API n’est pas réservée aux block themes. Le point clé est de charger correctement les assets et de rendre le markup avec les attributs data-wp-*.
Que faire si un plugin de snippets casse le code ?
Évitez de coller ce plugin dans un snippet. Créez un vrai plugin (comme ici). Les snippets sont pratiques, mais j’ai vu trop de sites tomber à cause d’un snippet activé sur une version PHP différente, ou d’un copier-coller avec une accolade en moins.