Drupal Gutenberg development
Build custom Gutenberg blocks and modules for Drupal.
Version target: Drupal Gutenberg 3.x, which bundles Gutenberg 16.7 (27 Sep 2023) / WordPress 6.4. Compatible with Drupal 10 and 11.
Getting started
The quickest way to a custom block is copying the example_block module included with Gutenberg:
- •Copy
gutenberg/modules/example_blocktomodules/custom/my_block - •Rename all
example_block.*files tomy_block.* - •Rename
package.example.jsontopackage.json - •Update all
example_blockreferences in.gutenberg.yml,.info.yml,.libraries.yml, andpackage.json - •Run
npm install - •Enable the module:
drush en my_block - •Run
npm run buildto compile ES6 to JS - •Run
npm startfor watch mode during development
Development workflow
Follow these steps in order when building a block. Choose the workflow that matches your block type.
Client-side blocks (save HTML)
Use this when the block saves its own HTML output — no server-side rendering needed.
- •Scaffold module structure — copy
gutenberg/modules/example_blockor createMODULE.info.yml,MODULE.libraries.yml,MODULE.gutenberg.yml,package.json,.babelrc, andsrc/directory - •Configure
.gutenberg.yml— add your editor library underlibraries-edit: - •Configure
.libraries.yml— declare the compiled JS withgutenberg/edit-nodeas a dependency - •Register block in
.es6.js— implementregisterBlockType()withedit()andsave()functions; useuseBlockProps()/useBlockProps.save() - •Build —
npm install && npm run build - •Enable —
drush en MODULE && drush cr - •Verify — run through the verification checklist below
Dynamic blocks (server-rendered via Twig)
Use this when block output is rendered by PHP/Twig — the block saves no HTML (save() returns null).
- •Scaffold module structure — same as client-side, plus create
MODULE.moduleandtemplates/directory - •Configure
.gutenberg.yml— add editor library underlibraries-edit:AND add the block underdynamic-blocks: - •Configure
.libraries.yml— declare the compiled JS withgutenberg/edit-nodeas a dependency - •Register block in
.es6.js— implementregisterBlockType()withedit()function;save()returnsnull - •Choose editor preview approach:
- •
ServerSideRender— if the block can render without page context (useconst ServerSideRender = wp.serverSideRender;) - •Static placeholder — if the block depends on runtime context (current node, route data)
- •
- •Register theme hook — in
MODULE.module, implementhook_theme()with'base hook' => 'gutenberg_block' - •Create Twig template — in
templates/gutenberg-block--{namespace}--{block-name}.html.twig; wrap output in{% if is_visible %} - •Implement preprocess hook — block-specific
hook_preprocess_gutenberg_block__MODULE__BLOCK()with data loading,is_visibleguard, and cache tags - •Build —
npm install && npm run build - •Enable —
drush en MODULE && drush cr - •Verify — run through the verification checklist below
Verification checklist
After implementing a block, verify each item before considering the task complete:
- • Block appears in the Gutenberg inserter
- • No JavaScript errors in browser console
- • Edit controls (InspectorControls, RichText, etc.) work correctly
- • Save and reload the node: no "Invalid block" warning
- •
npm run buildcompletes without errors - •
drush crand hard refresh show latest changes - • Client-side blocks:
save()output produces valid HTML markup - • Dynamic blocks:
ServerSideRenderpreview renders (or placeholder displays) - • Dynamic blocks: front-end renders correctly
- • Dynamic blocks: cache tags invalidate when referenced entities change
Troubleshooting
If something isn't working, see troubleshooting.md for symptom-based debugging organized by category (block not appearing, validation errors, SSR issues, build problems, media, translations, and more).
File types
| File | Purpose |
|---|---|
my_block.info.yml | Standard Drupal module info |
my_block.libraries.yml | Declares CSS/JS assets via Libraries API |
my_block.gutenberg.yml | Registers custom blocks, categories, patterns |
src/*.es6.js | ES6 source files (edit/save functions, block registration) |
dist/*.js | Compiled JavaScript output |
templates/*.html.twig | Twig templates for server-side rendered blocks |
Differences from WordPress
| Aspect | WordPress | Drupal |
|---|---|---|
| Content storage | Post meta | HTML in node__body.body_value |
| Block declaration | register_block_type() + block.json | .gutenberg.yml + Twig |
| SSR | render_callback | Twig templates + hooks |
| Assets | wp_enqueue_script/style() | Libraries API (.libraries.yml) |
| Build | @wordpress/scripts (webpack) | Babel/drupal-js-build (.es6.js) |
| Media | Native media library | Drupal file entities + optional media module |
Installation
composer require 'drupal/gutenberg:^3.0' drush en gutenberg
Then enable Gutenberg for specific content types at /admin/config/content/gutenberg.
Detailed references
See these files in this skill's directory:
- •module-structure.md -- file structure,
.info.yml,.libraries.yml,.gutenberg.yml - •javascript-build.md -- ES6 to ES5 workflow, Babel, webpack, React/JSX
- •block-registration.md -- registering blocks, categories, variations, block styles
- •server-side-rendering.md -- Twig templates, hooks, dynamic blocks,
ServerSideRender - •media-integration.md -- file entities, media library, media blocks, upload handling
- •troubleshooting.md -- symptom-based debugging for common block development issues