12k
All articles

Comment créer un bloc Gutenberg personnalisé

Créez un bloc Gutenberg personnalisé avec React, block.json et PHP. Gérez les attributs, le rendu statique ou dynamique et les versions obsolètes pour éviter les erreurs.

OpenReplay Team
OpenReplay Team
Comment créer un bloc Gutenberg personnalisé

Un bloc Gutenberg est un composant React que WordPress enregistre sous un nom doté d’un espace de noms. Il s’affiche dans l’éditeur via une fonction edit et produit son rendu côté front-end via une fonction save ou un template de rendu PHP.

Si vous savez écrire un composant React, vous savez écrire un bloc. L’essentiel de la difficulté réside dans les éléments propres à WordPress qui entourent le composant : un manifeste JSON, un appel d’enregistrement en PHP et une étape de validation qui affiche « Ce bloc contient un contenu inattendu ou non valide » dès que vous modifiez votre balisage.

Avant l’arrivée des blocs, on ajoutait des comportements personnalisés en intégrant du JavaScript personnalisé aux thèmes WordPress avec wp_enqueue_script. Pour le contenu, les blocs remplacent cette approche : au lieu d’un script exécuté sur du HTML statique, les rédacteurs disposent d’un composant visuel et modifiable. Ce guide construit un bloc « callout » (encadré) doté d’un corps RichText modifiable et d’un champ URL dans la barre latérale. Il détaille chaque fichier généré par l’outil de scaffolding et explique pourquoi les erreurs de validation surviennent et comment les corriger.

Points clés à retenir

  • npx @wordpress/create-block@latest callout --namespace=openreplay génère une extension de bloc complète, et @wordpress/scripts la compile avec npm start et npm run build.
  • Le fichier block.json déclare le bloc. Les appels d’enregistrement PHP et JavaScript lisent tous deux son nom, ses attributs, ses supports et les chemins de ses ressources à partir de ce seul fichier.
  • Les attributs sans source sont stockés au format JSON dans le commentaire HTML délimitant le bloc. Les attributs dotés d’une source et d’un selector sont extraits du balisage enregistré.
  • Au chargement d’un article, l’éditeur réexécute save() et compare le résultat au balisage stocké. Toute différence déclenche l’erreur de contenu non valide, jusqu’à ce qu’une entrée deprecated reproduise l’ancienne sortie.
  • Un bloc dynamique renvoie null depuis save et est rendu en PHP à chaque requête. Utilisez-en un pour tout contenu qui doit évoluer sans que personne n’ait à réenregistrer l’article.

Un bloc est un composant React

Un bloc Gutenberg est un composant React dont l’API propre à WordPress correspond à des patterns que les développeurs React connaissent déjà :

Concept ReactÉquivalent pour un bloc
JSX du composantFonction edit
Propsattributes
setStatesetAttributes
Rendu côté serveurFonction save ou render.php

Un bloc Gutenberg diffère d’un composant React classique sur deux points. D’abord, la sortie de save est écrite en HTML dans le contenu de l’article. Ensuite, le schéma des attributs est défini en JSON plutôt qu’avec PropTypes ou TypeScript.

Générer le bloc avec create-block

Le package officiel @wordpress/create-block génère une extension de bloc fonctionnelle, avec son outillage de build déjà configuré. Exécutez-le dans wp-content/plugins :

npx @wordpress/create-block@latest callout --namespace=openreplay
cd callout
npm start        # watch mode during development
npm run build    # production build

Si vous omettez --namespace, le bloc est nommé create-block/callout. Une fois npm start lancé, activez l’extension depuis l’interface d’administration.

FichierRôle
callout.phpEn-tête de l’extension et enregistrement du bloc sur le hook init
package.jsonScripts npm qui encapsulent @wordpress/scripts
src/callout/block.jsonDéclaration du bloc : nom, attributs, supports, ressources
src/callout/index.jsAppelle registerBlockType avec edit et save
src/callout/edit.jsComposant d’interface de l’éditeur
src/callout/save.jsBalisage stocké dans le contenu de l’article
src/callout/style.scssStyles pour le front-end et l’éditeur
src/callout/editor.scssStyles réservés à l’éditeur
build/Sortie compilée, réellement chargée par WordPress
build/blocks-manifest.phpMétadonnées de tous les blocs, générées au moment du build

