Si vous avez déjà vu un bloc « introuvable » après une mise en cache agressive, ou un script handle not found parce que votre build a changé de hash, vous avez touché du doigt le vrai sujet : un bloc moderne, en WordPress 6.9.4, se pilote d’abord par block.json et par l’API Block, pas par une pile de wp_enqueue_script() dispersés.

Le problème / Le besoin

Vous voulez enregistrer des blocs Gutenberg « modernes » (édition et front), avec des assets correctement déclarés, versionnés, traduisibles, et surtout maintenables. Vous voulez aussi éviter les vieilles recettes qui collent tout dans functions.php, cassent au premier refactor, et rendent le débogage pénible dès que vous avez deux environnements (dev/prod) et un plugin de cache.

Ce contenu s’adresse à des blogueurs avancés (et à leurs devs) qui publient régulièrement, qui ont des exigences de performance, et qui veulent pouvoir faire évoluer leurs blocs sans peur de casser les contenus existants.

À la fin, vous saurez :

  • définir un bloc via block.json (métadonnées, attributs, supports, styles, variations),
  • enregistrer le bloc côté PHP avec register_block_type() en pointant vers le dossier,
  • charger les scripts/styles via wp_register_script()/wp_register_style() + dépendances fiables (sans « deviner »),
  • rendre le bloc en dynamic render (PHP) avec une hygiène sécurité correcte (sanitization/escaping),
  • tester et dépanner les cas réels (cache, mauvais hook, build manquant, PHP trop vieux, etc.).

Résumé rapide

  • On crée un plugin (recommandé) qui expose un bloc via un dossier build/ contenant block.json + assets compilés.
  • On enregistre le bloc avec register_block_type( __DIR__ . '/build/mon-bloc' ), ce qui lit block.json.
  • On déclare explicitement les handles JS/CSS et leurs dépendances (idéalement via le fichier .asset.php généré par @wordpress/scripts).
  • On fait un bloc dynamique (PHP) pour un rendu robuste (SEO, performance, compatibilité thèmes), tout en gardant une preview correcte dans l’éditeur.
  • On ajoute une variante « simple » (bloc statique) et une variante « avancée » (rendu serveur + cache + variations).

