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.json et 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.json et register_block_type().
  • On utilise @wordpress/scripts pour builder src/index.js vers build/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, editorStyle du block.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/scripts standardise 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_DEBUG et WP_DEBUG_LOG sur 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.php pré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: 3 est la base actuelle pour les blocs modernes.
  • Le champ file: déclenche l’enregistrement automatique des assets, avec versions, via le .asset.php gé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.js
  • build/index.asset.php (critique)
  • build/index.css
  • build/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() pour level (valeur “slug”).
  • sanitize_text_field() pour title.
  • wp_kses_post() pour message car 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-alert et bpca-alert--{level}.
  • Assets :
    • dans l’éditeur : build/index.js + build/index.css chargés,
    • sur le front : build/style-index.css chargé uniquement si le bloc est présent.
  • 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.php existe ? S’il manque, WordPress ne saura pas gérer les dépendances.
  • Vous avez bien exécuté npm run build dans 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

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.