Le fichier PHP est une extension ordinaire (voir écrire une extension WordPress de A à Z) à laquelle s’ajoute un appel d’enregistrement. Le concept sous-jacent est register_block_type(), qui accepte un dossier contenant un block.json. Le code généré l’encapsule dans un appel groupé qui nécessite WordPress 6.8 ou une version ultérieure, et l’en-tête de l’extension générée définit Requires at least: 6.8 en conséquence. Voici une version simplifiée de cet appel :

add_action( 'init', function () {
	wp_register_block_types_from_metadata_collection(
		__DIR__ . '/build',
		__DIR__ . '/build/blocks-manifest.php'
	);
} );

Le guide d’enregistrement des blocs explique les deux approches.

block.json : là où le bloc est déclaré

Le fichier block.json constitue la déclaration unique d’un bloc. Les appels d’enregistrement PHP et JavaScript lisent tous deux son nom, ses attributs, ses supports et les chemins de ses ressources : un seul fichier décrit donc le bloc à la fois pour l’éditeur et pour le serveur.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "openreplay/callout",
  "version": "0.1.0",
  "title": "Callout",
  "category": "text",
  "icon": "info",
  "attributes": {
    "content": { "type": "string", "source": "html", "selector": "p" },
    "url": { "type": "string", "default": "" }
  },
  "supports": { "html": false },
  "textdomain": "callout",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css"
}

name suit le format namespace/slug. Considérez-le comme définitif, car il est inscrit dans le balisage de chaque article utilisant le bloc.

apiVersion: 3 est apparu avec WordPress 6.3 et indique que le bloc fonctionne dans l’éditeur en iframe. Depuis WordPress 6.9, le schéma n’accepte plus que la version 3, et les blocs enregistrés avec une version antérieure génèrent un avertissement dans la console lorsque SCRIPT_DEBUG est activé. À partir de WordPress 7.1, l’éditeur d’articles s’exécute systématiquement dans une iframe, quelle que soit l’apiVersion déclarée par un bloc : tous les blocs doivent donc y fonctionner.

attributes définit les données du bloc. type et default se comportent comme on s’y attend. source détermine où la valeur est stockée. La référence des attributs précise qu’une valeur sans source est placée dans le commentaire délimiteur, tandis que source: "html" lit le HTML interne de l’élément ciblé — c’est généralement sur ce mécanisme que reposent les champs RichText.

Définition de l’attributEmplacement de la valeur
Sans source (url)JSON dans le commentaire <!-- wp:... -->
source + selector (content)Extraite du HTML enregistré à chaque chargement

Voici à quoi ressemble le contenu stocké de l’article (à titre d’illustration) :

<!-- wp:openreplay/callout {"url":"https://example.com"} -->
<div class="wp-block-openreplay-callout"><p>Deploys freeze Friday at noon.</p></div>
<!-- /wp:openreplay/callout -->

supports active des fonctionnalités de l’éditeur. html: false masque l’option « Modifier en HTML ». Des clés comme align et color ajoutent des contrôles dans la barre d’outils et la barre latérale sans code supplémentaire, comme l’indique la référence des supports.

Les chemins file: sont résolus par rapport au block.json enregistré, c’est-à-dire la copie située dans build/callout/, à côté des fichiers compilés index.js, index.css et style-index.css. C’est pourquoi le code PHP pointe vers build et non vers src. La référence des métadonnées de bloc documente toutes les clés.

edit.js : RichText, InspectorControls et useBlockProps

Le composant edit n’est rendu que dans l’éditeur. Il reçoit attributes et setAttributes en props, et setAttributes joue le même rôle qu’un setter d’état : il fusionne les modifications transmises et déclenche un nouveau rendu.

import { __ } from '@wordpress/i18n';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
  const { content, url } = attributes;
  return (
    <>
      <InspectorControls>
        <PanelBody title={ __( 'Link', 'callout' ) }>
          <TextControl
            label={ __( 'URL', 'callout' ) }
            value={ url }
            onChange={ ( value ) => setAttributes( { url: value } ) }
          />
        </PanelBody>
      </InspectorControls>
      <div { ...useBlockProps() }>
        <RichText
          tagName="p"
          value={ content }
          onChange={ ( value ) => setAttributes( { content: value } ) }
          placeholder={ __( 'Write the callout…', 'callout' ) }
        />
      </div>
    </>
  );
}