Quand utiliser cette solution

  • Vous avez besoin d’un bloc réutilisable et versionnable (ex : encart newsletter, avis, encadré “à retenir”, sommaire, CTA).
  • Vous voulez un rendu front contrôlé côté serveur (ex : requêtes, personnalisation par contexte, A/B test, données externes).
  • Vous travaillez en équipe et vous voulez une source de vérité claire (block.json) plutôt que du code éclaté.
  • Vous devez gérer des dépendances JS WordPress (packages @wordpress/*) sans les casser à chaque mise à jour.
  • Vous ciblez WordPress 6.9.4+ et PHP 8.1+ (ce qui permet un code plus propre, typage léger, et un outillage moderne).

Quand ne PAS utiliser cette solution

  • Vous voulez juste une mise en forme simple et stable : un bloc natif (Core) + styles globaux + patterns suffit souvent. Regardez les Block Patterns et Style variations avant d’écrire du JS.
  • Vous êtes sur un site 100% Elementor/Divi/Avada et l’éditeur de blocs n’est pas utilisé pour le contenu. Dans ce cas, un widget/module du builder sera plus cohérent pour les équipes éditoriales.
  • Vous ne pouvez pas mettre en place une chaîne de build (ou vous refusez Node) : vous pouvez faire un bloc très simple « sans build », mais vous perdez vite en DX et robustesse.
  • Votre besoin relève d’un shortcode historique déjà partout. Migrer vers un bloc est faisable, mais demande une stratégie (dépréciation, conversion, tests de contenu).

Prérequis / avant de commencer

Je pars du principe que vous êtes sur WordPress 6.9.4 (avril 2026) et PHP 8.1+. Si vous êtes en dessous, vous allez cumuler les faux problèmes (erreurs fatales, dépendances JS incohérentes, build non reproductible).

  • Environnement : un site de staging + sauvegarde (base + wp-content), pas de tests directs en prod.
  • Outils : Node.js LTS (pour compiler), npm/pnpm, et idéalement @wordpress/scripts.
  • Connaissances : hooks WordPress, base de REST/nonce, et notions de build (webpack/vite via scripts WP).

Docs officielles utiles :

L’approche naïve (et pourquoi l’éviter)

Le classique que je vois encore en 2026 : un functions.php (ou un plugin de snippets) qui enregistre un script global et tente de « créer un bloc » à coups de JS inline, sans block.json, sans fichier .asset.php, sans gestion propre des dépendances.

<?php
// Exemple volontairement naïf : à éviter.
add_action('init', function () {
    wp_enqueue_script(
        'mon-bloc-js',
        get_stylesheet_directory_uri() . '/js/mon-bloc.js',
        array('wp-blocks', 'wp-element', 'wp-editor'),
        '1.0.0',
        true
    );
});

Pourquoi ça pose problème, concrètement :

  • Mauvais hook : wp_enqueue_script() dans init ne cible pas l’éditeur correctement (et peut polluer le front). Vous voulez enqueue_block_editor_assets ou, mieux, déclarer via block.json + registration.
  • Dépendances fragiles : vous « devinez » les handles. Avec les évolutions du Block Editor, vous finissez avec des erreurs type wp is not defined ou des packages chargés deux fois.
  • Cache & versions : sans versionnement basé sur le build, vous servez du JS obsolète (le bug le plus courant sur sites avec Cloudflare + plugin cache).
  • Maintenance : impossible de comprendre le bloc sans fouiller plusieurs fichiers et hooks.
  • Sécurité : quand le rendu est fait côté JS uniquement, beaucoup finissent par injecter des données non échappées dans le front.

La bonne approche — tutoriel pas à pas

Étape 1 — Créer un plugin dédié (plutôt qu’un thème)

Un bloc est une fonctionnalité. Dans mon expérience, le mettre dans un plugin évite de perdre les contenus lors d’un changement de thème.

# wp-content/plugins/bpcab-blocs/
mkdir -p wp-content/plugins/bpcab-blocs
<?php
/**
 * Plugin Name: BPCAB Blocs
 * Description: Blocs personnalisés modernes via block.json (WP 6.9.4+).
 * Version: 1.0.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 * Author: Votre Nom
 */

defined('ABSPATH') || exit;

require_once __DIR__ . '/src/Plugin.php';

add_action('plugins_loaded', static function () {
    BPCABBlocksPlugin::instance()->boot();
});

Étape 2 — Ajouter un mini “service container” (propre et testable)

Vous n’êtes pas obligé de faire du DI, mais dès que vous avez 2-3 blocs, ça paie. Ça évite aussi les collisions de fonctions globales.

<?php
// wp-content/plugins/bpcab-blocs/src/Plugin.php

namespace BPCABBlocks;

defined('ABSPATH') || exit;

final class Plugin {
    private static ?self $instance = null;

    /** @var array<string, object> */
    private array $services = [];

    public static function instance(): self {
        return self::$instance ??= new self();
    }

    public function boot(): void {
        $this->register_services();
        $this->init_hooks();
    }

    private function register_services(): void {
        $this->services['blocks'] = new ServiceBlocksRegistry(__DIR__ . '/../build');
    }

    private function init_hooks(): void {
        add_action('init', [$this->services['blocks'], 'register_blocks']);
    }
}

Étape 3 — Créer le dossier du bloc et son block.json

On va créer un bloc “Encart Note” (statique côté éditeur, rendu dynamique côté front). Le block.json est la source de vérité.

mkdir -p wp-content/plugins/bpcab-blocs/build/note-box
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "bpcab/note-box",
  "version": "1.0.0",
  "title": "Encart Note (BPCAB)",
  "category": "text",
  "icon": "info-outline",
  "description": "Encart éditorial avec titre, contenu et style.",
  "textdomain": "bpcab-blocs",
  "attributes": {
    "title": {
      "type": "string",
      "default": "À retenir"
    },
    "tone": {
      "type": "string",
      "default": "info"
    }
  },
  "supports": {
    "anchor": true,
    "align": ["wide", "full"],
    "html": false,
    "spacing": {
      "margin": true,
      "padding": true
    },
    "color": {
      "background": true,
      "text": true
    },
    "typography": {
      "fontSize": true,
      "lineHeight": true
    }
  },
  "editorScript": "bpcab-note-box-editor",
  "style": "bpcab-note-box-style",
  "render": "file:./render.php"
}

Notes techniques :

  • apiVersion: 3 est la base pour les blocs modernes (et cohérent avec WP 6.9.x).
  • render pointe vers un fichier PHP : c’est un bloc dynamique. Très utile pour garder un HTML front stable, même si l’éditeur change.
  • editorScript et style référencent des handles WordPress. On va les déclarer proprement.

Étape 4 — Déclarer les assets avec .asset.php (dépendances fiables)

Le pattern robuste, c’est : build JS → génère un fichier index.asset.php qui contient dependencies + version. C’est exactement ce qui évite les “handles devinés”.

