Un bloc d’accordéon existe en double dans le même plugin : une fonction save() qui génère du HTML statique enregistré en base, et un render.php qui recalcule un rendu quasi identique côté serveur. Les deux ne sont jamais appelés en même temps — l’attribut apiVersion et une case cochée dans block.json décident laquelle s’exécute — mais les deux vivent dans le dépôt, année après année.
Ce choix n’est presque jamais assumé comme une stratégie : il vient d’une hésitation initiale entre bloc statique et bloc dynamique, jamais tranchée, puis d’une peur de casser l’existant en supprimant l’une des deux versions. Le résultat est un bloc qui coûte deux fois plus cher à faire évoluer, pour un bénéfice qui, à l’examen, n’existe pas.
Comment ce doublon apparaît concrètement
Le scénario typique : un développeur crée un bloc simple avec save(), pensant qu’un rendu statique suffira. Quelques mois plus tard, un besoin de contenu dynamique apparaît — afficher une donnée qui change selon le contexte d’affichage — et plutôt que de migrer le bloc en dynamique, on ajoute un second chemin : un render_callback déclaré dans register_block_type(), activé conditionnellement.
Le block.json finit par ressembler à ceci, avec un attribut caché pour distinguer les deux modes :
{
"attributes": {
"modeAffichage": { "type": "string", "default": "statique" }
},
"render": "file:./render.php"
}
Sauf que render.php est appelé systématiquement dès qu’il est déclaré dans block.json : la fonction save() ne sert alors qu’à générer un aperçu HTML jamais réellement affiché en front, un vestige conservé « au cas où ». C’est là que la confusion s’installe : deux développeurs de la même équipe modifient chacun un fichier différent, persuadés de corriger le bug.
Le coût réel de la double implémentation

Sur un audit récent d’un thème d’agence, sept blocs sur vingt-trois suivaient ce schéma. Le coût ne se mesure pas seulement en lignes de code dupliquées, mais en trois points précis :
- Divergence silencieuse : une correction de style appliquée dans
render.phpmais oubliée danssave()ne casse rien immédiatement — elle attend qu’un contenu bascule en mode statique, souvent après une migration ou une restauration de sauvegarde. - Tests doublés : chaque scénario de test doit être rejoué deux fois, une fois par chemin de rendu, ce qui décourage vite d’écrire des tests du tout.
- Onboarding ralenti : un nouveau développeur découvre le second fichier par hasard, en général en debuggant un comportement incohérent, jamais par une documentation qui l’annonce.
Pourquoi la prudence initiale ne se justifie pas
L’argument avancé pour justifier ce doublon est presque toujours le même : « si le rendu dynamique tombe en panne, le contenu enregistré en base reste affichable ». C’est un raisonnement séduisant mais faux dans la pratique : un bloc déclaré dynamique dans block.json ne retombe jamais automatiquement sur son save() en cas d’erreur PHP — il affiche une erreur ou un contenu vide, comme n’importe quel autre bloc dynamique mal codé.
Un bloc dynamique bien testé, avec une valeur de repli explicite dans
render.php, protège mieux le site qu’un doublon jamais synchronisé.
Trancher : trois critères simples
Pour décider une fois pour toutes entre statique et dynamique, trois questions suffisent généralement :
| Critère | Statique (save) | Dynamique (render.php) |
|---|---|---|
| Le contenu dépend-il de données qui changent hors édition ? | Non | Oui → dynamique |
| Le contenu doit-il rester lisible même si le plugin est désactivé ? | Oui → statique | Non, disparaît avec le plugin |
| Le rendu dépend-il du rôle ou du contexte de l’utilisateur affichant la page ? | Non | Oui → dynamique |
Dans l’immense majorité des cas observés, la réponse penche nettement d’un côté : il n’y a pas de zone grise qui justifierait de garder les deux chemins actifs en permanence.
Migrer proprement vers une seule implémentation
Une fois le choix fait, la bascule se fait via le système de deprecated de l’API Blocks : on déclare l’ancienne signature comme dépréciée, avec sa fonction save d’origine conservée uniquement pour la validation du contenu existant, et on bascule la déclaration active vers render.php seul. Le contenu déjà publié continue de se valider contre l’ancienne version le temps que WordPress le réenregistre, sans qu’aucun code de production ne dépende plus des deux chemins.
En résumé
Maintenir un bloc dupliqué entre save() et render.php n’est presque jamais un choix technique assumé : c’est une décision reportée qui finit par coûter plus cher que le renoncement qu’elle voulait éviter. Trancher tôt entre statique et dynamique, documenter ce choix dans le readme du bloc, et supprimer le chemin mort évite des heures de débogage à chaque évolution future.