Si vous avez déjà vu “Block rendered as empty” dans l’éditeur, ou un bloc qui marche en local mais disparaît en production, le problème vient presque toujours du même endroit : un build mal branché, des assets mal enregistrés, ou un rendu serveur absent alors que votre bloc en dépend.
Le problème / Le besoin
Vous voulez créer un bloc Gutenberg personnalisé propre, maintenable, et compatible WordPress 6.9.4 (avril 2026), sans bricoler un enqueue de scripts à la main à chaque itération. Le besoin typique : un bloc “Encart Alerte” (ou “CTA”, “FAQ”, “Produit”, etc.) avec :
- un éditeur React (Inspector Controls, couleurs, options),
- un rendu front fiable (sans dépendre d’un JS runtime côté visiteur),
- une chaîne de build moderne via
@wordpress/scripts(Webpack/Babel prêt à l’emploi), - une enregistrement d’assets robuste via
block.jsonet les métadonnées.
À la fin, vous saurez livrer un bloc complet sous forme de plugin, avec build, versioning, internationalisation, et un rendu serveur (dynamic block) quand c’est pertinent. J’insiste sur le dynamic block parce que j’ai souvent vu des blocs “statiques” devenir ingérables dès que le contenu doit dépendre d’options globales, d’un CPT, ou d’un contexte.
Résumé rapide
- On crée un plugin qui enregistre un bloc via
block.jsonetregister_block_type(). - On utilise
@wordpress/scriptspour buildersrc/index.jsversbuild/index.js+ fichier.asset.php. - On écrit un bloc avec UI côté éditeur (JS) et rendu côté serveur (PHP), pour éviter les divergences front/éditeur.
- On charge CSS/JS via les champs
editorScript,style,editorStyledublock.json. - On traite les points qui cassent en prod : chemins, cache, dépendances, version PHP, et hooks d’enregistrement.
Quand utiliser cette solution
- Vous voulez un bloc réutilisable sur plusieurs sites, livré en plugin.
- Vous avez besoin d’un rendu stable côté front (SEO, performance, compatibilité cache) : dynamic block recommandé.
- Vous voulez éviter le “copier/coller” d’un snippet d’enqueue et adopter le flux standard WordPress :
block.json+ build. - Votre bloc doit évoluer (nouvelles options, variations, styles) sans casser le contenu existant.
- Vous travaillez en équipe : le build via
@wordpress/scriptsstandardise l’environnement.
Quand ne PAS utiliser cette solution
- Vous avez juste besoin d’un contenu simple : un pattern de blocs (block pattern) suffit souvent, sans JS. Voir Block Patterns.
- Vous cherchez une mise en page complexe mais sans logique : un bloc de groupe + styles globaux + variations peut remplacer un bloc custom.
- Vous ne pouvez pas gérer une étape de build (CI/CD, Node) : dans ce cas, utilisez un plugin générateur (scaffold) puis committez le dossier build et évitez d’exécuter Node en prod.
- Vous êtes sur un environnement verrouillé où Node est interdit : vous pouvez livrer uniquement les fichiers buildés, mais vous perdez la boucle de dev.
Prérequis / avant de commencer
Je pars du principe que vous développez pour WordPress 6.9.4 et PHP 8.1+. Beaucoup de “tutos” plus anciens cassent aujourd’hui parce qu’ils n’utilisent pas block.json correctement ou oublient le .asset.php.
- WordPress : 6.9.4 (ou plus récent).
- PHP : 8.1+ (recommandé). Référence : PHP Supported Versions.
- Node.js : une LTS récente (18/20/22 selon votre stack). Évitez les versions EOL.
- Accès : un environnement de staging/local. Ne testez pas un build de bloc directement en prod sans sauvegarde.
- Outils :
- WP-CLI (optionnel mais pratique).
- Un plugin de logs (ou accès
debug.log).
Précautions :
- Activez
WP_DEBUGetWP_DEBUG_LOGsur votre environnement de test. - Si vous utilisez un cache (plugin, Varnish, Cloudflare), prévoyez de purger : j’ai souvent vu des CSS de bloc “ne pas se charger” alors que c’était juste un cache agressif.
Docs officielles utiles :
L’approche naïve (et pourquoi l’éviter)
Le classique que je vois encore en 2026 : un bloc “fait à la main” avec un gros wp_enqueue_script dans wp_enqueue_scripts, sans .asset.php, sans dépendances déclarées, et parfois même sans block.json. Ça marche “chez moi”, puis ça casse après une mise à jour de WordPress/Gutenberg.
Exemple naïf (à ne pas reproduire)
<?php
// Mauvaise pratique : charge partout, pas seulement dans l'éditeur, dépendances non gérées.
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'mon-bloc',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-editor' ), // Souvent faux/incomplet.
'1.0.0',
true
);
} );
Pourquoi c’est un problème :
- Performance : le JS du bloc part sur tout le front, même si aucun bloc n’est utilisé.
- Dépendances : la liste à la main finit toujours par être fausse. WordPress génère un
.asset.phpprécisément pour ça. - Maintenance : vous dupliquez la logique d’enregistrement d’assets au lieu d’utiliser les métadonnées.
- Risque de conflits : handle global, collisions, et scripts chargés dans le mauvais contexte.
La bonne approche — tutoriel pas à pas
Objectif : un plugin bpca-alert-block qui ajoute un bloc “Encart Alerte” avec :
- un titre + contenu,
- un niveau (info/succès/attention/erreur),
- option “icône” (oui/non),
- rendu front en PHP (dynamic block) pour garantir la cohérence et permettre des évolutions.
Étape 1 — Créer la structure du plugin
Dans wp-content/plugins/ :
mkdir -p bpca-alert-block/src bpca-alert-block/build bpca-alert-block/assets
Créez le fichier principal :
<?php
/**
* Plugin Name: BPCA Alert Block
* Description: Bloc Gutenberg "Encart Alerte" (dynamic block) avec build via @wordpress/scripts.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
* Author: BPCA
* License: GPL-2.0-or-later
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'BPCA_ALERT_BLOCK_FILE', __FILE__ );
define( 'BPCA_ALERT_BLOCK_DIR', __DIR__ );
require_once BPCA_ALERT_BLOCK_DIR . '/includes/class-bpca-alert-block.php';
add_action( 'init', array( 'BPCA\AlertBlock\Plugin', 'init' ) );
Créez includes/class-bpca-alert-block.php :
<?php
namespace BPCAAlertBlock;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class Plugin {
public static function init(): void {
// Enregistre le bloc via block.json + render_callback.
add_action( 'init', array( __CLASS__, 'register_block' ) );
}
public static function register_block(): void {
$block_json = BPCA_ALERT_BLOCK_DIR . '/block.json';
// register_block_type() sait lire block.json et enregistrer les assets déclarés.
register_block_type(
$block_json,
array(
'render_callback' => array( __CLASS__, 'render' ),
)
);
}
/**
* Rendu serveur (dynamic block).
*
* @param array $attributes Attributs du bloc.
* @param string $content Contenu interne (InnerBlocks), non utilisé ici.
* @return string HTML rendu.
*/
public static function render( array $attributes, string $content ): string {
$level = isset( $attributes['level'] ) ? sanitize_key( $attributes['level'] ) : 'info';
$title = isset( $attributes['title'] ) ? sanitize_text_field( $attributes['title'] ) : '';
$message = isset( $attributes['message'] ) ? wp_kses_post( $attributes['message'] ) : '';
$icon = ! empty( $attributes['showIcon'] );
$allowed_levels = array( 'info', 'success', 'warning', 'error' );
if ( ! in_array( $level, $allowed_levels, true ) ) {
$level = 'info';
}
$classes = array(
'bpca-alert',
'bpca-alert--' . $level,
);
$icon_html = '';
if ( $icon ) {
// Icônes simples en SVG inline (pas de dépendance externe).
$icon_html = '<span class="bpca-alert__icon" aria-hidden="true">' . self::get_icon_svg( $level ) . '</span>';
}
$title_html = '';
if ( $title !== '' ) {
$title_html = '<div class="bpca-alert__title">' . esc_html( $title ) . '</div>';
}
// Note : $message est déjà filtré via wp_kses_post() mais on l'échappe en contexte HTML.
$message_html = '';
if ( $message !== '' ) {
$message_html = '<div class="bpca-alert__message">' . $message . '</div>';
}
$html = '<div class="' . esc_attr( implode( ' ', $classes ) ) . '" role="note">';
$html .= $icon_html;
$html .= '<div class="bpca-alert__body">' . $title_html . $message_html . '</div>';
$html .= '</div>';
return $html;
}
private static function get_icon_svg( string $level ): string {
// SVG minimalistes. Vous pouvez les remplacer par vos propres assets.
switch ( $level ) {
case 'success':
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z"></path></svg>';
case 'warning':
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M1 21h22L12 2 1 21zm12-3h-2v2h2v-2zm0-8h-2v6h2V10z"></path></svg>';
case 'error':
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></svg>';
case 'info':
default:
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M11 17h2v-6h-2v6zm0-8h2V7h-2v2zm1-7C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z"></path></svg>';
}
}
}
Étape 2 — Ajouter block.json (métadonnées + assets)
Créez block.json à la racine du plugin :
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "bpca/alert",
"title": "Encart Alerte (BPCA)",
"category": "widgets",
"icon": "warning",
"description": "Affiche un encart d'alerte avec niveau et option d'icône.",
"textdomain": "bpca-alert-block",
"attributes": {
"level": { "type": "string", "default": "info" },
"title": { "type": "string", "default": "" },
"message": { "type": "string", "default": "" },
"showIcon": { "type": "boolean", "default": true }
},
"supports": {
"anchor": true,
"html": false
},
"editorScript": "file:./build/index.js",
"style": "file:./build/style-index.css",
"editorStyle": "file:./build/index.css"
}
Notes pratiques :
apiVersion: 3est la base actuelle pour les blocs modernes.- Le champ
file:déclenche l’enregistrement automatique des assets, avec versions, via le.asset.phpgénéré. supports.html: falseévite que l’utilisateur “édite en HTML” et casse la structure.
Étape 3 — Installer @wordpress/scripts et configurer package.json
Dans le dossier du plugin :
npm init -y
npm install --save-dev @wordpress/scripts
Remplacez package.json (ou adaptez) :
{
"name": "bpca-alert-block",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "wp-scripts start",
"build": "wp-scripts build",
"lint:js": "wp-scripts lint-js",
"format": "wp-scripts format"
},
"devDependencies": {
"@wordpress/scripts": "^30.0.0"
}
}
Référence : @wordpress/scripts.
Étape 4 — Écrire le code du bloc (éditeur) dans src/
Créez src/index.js :
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import {
InspectorControls,
useBlockProps,
RichText,
} from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
ToggleControl,
TextControl,
} from '@wordpress/components';
import './editor.css';
import './style.css';
registerBlockType( 'bpca/alert', {
edit: ( { attributes, setAttributes } ) => {
const { level, title, message, showIcon } = attributes;
const blockProps = useBlockProps( {
className: `bpca-alert bpca-alert--${ level }`,
} );
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Réglages', 'bpca-alert-block' ) }>
<SelectControl
label={ __( 'Niveau', 'bpca-alert-block' ) }
value={ level }
options={ [
{ label: __( 'Info', 'bpca-alert-block' ), value: 'info' },
{ label: __( 'Succès', 'bpca-alert-block' ), value: 'success' },
{ label: __( 'Attention', 'bpca-alert-block' ), value: 'warning' },
{ label: __( 'Erreur', 'bpca-alert-block' ), value: 'error' },
] }
onChange={ ( next ) => setAttributes( { level: next } ) }
/>
<ToggleControl
label={ __( 'Afficher une icône', 'bpca-alert-block' ) }
checked={ !! showIcon }
onChange={ ( next ) => setAttributes( { showIcon: !! next } ) }
/>
<TextControl
label={ __( 'Titre (optionnel)', 'bpca-alert-block' ) }
value={ title }
onChange={ ( next ) => setAttributes( { title: next } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
{ showIcon && (
<span className="bpca-alert__icon" aria-hidden="true">
<span className="bpca-alert__icon-placeholder">!</span>
</span>
) }
<div className="bpca-alert__body">
{ title ? (
<div className="bpca-alert__title">{ title }</div>
) : null }
<RichText
tagName="div"
className="bpca-alert__message"
value={ message }
allowedFormats={ [ 'core/bold', 'core/italic', 'core/link' ] }
placeholder={ __( 'Votre message…', 'bpca-alert-block' ) }
onChange={ ( next ) => setAttributes( { message: next } ) }
/>
</div>
</div>
</>
);
},
// Dynamic block : save() doit retourner null.
save: () => null,
} );
Deux fichiers CSS :
/* src/style.css - CSS front + éditeur (partagé) */
.bpca-alert{
display:flex;
gap:12px;
padding:14px 16px;
border-radius:10px;
border:1px solid transparent;
align-items:flex-start;
}
.bpca-alert__icon svg{ display:block; fill: currentColor; }
.bpca-alert__title{ font-weight: 650; margin-bottom: 6px; }
.bpca-alert__message a{ text-decoration: underline; }
.bpca-alert--info{ background:#eef6ff; border-color:#cfe6ff; color:#0b3d91; }
.bpca-alert--success{ background:#eafff1; border-color:#c9f2d7; color:#0b5d2a; }
.bpca-alert--warning{ background:#fff7e6; border-color:#ffe3a3; color:#7a4a00; }
.bpca-alert--error{ background:#ffecec; border-color:#ffc2c2; color:#7a0000; }
/* src/editor.css - uniquement éditeur */
.wp-block-bpca-alert .bpca-alert__icon-placeholder{
display:inline-flex;
width:20px;
height:20px;
border-radius:4px;
align-items:center;
justify-content:center;
background: rgba(0,0,0,.08);
font-weight: 700;
}
Étape 5 — Builder
Lancez :
npm run build
Vous devez obtenir :
build/index.jsbuild/index.asset.php(critique)build/index.cssbuild/style-index.css
Étape 6 — Activer le plugin et tester
- Activez le plugin dans l’admin.
- Dans l’éditeur de blocs, cherchez “Encart Alerte (BPCA)”.
- Ajoutez-le, changez le niveau, testez avec/sans icône, publiez.
Code complet
Ce qui suit est un copier-coller fonctionnel (plugin complet). Remarquez que le build (build/) n’est pas inclus ici : vous devez exécuter npm run build pour générer les fichiers.
1) bpca-alert-block.php
<?php
/**
* Plugin Name: BPCA Alert Block
* Description: Bloc Gutenberg "Encart Alerte" (dynamic block) avec build via @wordpress/scripts.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
* Author: BPCA
* License: GPL-2.0-or-later
* Text Domain: bpca-alert-block
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'BPCA_ALERT_BLOCK_FILE', __FILE__ );
define( 'BPCA_ALERT_BLOCK_DIR', __DIR__ );
require_once BPCA_ALERT_BLOCK_DIR . '/includes/class-bpca-alert-block.php';
add_action( 'init', array( 'BPCA\AlertBlock\Plugin', 'init' ) );
2) includes/class-bpca-alert-block.php
<?php
namespace BPCAAlertBlock;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class Plugin {
public static function init(): void {
add_action( 'init', array( __CLASS__, 'register_block' ) );
}
public static function register_block(): void {
register_block_type(
BPCA_ALERT_BLOCK_DIR . '/block.json',
array(
'render_callback' => array( __CLASS__, 'render' ),
)
);
}
public static function render( array $attributes, string $content ): string {
$level = isset( $attributes['level'] ) ? sanitize_key( $attributes['level'] ) : 'info';
$title = isset( $attributes['title'] ) ? sanitize_text_field( $attributes['title'] ) : '';
$message = isset( $attributes['message'] ) ? wp_kses_post( $attributes['message'] ) : '';
$icon = ! empty( $attributes['showIcon'] );
$allowed_levels = array( 'info', 'success', 'warning', 'error' );
if ( ! in_array( $level, $allowed_levels, true ) ) {
$level = 'info';
}
$classes = array( 'bpca-alert', 'bpca-alert--' . $level );
$icon_html = '';
if ( $icon ) {
$icon_html = '<span class="bpca-alert__icon" aria-hidden="true">' . self::get_icon_svg( $level ) . '</span>';
}
$title_html = $title !== '' ? '<div class="bpca-alert__title">' . esc_html( $title ) . '</div>' : '';
$message_html = $message !== '' ? '<div class="bpca-alert__message">' . $message . '</div>' : '';
return '<div class="' . esc_attr( implode( ' ', $classes ) ) . '" role="note">'
. $icon_html
. '<div class="bpca-alert__body">' . $title_html . $message_html . '</div>'
. '</div>';
}
private static function get_icon_svg( string $level ): string {
switch ( $level ) {
case 'success':
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z"></path></svg>';
case 'warning':
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M1 21h22L12 2 1 21zm12-3h-2v2h2v-2zm0-8h-2v6h2V10z"></path></svg>';
case 'error':
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></svg>';
case 'info':
default:
return '<svg viewBox="0 0 24 24" width="20" height="20" focusable="false"><path d="M11 17h2v-6h-2v6zm0-8h2V7h-2v2zm1-7C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z"></path></svg>';
}
}
}
3) block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "bpca/alert",
"title": "Encart Alerte (BPCA)",
"category": "widgets",
"icon": "warning",
"description": "Affiche un encart d'alerte avec niveau et option d'icône.",
"textdomain": "bpca-alert-block",
"attributes": {
"level": { "type": "string", "default": "info" },
"title": { "type": "string", "default": "" },
"message": { "type": "string", "default": "" },
"showIcon": { "type": "boolean", "default": true }
},
"supports": {
"anchor": true,
"html": false
},
"editorScript": "file:./build/index.js",
"style": "file:./build/style-index.css",
"editorStyle": "file:./build/index.css"
}
4) src/index.js + src/style.css + src/editor.css
Reprenez les fichiers de la section pas à pas.
Explication du code
Pourquoi block.json simplifie vraiment la vie
block.json est devenu le point central. WordPress lit les métadonnées, enregistre le bloc, et sait comment charger les assets au bon endroit (éditeur vs front). Quand vous utilisez "file:./build/index.js", WordPress s’appuie sur le fichier build/index.asset.php généré par le build pour :
- déclarer les dépendances exactes (ex :
wp-element,wp-i18n,wp-block-editor), - déclarer la version (hash) pour le cache-busting.
Référence officielle : Block Metadata.
Pourquoi un dynamic block (save: null) évite des bugs réels
Avec un bloc statique, vous devez maintenir deux rendus :
- le rendu JSX dans
edit(), - le HTML sérialisé par
save().
Dans la pratique, dès que vous ajoutez une option, vous oubliez de mettre à jour save(), et vous vous retrouvez avec du contenu “ancien format” en base. En dynamic block, le front passe par PHP, donc :
- vous pouvez faire évoluer le HTML sans migration de contenu,
- vous centralisez l’échappement et la sanitation,
- vous gérez mieux les contextes (site multilingue, options globales, A/B test, etc.).
La contrepartie : le rendu dépend du serveur, donc vous devez être strict sur la performance et le cache (on y revient).
Hooks et timing
On enregistre le bloc sur init. C’est le timing attendu : les types de blocs doivent être enregistrés suffisamment tôt, mais après le chargement du cœur. Référence : register_block_type().
Sanitization et escaping
sanitize_key()pourlevel(valeur “slug”).sanitize_text_field()pourtitle.wp_kses_post()pourmessagecar on autorise un sous-ensemble HTML (liens, emphase). Doc : wp_kses_post().esc_attr()sur la classe CSS,esc_html()sur le titre.
Sur ce type de bloc, le risque principal est l’injection HTML si vous faites confiance aux attributs. Même si l’éditeur est “admin”, vous ne voulez pas d’un XSS persistant si un rôle inférieur peut publier.
Variantes et cas d’usage
Variante 1 — Bloc statique (si vous tenez au HTML sérialisé)
Cas : vous voulez que le contenu soit 100% portable sans dépendre du plugin (ex : export vers un autre site sans le plugin). Vous pouvez implémenter save() et supprimer render_callback.
Ce que je fais dans ce cas : je garde un HTML minimal en save() et je limite les options. Sinon, vous allez gérer des migrations de version de bloc plus tôt que prévu.
Variante 2 — Ajouter des styles de bloc (variations de style) sans complexifier l’UI
Vous pouvez déclarer des styles via JS (registerBlockStyle) ou via block.json selon votre stratégie. Pour des blogueurs avancés, c’est souvent plus simple de laisser l’utilisateur choisir “Contour” vs “Rempli” sans ajouter un toggle en Inspector.
Doc : Block Styles.
Variante 3 — Rendu dépendant d’une option globale (Settings API)
Exemple : vous voulez une option “Couleurs de marque” configurée une fois. Dans render(), récupérez une option et ajustez les classes ou un style inline (en restant sobre). Attention : si vous commencez à générer du CSS inline par bloc, vous pouvez exploser la taille HTML sur des pages longues.
Compatibilité Divi 5 / Elementor / Avada
Point clé : Divi 5, Elementor et Avada peuvent coexister avec l’éditeur de blocs, mais le contenu peut être produit via leurs builders. Votre bloc Gutenberg restera utilisable :
- dans l’éditeur de blocs natif,
- dans des zones “Gutenberg”/“Block Editor” que ces thèmes/plugins exposent,
- parfois dans des modules dédiés (selon le builder).
Divi 5
Divi 5 a une meilleure interop avec les blocs, mais j’ai encore vu des problèmes de CSS global Divi qui écrase des styles de blocs (line-height, box-sizing). Votre bloc est robuste si :
- vous préfixez vos classes (
bpca-alert), - vous évitez des sélecteurs trop génériques.
Si vous voulez l’intégrer en “module Divi”, vous pouvez proposer un shortcode (fallback) qui réutilise la même fonction de rendu, mais ne dupliquez pas la logique. Gardez une source de vérité (PHP).
Elementor
Elementor permet d’insérer des shortcodes et, selon la configuration, des blocs via des widgets “WordPress”. Pour un usage avancé :
- proposez un shortcode
[bpca_alert]qui appelle la même méthode de rendu, - ou exposez un widget Elementor dédié (plus long à maintenir).
Je recommande le shortcode comme passerelle légère si votre audience est mixte.
Avada (Fusion Builder)
Avada a historiquement beaucoup de CSS global. Testez particulièrement :
- les marges par défaut sur
div, - les styles de liens,
- les couleurs héritées.
En cas de conflit, ajoutez une couche CSS plus spécifique dans src/style.css (sans tomber dans la guerre du !important).
Vérifications après mise en place
- Éditeur : le bloc apparaît, les contrôles changent bien l’aperçu.
- Front : le rendu HTML contient bien
bpca-alertetbpca-alert--{level}. - Assets :
- dans l’éditeur :
build/index.js+build/index.csschargés, - sur le front :
build/style-index.csschargé uniquement si le bloc est présent.
- dans l’éditeur :
- Cache : après
npm run build, purge cache plugin/CDN + hard refresh navigateur. - Logs : pas de “failed to load resource” sur les CSS/JS du bloc.
Tableau de diagnostic rapide
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Le bloc n’apparaît pas dans l’inserter | Plugin inactif ou erreur PHP au chargement | Admin > Extensions, + debug.log |
Corriger l’erreur, vérifier namespace/fichier inclus |
| Bloc visible mais “vide” en front | Dynamic block sans render_callback effectif, ou erreur dans render() |
Voir source HTML + logs PHP | Vérifier register_block_type(... render_callback ...) et sanitation |
| CSS absent en front | style mal déclaré dans block.json ou build manquant |
Onglet Network, fichier style-index.css |
Relancer npm run build, vérifier chemins file: |
| Erreur JS dans l’éditeur | Dépendances non résolues / build cassé | Console navigateur dans l’éditeur | Supprimer build/, relancer build, vérifier index.asset.php |
| Changements non visibles après build | Cache navigateur/CDN | Comparer hash des assets | Purge cache + hard reload |
Si ça ne marche pas
1) Vérifiez d’abord le build
build/index.asset.phpexiste ? S’il manque, WordPress ne saura pas gérer les dépendances.- Vous avez bien exécuté
npm run builddans le dossier du plugin (pas à la racine du projet) ?
2) Vérifiez les chemins
Le champ "file:./build/index.js" est relatif au block.json. Si vous avez déplacé block.json dans un sous-dossier, tout casse. C’est une erreur fréquente quand on réorganise un repo.
3) Vérifiez le hook
Enregistrement sur init. Si vous enregistrez trop tôt (ex : au chargement du fichier sans hook), vous pouvez tomber sur des fonctions pas prêtes selon l’ordre de chargement.
4) Vérifiez la version PHP
Je vois encore des hébergements où le site tourne en PHP 7.4/8.0 alors que le plugin est écrit pour 8.1. Résultat : erreurs fatales. Vérifiez dans Outils > Santé du site, ou via phpinfo().
5) Conflits plugins/snippets
Un “plugin de snippets” peut casser un fichier si vous collez du code avec une parenthèse manquante. Si votre site plante après activation :
- désactivez le plugin via FTP (renommez le dossier),
- corrigez l’erreur dans
debug.log, - réactivez.
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
Copier le code PHP dans functions.php au lieu d’un plugin |
Le thème change, le bloc disparaît | Emballez en plugin, versionnez, déployez proprement |
Parse error: syntax error, unexpected ... |
Point-virgule/parenthèse manquante | Relire le diff, activer un linter PHP, vérifier debug.log |
| Le bloc apparaît mais les contrôles ne répondent pas | Attributs mal déclarés ou typo dans setAttributes |
Vérifier block.json vs attributes utilisés en JS |
Failed to load resource sur index.js |
Build absent, chemin faux, ou fichier non déployé | Déployer build/ en prod, vérifier permissions |
| CSS du bloc non chargé | Cache, ou style/editorStyle inversés |
Purger cache, vérifier block.json, tester en navigation privée |
| Confusion actions/filtres | Utilisation de add_filter au lieu de add_action sur init |
Utiliser add_action( 'init', ... ) |
| Bloc “cassé” après mise à jour | Dépendances hardcodées au lieu de .asset.php |
Passer par file: + build standard |
| Test direct sur production sans sauvegarde | Risque de fatal error et downtime | Staging + sauvegarde + déploiement (zip/release) contrôlé |
| Permaliens “à régénérer” (effet secondaire) | Rare, mais certains plugins modifient rewrite à l’activation | Réenregistrer les permaliens si comportement étrange post-activation |
Conseils sécurité, performance et maintenance
Sécurité
- Traitez tous les attributs comme non fiables. Même si l’UI est dans l’admin, le contenu peut être injecté via REST ou import.
- Si vous ajoutez des endpoints REST pour votre bloc, utilisez permissions_callback, nonces, et capacités adaptées.
- Évitez de rendre du HTML brut depuis des attributs sans
wp_kses_*.
Performance
- Dynamic block : gardez
render()rapide, sans requêtes lourdes par bloc. - Si vous devez requêter (ex : CPT), mettez en cache (object cache) et évitez N+1.
- Gardez le CSS minimal. Les blocs se répètent : 20 alertes sur une page = votre CSS doit rester constant.
Maintenance (déploiement réel)
- Ne build pas en production. Build en CI, livrez le plugin avec
build/versionné (ou attaché à une release). - Verrouillez la version Node en dev (nvm, volta) pour éviter des builds différents selon machine.
- Surveillez les évolutions dans Gutenberg : le meilleur signal reste les PR sur github.com/WordPress/gutenberg.
Ressources
- @wordpress/scripts (doc officielle)
- block.json / Block Metadata
- register_block_type()
- wp_kses_post()
- Styles de blocs
- Repo Gutenberg (suivi des changements)
- WordPress Core Trac (tickets, régressions)
- Versions supportées de PHP
FAQ
Pourquoi utiliser @wordpress/scripts plutôt que mon Webpack maison ?
Parce que vous réduisez la surface de maintenance. @wordpress/scripts colle aux conventions WordPress (Babel, dépendances WP, génération .asset.php). Un Webpack maison finit souvent par diverger et casser au prochain saut majeur.
Dois-je commit le dossier build/ dans Git ?
Pour un plugin déployé sur un site, oui, vous devez livrer build/. Que vous le commitiez ou que vous l’attachiez à une release CI, peu importe, mais la prod ne doit pas dépendre d’un build Node.
Pourquoi mon bloc ne charge pas ses styles sur le front ?
Dans 80% des cas : style mal déclaré dans block.json, build non déployé, ou cache. Vérifiez que build/style-index.css existe et est accessible.
Dynamic block : est-ce mauvais pour le cache ?
Pas forcément. Le HTML est rendu au moment de la génération de page, puis mis en cache comme le reste. Le vrai problème, c’est si votre render() fait des requêtes lourdes non cachées, ou dépend d’un état utilisateur (dans ce cas, vous fragmentez le cache).
Puis-je utiliser InnerBlocks avec cette approche ?
Oui. Vous devrez : (1) déclarer supports.inserter selon votre besoin, (2) gérer $content dans render() et l’échapper correctement (souvent via do_blocks() si vous manipulez du contenu bloc). Faites-le seulement si vous avez un vrai besoin, sinon vous multipliez les cas limites.
Comment gérer l’internationalisation (i18n) proprement ?
Côté JS, utilisez __() et définissez textdomain dans block.json. Côté PHP, utilisez __()/esc_html__(). Ensuite, générez vos fichiers .po/.mo via votre chaîne habituelle. Doc i18n bloc : Internationalization in the Block Editor.
Mon éditeur plante avec “Cannot read properties of undefined”
Typiquement : un attribut absent (mauvais nom) ou un type inattendu. Vérifiez la cohérence block.json ↔ JS. En debug, loggez attributes dans edit() et regardez l’état réel.
Peut-on ajouter une API REST pour alimenter le bloc ?
Oui, mais sécurisez. Utilisez register_rest_route() avec permission_callback, validez les paramètres, et gérez les capacités. Doc : Adding custom REST API endpoints.
Comment tester ce code proprement ?
Je teste en trois passes :
- Unit/Integration : au minimum, test PHP du rendu (
render()) avec des attributs invalides (level inconnu, HTML dans title, etc.). - E2E : créer un article, insérer le bloc, publier, vérifier HTML + CSS.
- Compat : activer un thème “lourd” (Avada/Divi) sur un staging et vérifier que vos classes ne se font pas écraser.
Où suivre les changements qui peuvent impacter mes blocs ?
Sur le repo Gutenberg (GitHub) et sur Core Trac (Trac) quand une modification est mergée vers WordPress core. C’est là que vous verrez passer les dépréciations et changements d’API.