useBlockProps() ajoute les noms de classes et les attributs data dont l’éditeur a besoin pour la sélection et la mise en forme ; appliquez-le donc (via l’opérateur de décomposition) sur l’élément le plus externe. RichText permet aux utilisateurs de modifier le texte directement dans le bloc. InspectorControls déplace vers la barre latérale les réglages qui ne se modifient pas en ligne, comme une URL. PanelBody regroupe ces contrôles dans une section repliable.

save.js et l’erreur de validation de bloc

La fonction save produit le balisage stocké dans le contenu de l’article. Elle doit être une fonction pure des attributs : ni état, ni effets, ni hooks, à l’exception de useBlockProps.save().

import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
  return (
    <div { ...useBlockProps.save() }>
      <RichText.Content tagName="p" value={ attributes.content } />
    </div>
  );
}

RichText.Content produit la valeur stockée sous forme de balisage brut, et useBlockProps.save() ajoute la classe du conteneur côté front-end.

Pourquoi la validation d’un bloc échoue-t-elle ?

Lorsqu’un article s’ouvre dans l’éditeur, WordPress exécute la fonction save actuelle du bloc avec les attributs extraits et compare le résultat au balisage stocké dans l’article. Le guide Edit et Save considère tout écart entre cette nouvelle sortie et le HTML stocké comme un bloc non valide. L’éditeur affiche alors « Ce bloc contient un contenu inattendu ou non valide » et journalise un diff dans la console du navigateur.

Modifier une fonction save ne réécrit pas les articles qui contiennent déjà le bloc. Supposons que la v2 ajoute une classe et un lien :

export default function save( { attributes } ) {
  const { content, url } = attributes;
  return (
    <div { ...useBlockProps.save() }>
      <RichText.Content tagName="p" className="callout__body" value={ content } />
      { url && <a href={ url }>Learn more</a> }
    </div>
  );
}

Tous les encadrés existants échouent désormais à la validation, car la nouvelle sortie de save ne correspond plus à l’ancien balisage stocké.

La solution : deprecated

Une entrée deprecated corrige l’erreur de validation en conservant les attributs et la fonction save précédents. Elle peut également inclure une fonction migrate facultative qui convertit les anciens attributs vers une nouvelle structure. Grâce à cette entrée, l’éditeur reconnaît l’ancien balisage et le met à niveau au prochain enregistrement de l’article.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText } from '@wordpress/block-editor';
import Edit from './edit';
import save from './save';
import metadata from './block.json';

const v1 = {
  attributes: {
    content: { type: 'string', source: 'html', selector: 'p' },
    url: { type: 'string', default: '' },
  },
  save( { attributes } ) {
    return (
      <div { ...useBlockProps.save() }>
        <RichText.Content tagName="p" value={ attributes.content } />
      </div>
    );
  },
};

registerBlockType( metadata.name, { edit: Edit, save, deprecated: [ v1 ] } );

Chaque entrée embarque ses propres attributes, car l’analyse du balisage en dépend. Si la v2 avait aussi remplacé le sélecteur p par .callout__body, v1 aurait toujours besoin de selector: 'p' pour lire correctement les anciens articles.

La référence sur les dépréciations établit les règles suivantes :

  • Classez les entrées de la plus récente à la plus ancienne.
  • L’éditeur teste chaque entrée séparément par rapport au balisage tel qu’il a été enregistré. Une entrée ne transmet jamais son résultat à la suivante.
  • Si la sortie de save d’une entrée ne correspond pas au HTML stocké, l’éditeur ignore cette entrée, et sa fonction migrate n’est jamais exécutée.
  • Une fonction isEligible facultative permet d’appliquer une entrée à un bloc qui passe déjà la validation.

Trois bonnes pratiques évitent la plupart des erreurs de validation :

  1. Ne placez jamais de valeurs non déterministes dans save, comme des dates ou des identifiants aléatoires.
  2. Ne lisez jamais d’état ni de contexte dans save.
  3. Traitez la sortie de save comme un schéma de base de données : toute modification nécessite une migration.

Qu’est-ce qu’un bloc dynamique ?

Un bloc dynamique renvoie null depuis save, ne stocke que ses attributs dans le contenu de l’article et est rendu par PHP à chaque requête. Optez pour ce type de bloc lorsque le rendu doit évoluer sans que personne ne réenregistre l’article, par exemple pour afficher une liste des dernières publications.

