« Ce bloc marchait très bien la semaine dernière, et maintenant tous les articles affichent un bandeau d’erreur. » Cette phrase, entendue sur plusieurs projets au tournant de l’année, désigne presque toujours la même cause : une logique conditionnelle glissée dans la fonction save() d’un bloc, plutôt que dans son rendu côté serveur.
Ce billet décrit cet antipattern précis, pourquoi il casse au moindre changement, et comment le remplacer par un rendu dynamique. Les blocs purement statiques, sans aucune logique conditionnelle, ne sont pas concernés par ce problème et restent hors sujet ici.
Ce qu’on voit sur le terrain
Un bloc « offre spéciale » a été développé pour afficher un bandeau différent selon que la date de fin de promotion est dépassée ou non. La tentation, très répandue chez qui découvre l’API des blocs, consiste à écrire cette condition directement dans la fonction save() :
save( { attributes } ) {
const expire = new Date( attributes.dateFin ) < new Date();
return (
<div className={ expire ? 'offre-expiree' : 'offre-active' }>
{ attributes.texte }
</div>
);
}
Ce code compile sans erreur, s’affiche correctement à l’écriture de l’article, et pose problème uniquement plus tard, silencieusement.
Pourquoi c’est un problème
La fonction save() ne s’exécute qu’une seule fois, au moment de l’enregistrement de l’article dans l’éditeur. Son résultat est figé dans le contenu HTML stocké en base, sous forme de commentaires de bloc. À la prochaine ouverture de l’article, WordPress compare le balisage attendu (recalculé à partir du code actuel) au balisage stocké : si expire a changé de valeur entre-temps, la comparaison échoue, et l’article passe en « contenu invalide », avec le fameux écran de récupération de bloc.
Autrement dit, une valeur calculée à un instant T (ici une date comparée à « maintenant ») ne peut jamais rester stable dans save(), puisque « maintenant » change à chaque ouverture de la page. C’est le cœur du problème : save() doit produire un résultat déterministe, toujours identique pour les mêmes attributs, jamais dépendant de l’heure d’exécution.

Ce qu’il faut faire à la place
La bonne approche consiste à déclarer le bloc comme dynamique, avec une fonction save() qui ne renvoie rien (return null), et à déplacer toute la logique conditionnelle dans un render_callback PHP, exécuté à chaque affichage de la page :
save() {
return null;
}
// côté PHP
function rendre_offre_speciale( $attributs ) {
$expire = strtotime( $attributs['dateFin'] ) < time();
$classe = $expire ? 'offre-expiree' : 'offre-active';
return sprintf( '<div class="%s">%s</div>', esc_attr( $classe ), esc_html( $attributs['texte'] ) );
}
Ce déplacement change tout : la date n’est plus jamais comparée qu’au moment réel de l’affichage de la page, jamais figée dans le contenu stocké. Le bloc reste valide indéfiniment, quelle que soit la date à laquelle un visiteur consulte la page.
Repérer ce piège avant qu’il ne coûte cher
- Toute comparaison à
new Date()danssave()est un signal d’alarme immédiat - Tout accès à une donnée externe (compteur, stock, statut) dans
save()doit être banni - Si
save()contient une conditionifqui dépend d’autre chose que des attributs eux-mêmes, la logique est mal placée
Le test qui aurait tout révélé
Un test simple aurait suffi à détecter ce problème avant la mise en production : publier un article avec une date de fin proche, puis rouvrir cet article après l’échéance. Sur un bloc bien conçu, aucun changement d’état ne doit jamais rendre le contenu stocké invalide.
Une règle qu’on applique systématiquement en revue de code : si une fonction
save()contient autre chose qu’un rendu déterministe des attributs, quelque chose ne va pas.
Quoi faire
Toute logique métier, aussi simple soit-elle, appartient au rendu côté serveur d’un bloc dynamique, jamais à sa fonction save(). Ce réflexe, une fois acquis, évite la quasi-totalité des écrans de récupération de bloc rencontrés sur des blocs personnalisés en début de projet.