Pour rester 100% copiable-collable ici, je vais vous montrer le côté PHP qui utilise ce fichier. (Vous pouvez générer ce build via @wordpress/scripts.)

Étape 5 — Enregistrer le bloc via register_block_type() en pointant vers le dossier

On crée un registre qui scanne un dossier build/ et enregistre chaque bloc qui contient un block.json. C’est un pattern que j’utilise souvent sur des sites multi-blocs.

<?php
// wp-content/plugins/bpcab-blocs/src/Service/BlocksRegistry.php

namespace BPCABBlocksService;

use WP_Error;

defined('ABSPATH') || exit;

final class BlocksRegistry {
    public function __construct(
        private readonly string $build_dir
    ) {}

    public function register_blocks(): void {
        if (!function_exists('register_block_type')) {
            // Sécurité : si l’API n’est pas disponible (cas rarissime), on stoppe.
            return;
        }

        $block_dirs = glob($this->build_dir . '/*', GLOB_ONLYDIR) ?: [];

        foreach ($block_dirs as $dir) {
            $block_json = $dir . '/block.json';
            if (!is_readable($block_json)) {
                continue;
            }

            // 1) Enregistrer les assets référencés par block.json (handles)
            $this->register_assets_for_block_dir($dir);

            // 2) Enregistrer le bloc à partir de son dossier (lit block.json)
            $result = register_block_type($dir);

            if ($result === false) {
                // En prod, évitez de spammer les logs. Ici, c’est volontairement visible.
                error_log('[BPCAB] Échec register_block_type pour: ' . $dir);
            }
        }
    }

    private function register_assets_for_block_dir(string $dir): void {
        // Convention : build/note-box/index.js + index.asset.php + style.css
        $slug = basename($dir);

        $editor_handle = 'bpcab-' . $slug . '-editor';
        $style_handle  = 'bpcab-' . $slug . '-style';

        $editor_js     = $dir . '/index.js';
        $editor_asset  = $dir . '/index.asset.php';
        $style_css     = $dir . '/style.css';

        if (is_readable($editor_js) && is_readable($editor_asset)) {
            $asset = require $editor_asset;

            $deps = isset($asset['dependencies']) && is_array($asset['dependencies'])
                ? $asset['dependencies']
                : [];

            $ver = isset($asset['version']) && is_string($asset['version'])
                ? $asset['version']
                : filemtime($editor_js);

            wp_register_script(
                $editor_handle,
                plugins_url('build/' . $slug . '/index.js', dirname(__DIR__, 2) . '/bpcab-blocs.php'),
                $deps,
                $ver,
                true
            );

            // Optionnel : traductions JS si vous utilisez i18n côté JS.
            // wp_set_script_translations($editor_handle, 'bpcab-blocs', plugin_dir_path(...). 'languages');
        }

        if (is_readable($style_css)) {
            wp_register_style(
                $style_handle,
                plugins_url('build/' . $slug . '/style.css', dirname(__DIR__, 2) . '/bpcab-blocs.php'),
                [],
                filemtime($style_css)
            );
        }
    }
}

Oui, le plugins_url() avec un chemin “ancre” est pénible. Je le garde explicite parce que c’est un des bugs que je vois le plus : des URLs d’assets qui pointent vers le mauvais plugin après un refactor. Alternative : définir une constante BPCAB_BLOCKS_FILE dans le fichier principal.

Étape 6 — Écrire le rendu serveur (render.php) avec hygiène sécurité

Le rendu serveur doit traiter les attributs comme des entrées utilisateur. Même si l’éditeur vous semble « sûr », un attribut peut être injecté via REST ou import de contenu.

<?php
// wp-content/plugins/bpcab-blocs/build/note-box/render.php

defined('ABSPATH') || exit;

/**
 * Rendu serveur du bloc bpcab/note-box.
 *
 * @param array $attributes Attributs du bloc (non fiables).
 * @param string $content Contenu interne (si InnerBlocks).
 * @param WP_Block $block Instance du bloc.
 */
return function (array $attributes, string $content, $block): string {
    $title = isset($attributes['title']) ? sanitize_text_field($attributes['title']) : 'À retenir';
    $tone  = isset($attributes['tone']) ? sanitize_key($attributes['tone']) : 'info';

    $allowed_tones = ['info', 'warning', 'success'];
    if (!in_array($tone, $allowed_tones, true)) {
        $tone = 'info';
    }

    // Récupère des classes générées par WordPress (align, spacing, colors, typography, etc.)
    $wrapper_attributes = get_block_wrapper_attributes([
        'class' => 'bpcab-note-box bpcab-note-box--' . $tone,
    ]);

    ob_start();
    ?>
    <div <?php echo $wrapper_attributes; ?>>
        <div class="bpcab-note-box__title"><?php echo esc_html($title); ?></div>
        <div class="bpcab-note-box__content">
            <?php
            // Ici, on autorise seulement du HTML “post” standard.
            // Si vous n’avez pas besoin de HTML, préférez esc_html().
            echo wp_kses_post($content);
            ?>
        </div>
    </div>
    <?php
    return (string) ob_get_clean();
};

