Le problème : un site d’actualités locales republiait chaque nuit environ 5 000 articles pour corriger une métadonnée de catégorie mal renseignée lors d’une migration antérieure, via un script WP-CLI exécuté en tâche planifiée. Après chaque exécution, les blocs Query Loop affichant les « derniers articles par catégorie » sur la page d’accueil et les pages de rubrique continuaient à afficher les anciennes catégories pendant plusieurs heures, alors que la base de données, elle, contenait déjà la correction.
Pourquoi le cache de blocs ne se comporte pas comme le cache de page
Cette catégorie de problème ne concerne pas le cache de page HTML classique (FastCGI, Varnish), qui expirait normalement selon sa propre configuration et affichait bien la page mise à jour dès sa prochaine régénération. Le problème se situait dans un cache de blocs plus fin, mis en place via l’API de cache persistant de WordPress spécifiquement pour le rendu du bloc Query Loop, afin d’éviter de relancer la requête de sélection d’articles à chaque affichage. Ce cache de blocs a son propre cycle de vie et ses propres clés, indépendantes du cache de page.

Pourquoi la mise à jour en masse ne déclenchait pas l’invalidation attendue
Le script WP-CLI mettait à jour les articles via wp post update en boucle, ce qui déclenche normalement le hook save_post, censé invalider le cache concerné. Le problème venait d’un détail technique précis : pour accélérer le traitement des 5 000 articles, le script utilisait l’option --skip-cache-flush ainsi qu’un appel direct à $wpdb->update() pour la métadonnée de catégorie spécifique, en contournant volontairement les hooks WordPress standards par souci de rapidité d’exécution sur un aussi grand volume. Ce contournement, raisonnable pour la performance du script lui-même, avait pour effet secondaire de ne jamais déclencher l’invalidation du cache de blocs qui dépend justement de ces hooks.
# Extrait du script original, qui contourne les hooks pour la vitesse
foreach ( $ids_articles as $id ) {
$wpdb->update(
$wpdb->postmeta,
[ 'meta_value' => $nouvelle_categorie ],
[ 'post_id' => $id, 'meta_key' => 'categorie_legacy' ]
);
}
// Aucun hook save_post déclenché ici : le cache de blocs ne le sait jamais.
La recette d’invalidation ciblée
Plutôt que de restaurer les hooks standards (ce qui aurait ralenti significativement le traitement des 5 000 articles, l’objectif initial du contournement), le choix a été d’ajouter une étape d’invalidation ciblée en fin de script, qui vide uniquement les clés de cache correspondant aux catégories effectivement modifiées, sans toucher au reste du cache de blocs qui reste valide.
$categories_modifiees = array_unique( $categories_touchees_par_le_script );
foreach ( $categories_modifiees as $slug_categorie ) {
wp_cache_delete( 'query_loop_categorie_' . $slug_categorie, 'blocs_rendus' );
}
WP_CLI::log( sprintf(
'Cache de blocs invalidé pour %d catégorie(s) : %s',
count( $categories_modifiees ),
implode( ', ', $categories_modifiees )
) );
Cette approche suppose que le code responsable du rendu du bloc Query Loop utilise bien une clé de cache prévisible par catégorie, ce qui était le cas ici grâce à un filtre personnalisé déjà en place sur render_block pour ce type de bloc.
add_filter( 'render_block', function ( $contenu, $bloc ) {
if ( 'core/query' !== $bloc['blockName'] ) {
return $contenu;
}
$slug_categorie = $bloc['attrs']['query']['taxQuery']['category'][0] ?? null;
if ( ! $slug_categorie ) {
return $contenu;
}
$cle = 'query_loop_categorie_' . $slug_categorie;
$cache = wp_cache_get( $cle, 'blocs_rendus' );
if ( false !== $cache ) {
return $cache;
}
wp_cache_set( $cle, $contenu, 'blocs_rendus', HOUR_IN_SECONDS );
return $contenu;
}, 10, 2 );
Pourquoi ne pas simplement tout purger
Une purge totale du groupe de cache blocs_rendus après chaque exécution du script aurait été plus simple à écrire, mais aurait forcé la régénération de toutes les pages de rubrique du site, y compris celles dont aucun article n’avait changé de catégorie, avec un pic de charge inutile sur la base de données au moment précis où le trafic nocturne, bien que faible, existe encore. L’invalidation ciblée par catégorie modifiée évite ce pic disproportionné par rapport à l’ampleur réelle du changement.
- Le cache de blocs Query Loop suit un cycle de vie distinct du cache de page HTML classique.
- Un script qui contourne les hooks WordPress pour la vitesse doit prévoir sa propre invalidation.
- Invalider par clé ciblée plutôt que par groupe complet limite le pic de charge après une mise à jour massive.
Contourner les hooks WordPress pour aller plus vite est un choix parfaitement défendable, à condition de ne jamais oublier ce que ces hooks faisaient réellement avant de les court-circuiter.
Hors périmètre
Cette recette ne concerne pas le cache de page HTML classique, qui suivait ici son propre cycle sans lien avec le problème rencontré, et qui aurait de toute façon fini par expirer et régénérer la page correctement, contrairement au cache de blocs qui n’avait aucune raison de se renouveler de lui-même sans invalidation explicite.