Bloc statiqueBloc dynamique
Emplacement du balisageContenu de l’articleTemplate PHP
Mise à jourAu réenregistrement de l’articleÀ chaque requête
Valeur renvoyée par saveJSXnull
Coût serveurAucun au-delà de la diffusion du contenuExécution de PHP à chaque rendu

Générez-en un avec npx @wordpress/create-block@latest callout --namespace=openreplay --variant dynamic, ou ajoutez vous-même "render": "file:./render.php" au block.json. Le template ci-dessous liste les entrées d’un type de contenu personnalisé. Il suppose que vous avez ajouté un attribut count de type number :

<?php
$query = new WP_Query( array(
	'post_type'      => 'project',
	'posts_per_page' => isset( $attributes['count'] ) ? (int) $attributes['count'] : 3,
) );
?>
<ul <?php echo get_block_wrapper_attributes(); ?>>
	<?php while ( $query->have_posts() ) : $query->the_post(); ?>
		<li><a href="<?php echo esc_url( get_permalink() ); ?>"><?php echo esc_html( get_the_title() ); ?></a></li>
	<?php endwhile; wp_reset_postdata(); ?>
</ul>

$attributes contient les attributs extraits du bloc. get_block_wrapper_attributes() est l’équivalent PHP de useBlockProps. Si vous préférez une fonction à un fichier de template, passez un render_callback à register_block_type(). Les blocs dynamiques ne rencontrent jamais d’erreurs de validation, puisque l’éditeur ne dispose d’aucun balisage stocké auquel se comparer.

Conclusion

Un bloc, c’est un composant React, une déclaration JSON et un contrat portant sur le balisage stocké. Pour un développeur React, le composant est la partie facile. C’est le contrat qui fait échouer les blocs : dès que de vrais articles contiennent votre bloc, toute modification de save exige une entrée deprecated correspondante. Générez l’encadré, ajoutez une classe à la sortie de sa fonction save et observez l’erreur apparaître dans la console. Écrivez ensuite la dépréciation qui la fait disparaître. Une fois ce cycle expérimenté, l’erreur de contenu non valide n’aura plus rien de mystérieux.

FAQ

Un bloc Gutenberg personnalisé doit-il résider dans une extension ou dans un thème ?

Placez-le dans une extension. Le balisage du bloc est enregistré dans le contenu des articles : le bloc doit donc rester enregistré tant que des articles l'utilisent. Si le code qui l'enregistre est désactivé, par exemple lors d'un changement de thème, l'éditeur affiche un message indiquant que le site ne prend pas en charge ce bloc et propose de le conserver tel quel ou de le supprimer. Une extension garantit la disponibilité du bloc indépendamment des changements de thème.

Que fait l'option « Tenter la récupération du bloc » sur un bloc non valide ?

« Tenter la récupération du bloc » (Attempt Block Recovery) reconstruit le bloc à partir de ses attributs extraits à l'aide de la fonction save actuelle, puis remplace le balisage stocké. Elle corrige un seul bloc dans un seul article et supprime tout contenu non capturé par les attributs. Elle contourne le problème dans l'éditeur, mais ne corrige pas le code. Une entrée deprecated permet de mettre à niveau tous les articles existants sans que personne n'ait à récupérer les blocs manuellement.

Comment ajouter un second bloc à la même extension ?

Créez un nouveau dossier sous src contenant ses propres fichiers block.json, index.js, edit.js et save.js. Les commandes start et build de @wordpress/scripts parcourent src et ses sous-dossiers à la recherche de fichiers block.json, compilent chacun comme un point d'entrée distinct dans un dossier correspondant sous build, et inscrivent chaque bloc dans le manifeste de blocs enregistré par l'extension. Relancez npm start après avoir ajouté le dossier.

Un bloc dynamique peut-il contenir des blocs imbriqués ?

Oui, mais save ne peut alors pas renvoyer null. Affichez la zone des blocs enfants avec InnerBlocks ou useInnerBlocksProps dans edit, et renvoyez InnerBlocks.Content depuis save afin que le balisage des enfants soit stocké dans le contenu de l'article. Le template PHP reçoit ce balisage enfant rendu dans la variable $content, aux côtés de $attributes. Chaque bloc ne prend en charge qu'une seule zone InnerBlocks, et allowedBlocks restreint les blocs enfants que les rédacteurs peuvent insérer.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.