Étape 7 — CSS front (style.css) minimal et stable

/* wp-content/plugins/bpcab-blocs/build/note-box/style.css */
.bpcab-note-box {
  border-left: 4px solid currentColor;
  padding: 1rem;
  background: rgba(0,0,0,.03);
}

.bpcab-note-box__title {
  font-weight: 700;
  margin-bottom: .5rem;
}

.bpcab-note-box--info { color: #0b5fff; }
.bpcab-note-box--warning { color: #b45309; }
.bpcab-note-box--success { color: #15803d; }

Étape 8 — JS éditeur (index.js) : edit UI simple

Je fournis un exemple minimal. En pratique, vous le compilerez (et générerez index.asset.php).

// wp-content/plugins/bpcab-blocs/build/note-box/index.js
import { registerBlockType } from '@wordpress/blocks';
import { InspectorControls, RichText, useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { PanelBody, SelectControl, TextControl } from '@wordpress/components';

registerBlockType('bpcab/note-box', {
	edit({ attributes, setAttributes }) {
		const { title, tone } = attributes;

		const blockProps = useBlockProps();

		return (
			<>
				<InspectorControls>
					<PanelBody title="Réglages">
						<TextControl
							label="Titre"
							value={ title }
							onChange={ (value) => setAttributes({ title: value }) }
						/>
						<SelectControl
							label="Ton"
							value={ tone }
							options={ [
								{ label: 'Info', value: 'info' },
								{ label: 'Avertissement', value: 'warning' },
								{ label: 'Succès', value: 'success' },
							] }
							onChange={ (value) => setAttributes({ tone: value }) }
						/>
					</PanelBody>
				</InspectorControls>

				<div { ...blockProps }>
					<div className="bpcab-note-box__title">
						<RichText
							tagName="span"
							value={ title }
							allowedFormats={ [] }
							onChange={ (value) => setAttributes({ title: value }) }
							placeholder="Titre…"
						/>
					</div>
					<div className="bpcab-note-box__content">
						<InnerBlocks
							template={ [
								['core/paragraph', { placeholder: 'Votre note…' }],
							] }
							templateLock={ false }
						/>
					</div>
				</div>
			</>
		);
	},

	// Bloc dynamique : le HTML front est rendu par render.php
	save() {
		return <InnerBlocks.Content />;
	},
});

Code complet

Voici l’ensemble minimal fonctionnel (plugin + registre + bloc). Vous devrez ajouter le build JS réel (ou utiliser votre pipeline) pour générer index.asset.php. Le reste est copiable-collable tel quel.

Arborescence

wp-content/plugins/bpcab-blocs/
  bpcab-blocs.php
  src/
    Plugin.php
    Service/
      BlocksRegistry.php
  build/
    note-box/
      block.json
      render.php
      style.css
      index.js
      index.asset.php   # généré par build (ex: @wordpress/scripts)

bpcab-blocs.php

<?php
/**
 * Plugin Name: BPCAB Blocs
 * Description: Blocs personnalisés modernes via block.json (WP 6.9.4+).
 * Version: 1.0.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 */

defined('ABSPATH') || exit;

define('BPCAB_BLOCKS_FILE', __FILE__);
define('BPCAB_BLOCKS_DIR', __DIR__);

require_once BPCAB_BLOCKS_DIR . '/src/Plugin.php';

add_action('plugins_loaded', static function () {
    BPCABBlocksPlugin::instance()->boot();
});

src/Plugin.php

<?php
namespace BPCABBlocks;

defined('ABSPATH') || exit;

final class Plugin {
    private static ?self $instance = null;

    /** @var array<string, object> */
    private array $services = [];

    public static function instance(): self {
        return self::$instance ??= new self();
    }

    public function boot(): void {
        $this->register_services();
        $this->init_hooks();
    }

    private function register_services(): void {
        $this->services['blocks'] = new ServiceBlocksRegistry(BPCAB_BLOCKS_DIR . '/build');
    }

    private function init_hooks(): void {
        add_action('init', [$this->services['blocks'], 'register_blocks']);
    }
}

src/Service/BlocksRegistry.php

<?php
namespace BPCABBlocksService;

defined('ABSPATH') || exit;

final class BlocksRegistry {
    public function __construct(
        private readonly string $build_dir
    ) {}

    public function register_blocks(): void {
        $block_dirs = glob($this->build_dir . '/*', GLOB_ONLYDIR) ?: [];

        foreach ($block_dirs as $dir) {
            if (!is_readable($dir . '/block.json')) {
                continue;
            }

            $this->register_assets_for_block_dir($dir);

            $result = register_block_type($dir);
            if ($result === false) {
                error_log('[BPCAB] Échec register_block_type pour: ' . $dir);
            }
        }
    }

    private function register_assets_for_block_dir(string $dir): void {
        $slug = basename($dir);

        $editor_handle = 'bpcab-' . $slug . '-editor';
        $style_handle  = 'bpcab-' . $slug . '-style';

        $editor_js     = $dir . '/index.js';
        $editor_asset  = $dir . '/index.asset.php';
        $style_css     = $dir . '/style.css';

        if (is_readable($editor_js) && is_readable($editor_asset)) {
            $asset = require $editor_asset;

            $deps = (isset($asset['dependencies']) && is_array($asset['dependencies']))
                ? $asset['dependencies']
                : [];

            $ver = (isset($asset['version']) && is_string($asset['version']))
                ? $asset['version']
                : (string) filemtime($editor_js);

            wp_register_script(
                $editor_handle,
                plugins_url('build/' . $slug . '/index.js', BPCAB_BLOCKS_FILE),
                $deps,
                $ver,
                true
            );
        }

        if (is_readable($style_css)) {
            wp_register_style(
                $style_handle,
                plugins_url('build/' . $slug . '/style.css', BPCAB_BLOCKS_FILE),
                [],
                (string) filemtime($style_css)
            );
        }
    }
}

build/note-box/block.json

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "bpcab/note-box",
  "version": "1.0.0",
  "title": "Encart Note (BPCAB)",
  "category": "text",
  "icon": "info-outline",
  "description": "Encart éditorial avec titre, contenu et style.",
  "textdomain": "bpcab-blocs",
  "attributes": {
    "title": { "type": "string", "default": "À retenir" },
    "tone": { "type": "string", "default": "info" }
  },
  "supports": {
    "anchor": true,
    "align": ["wide", "full"],
    "html": false,
    "spacing": { "margin": true, "padding": true },
    "color": { "background": true, "text": true },
    "typography": { "fontSize": true, "lineHeight": true }
  },
  "editorScript": "bpcab-note-box-editor",
  "style": "bpcab-note-box-style",
  "render": "file:./render.php"
}

build/note-box/render.php

<?php
defined('ABSPATH') || exit;

return function (array $attributes, string $content, $block): string {
    $title = isset($attributes['title']) ? sanitize_text_field($attributes['title']) : 'À retenir';
    $tone  = isset($attributes['tone']) ? sanitize_key($attributes['tone']) : 'info';

    $allowed_tones = ['info', 'warning', 'success'];
    if (!in_array($tone, $allowed_tones, true)) {
        $tone = 'info';
    }

    $wrapper_attributes = get_block_wrapper_attributes([
        'class' => 'bpcab-note-box bpcab-note-box--' . $tone,
    ]);

    ob_start();
    ?>
    <div <?php echo $wrapper_attributes; ?>>
        <div class="bpcab-note-box__title"><?php echo esc_html($title); ?></div>
        <div class="bpcab-note-box__content"><?php echo wp_kses_post($content); ?></div>
    </div>
    <?php
    return (string) ob_get_clean();
};

build/note-box/style.css

.bpcab-note-box {
  border-left: 4px solid currentColor;
  padding: 1rem;
  background: rgba(0,0,0,.03);
}

.bpcab-note-box__title {
  font-weight: 700;
  margin-bottom: .5rem;
}

.bpcab-note-box--info { color: #0b5fff; }
.bpcab-note-box--warning { color: #b45309; }
.bpcab-note-box--success { color: #15803d; }

build/note-box/index.asset.php (exemple)

Ce fichier est généré par votre outil de build. Exemple réaliste (les dépendances varient selon votre code).

<?php
// Fichier généré automatiquement par le build.
return array(
    'dependencies' => array(
        'wp-blocks',
        'wp-element',
        'wp-components',
        'wp-block-editor',
        'wp-i18n',
    ),
    'version' => 'b7c1b2d9f1a2',
);

Explication du code

Pourquoi block.json change la donne

block.json centralise les métadonnées (nom, icône, attributs, supports) et la déclaration des assets (editorScript/style/render). WordPress sait lire ce fichier et construire l’enregistrement du bloc. Résultat : moins de code PHP “colle”, et une structure que vos outils (build, lint, CI) comprennent.

La référence officielle est ici : Block Metadata.

Pourquoi register_block_type($dir) est le point d’entrée propre

register_block_type() accepte un chemin de dossier. WordPress y cherche block.json, et en déduit le bloc. Vous évitez les tableaux PHP énormes qui dupliquent les infos.

Doc : register_block_type().

Pourquoi on enregistre les handles avant register_block_type()

Dans votre block.json, editorScript et style pointent vers des handles (ex : bpcab-note-box-editor). WordPress ne devine pas leur URL. Il faut donc wp_register_script() / wp_register_style() avant l’enregistrement du bloc, sinon vous aurez des assets manquants (ou un bloc qui s’édite sans UI).

Docs : wp_register_script() et wp_register_style().

Pourquoi le rendu serveur (render.php) est souvent plus fiable

Quand le bloc est dynamique, le HTML front n’est pas “sauvé” dans le contenu. Il est recalculé. Ça aide pour :

  • corriger un bug d’HTML/CSS sans “migrer” tous les posts,
  • ajouter une classe, un wrapper, une donnée structurée,
  • gérer des contextes (catégories, auteurs, ACF, options) côté PHP.

En échange, vous devez faire attention au cache (page cache, fragment cache) et à la stabilité du markup (pour éviter des surprises SEO).

Sanitization et escaping (les deux, pas l’un ou l’autre)

  • Sanitization à l’entrée (ici, attributs) : sanitize_text_field(), sanitize_key().
  • Validation de domaine (ici, $allowed_tones) : vous refusez toute valeur inattendue.
  • Escaping à la sortie (HTML) : esc_html(), et wp_kses_post() si vous autorisez un sous-ensemble HTML.

J’ai souvent vu des blocs “internes” se faire injecter des valeurs bizarres via import/export ou via API. Traitez les attributs comme non fiables, systématiquement.

Variantes et cas d’usage

Variante 1 — Bloc statique (save() complet) pour sites sans rendu serveur

Si vous voulez un bloc qui enregistre son HTML final dans le post (moins de PHP, plus simple à héberger), supprimez "render" dans block.json et faites un save() qui rend la structure complète. Attention : si vous changez le markup plus tard, vous devrez gérer deprecated côté JS pour ne pas casser les anciens contenus.

Variante 2 — Ajout d’un style “editor only”

Sur des blocs qui ont une UI dense, je sépare souvent le CSS éditeur (pour l’ergonomie) du CSS front (pour la perf). Ajoutez dans block.json une clé editorStyle (handle) et enregistrez-la comme style.

{
  "editorStyle": "bpcab-note-box-editor-style"
}

Variante 3 — Cache de rendu (fragment cache) pour blocs dynamiques coûteux

Si votre rendu fait une requête externe ou une requête WP_Query lourde, mettez un cache transients par post + attributs. Edge case classique : invalider le cache à la mise à jour du post.

<?php
// Exemple de pattern (à adapter) : cache par post ID + hash attributs.
$key = 'bpcab_nb_' . (int) ($block->context['postId'] ?? 0) . '_' . md5(wp_json_encode($attributes));

$cached = get_transient($key);
if (is_string($cached) && $cached !== '') {
    return $cached;
}

$html = '...'; // générez votre HTML

set_transient($key, $html, HOUR_IN_SECONDS);
return $html;

Attention aux invalidations : si vous cachez par post, invalidez sur save_post et sur modification d’options globales.

Compatibilité Divi 5 / Elementor / Avada

Les blocs Gutenberg cohabitent généralement bien avec Divi 5, Elementor et Avada, mais il y a des détails pratiques.

Divi 5

  • Si vos pages sont construites dans Divi, le bloc sera surtout utile dans l’éditeur WordPress natif (articles, CPT). Gardez le CSS du bloc très “namespaced” (comme .bpcab-note-box) pour éviter les styles globaux Divi.
  • Si Divi applique des styles typographiques globaux agressifs, utilisez des supports typography et laissez l’utilisateur ajuster dans l’éditeur.

Elementor

  • Elementor peut encapsuler le contenu dans des wrappers. Évitez les sélecteurs CSS trop dépendants de la structure du DOM parent.
  • Si vous insérez le bloc dans une zone “Shortcode” ou “HTML” Elementor via rendu de contenu, testez la largeur (alignwide/full) : certains thèmes Elementor limitent la largeur via max-width.

Avada (Fusion Builder)

  • Avada a souvent un système de CSS critique et de cache d’assets. Après ajout/modif de bloc, videz le cache Avada + cache navigateur, sinon vous allez croire que style.css ne se charge pas.
  • Si Avada minifie/concatène, vérifiez que vos handles restent stables (d’où l’intérêt du versionnement par filemtime ou hash build).

Vérifications après mise en place

  1. Bloc visible : dans l’éditeur, recherchez “Encart Note (BPCAB)”. S’il n’apparaît pas, c’est un problème d’enregistrement (init, chemins, block.json invalid).
  2. Assets chargés : ouvrez l’inspecteur réseau dans l’éditeur, vérifiez que build/note-box/index.js et style.css sont chargés.
  3. Rendu front : publiez un post, vérifiez le HTML généré. Vous devez voir class="bpcab-note-box ...".
  4. Supports : testez l’ancre, les couleurs, le padding/margin. Vérifiez que get_block_wrapper_attributes() reflète vos choix.
  5. Cache : si vous avez un plugin de cache, purge complète après ajout du plugin et après build.

Si ça ne marche pas

Procédure que j’applique quand un bloc ne se charge pas (et qui évite de partir dans tous les sens).

1) Vérifier les erreurs PHP (logs)

  • Activez WP_DEBUG et WP_DEBUG_LOG sur staging.
  • Regardez wp-content/debug.log.
  • Erreurs fréquentes : PHP < 8.1, chemin de fichier incorrect, require d’un fichier absent.

2) Vérifier que init s’exécute

Si vous avez mis le code dans le mauvais endroit (un snippet qui ne charge pas sur init, ou un MU-plugin mal placé), register_block_type() ne tourne jamais. Ajoutez temporairement un error_log() dans register_blocks() pour confirmer.

3) Vérifier block.json (JSON invalide)

Une virgule en trop et WordPress ignore le bloc. Validez le JSON. Sur des équipes, j’ai vu des fichiers enregistrés en UTF-8 avec BOM qui posent des surprises.

4) Vérifier les handles d’assets

Si block.json contient "editorScript": "bpcab-note-box-editor", alors votre PHP doit enregistrer exactement ce handle. Une lettre différente, et l’éditeur n’a pas votre UI.

5) Vider caches (vraiment)

  • cache plugin (page/object),
  • cache serveur (Nginx fastcgi, varnish),
  • CDN (Cloudflare),
  • cache navigateur (hard reload).

Tableau diagnostic (rapide)

Symptôme Cause probable Vérification Solution
Le bloc n’apparaît pas dans l’inserter block.json non lu / erreur JSON / hook non exécuté Logs PHP + vérifier build/*/block.json Corriger JSON, vérifier add_action('init', ...), chemins
Le bloc apparaît mais l’UI est cassée Script éditeur non chargé (handle/dépendances) Console navigateur (éditeur) + onglet Réseau Vérifier handle, générer index.asset.php, purge caches
Le CSS front ne s’applique pas style non enregistré ou cache Avada/Autoptimize Voir source HTML + link rel="stylesheet" Vérifier wp_register_style, purge/minification
Erreur “Call to undefined function register_block_type” Code exécuté trop tôt / environnement cassé Stacktrace Exécuter sur init, vérifier chargement WP Core
Le rendu front est vide render.php ne retourne pas une callback valide Activer debug + tester un return '<div>ok</div>'; Corriger return function (...) { ... }, vérifier permissions/erreurs

Pièges et erreurs courantes

Erreur Cause Solution
Le code est copié dans le mauvais fichier (thème au lieu de plugin) Changement de thème = bloc disparu Mettre l’enregistrement dans un plugin, garder le thème pour le design
Parse error: syntax error, unexpected … Parenthèse/point-virgule manquant dans un snippet Passer le plugin dans un linter/IDE, activer logs sur staging
Utiliser wp_enqueue_script au lieu de wp_register_script Le script se charge partout et/ou trop tôt Déclarer le handle, laisser WordPress charger quand nécessaire via block.json
Hook inadapté (plugins_loaded au lieu de init) API blocks pas prête / ordre de chargement Enregistrer sur init (priorité par défaut), ou ajuster si besoin
Conflit de cache (JS ancien servi) Version d’asset figée Utiliser .asset.php (hash) ou filemtime, purger CDN
index.asset.php manquant Build non exécuté, déploiement incomplet Ajouter une étape CI/CD, ou fallback (mais documentez-le)
Permaliens non régénérés (cas indirect) Après migration, endpoints/rewrites incohérents Ré-enregistrer les permaliens (Réglages > Permaliens > Enregistrer)
Confusion actions/filtres Retourner une valeur dans une action ne sert à rien Utiliser add_action pour exécuter, add_filter pour transformer
CSS/JS non chargés à cause d’une URL erronée plugins_url ancré sur le mauvais fichier Définir BPCAB_BLOCKS_FILE et s’y référer partout
Erreur liée à une version PHP trop ancienne Syntaxe PHP 8.1 (types, propriétés readonly) Mettre à niveau PHP, ou enlever les features modernes (non recommandé)
Code d’un ancien tutoriel (pré-block.json) incompatible Approche legacy, dépendances incertaines Revenir à block.json + register_block_type($dir)

Conseils sécurité, performance et maintenance

  • Sécurité : traitez les attributs comme non fiables. Sanitization + validation + escaping. Pour du HTML riche, wp_kses_post() est un minimum, mais restreignez si possible.
  • Performance : évitez de charger des scripts globaux. Le couplage block.json + handles enregistrés permet à WordPress de charger au bon endroit.
  • Stabilité : pour un bloc statique, prévoyez deprecated côté JS si vous changez le markup. Pour un bloc dynamique, gardez un markup front stable (classes, structure), sinon vous créez des régressions CSS.
  • Build reproductible : la cause n°1 des blocs “cassés” en prod, c’est un build local non déployé. Mettez une étape CI qui produit build/ et l’archive.
  • Compatibilité future : évitez d’utiliser des APIs JS non stabilisées. Restez sur les packages documentés. Suivez le dépôt officiel WordPress/gutenberg.

Pour suivre les changements core (et comprendre pourquoi un comportement change), je garde un œil sur Trac et les PR Gutenberg. Trac : core.trac.wordpress.org.

Ressources

FAQ

Est-ce que je dois absolument utiliser un plugin plutôt qu’un thème ?

Si le bloc est utilisé dans le contenu, oui, dans la majorité des cas. Sinon vous liez vos contenus à un thème. J’ai vu des migrations où 300 articles perdaient leur bloc parce que le thème changeait.

Pourquoi mon bloc apparaît, mais les contrôles (InspectorControls) ne s’affichent pas ?

Presque toujours : le script éditeur n’est pas chargé. Vérifiez le handle dans block.json et celui de wp_register_script(). Puis vérifiez que index.asset.php existe et contient les dépendances correctes.

Je peux mettre file:./index.js directement dans block.json ?

Vous pouvez référencer des fichiers via file: dans certains contextes, mais en pratique (et en prod), je privilégie les handles + wp_register_script avec .asset.php. C’est plus prévisible avec cache/CDN et pipelines CI.

Comment gérer les traductions côté JS ?

Utilisez wp_set_script_translations() sur le handle du script éditeur, et un textdomain cohérent. Gardez vos fichiers .po/.mo dans un dossier languages. Doc : wp_set_script_translations().

Bloc dynamique ou statique : lequel choisir ?

  • Dynamique si le rendu doit évoluer, ou dépend du contexte, ou si vous voulez corriger sans migrer le contenu.
  • Statique si vous voulez un HTML “gelé” dans le post, facile à exporter, et sans dépendance PHP.

Pourquoi utiliser get_block_wrapper_attributes() plutôt que construire les classes à la main ?

Parce que WordPress ajoute automatiquement des attributs/classes liés aux supports (align, spacing, colors, etc.). Si vous reconstruisez tout à la main, vous perdez des fonctionnalités et vous cassez des comportements de thèmes.

Mon CSS est chargé, mais il est écrasé par le thème (ou Divi/Avada). Que faire ?

Évitez d’augmenter la spécificité à l’infini. Namespacer vos classes (comme .bpcab-note-box) et utiliser des variables CSS ou les supports de couleur/typo est souvent plus robuste. Et testez avec le CSS du thème en conditions réelles.

Est-ce que je peux scanner automatiquement tous les blocs dans build/ comme dans l’exemple ?

Oui, c’est pratique. Sur des plugins très gros, je préfère parfois une liste explicite (pour contrôler l’ordre, gérer des dépendances, ou désactiver un bloc). Le scan est OK tant que vous gardez une convention stricte.

Que faire si un plugin de minification concatène et casse l’éditeur ?

Désactivez la minification sur l’admin/éditeur, ou excluez vos handles. J’ai souvent vu Autoptimize/LSCache casser des scripts du Block Editor si mal configurés.

Comment suivre les changements de l’API Block dans le temps ?

Suivez les notes de version WordPress sur developer.wordpress.org/news, et le repo WordPress/gutenberg. Pour les changements core, Trac reste la source primaire : core.trac.wordpress.org.