Gutenberg and the Block concept is such a radical change to WordPress, I felt it deserved a separate page, away from my main WordPress docs page (which you still should definitely check out 😀).
There is still a lot of overlap between a modern Gutenberg setup and the typical PHP plugin or theme development setup - so there might be things left out of this document because they are covered in the aforementioned main WordPress doc.
Resources
Resources List
- WordPress Dev: Block Editor Handbook
- Unofficial Guides
- Block Plugin Samples
- Official: Gutenberg Examples
- Official: Gutenberg Block Library
- These are basically the built-in blocks that ship with WordPress!
- There are so many good examples in here!
- Bootstrap Gutenberg Blocks for WordPress
- GoDaddy CoBlocks
- Stackable
- ahmadawais/Gutenberg-Boilerplate
- Scaffolding / Generators
- wp-scripts (highly recommend as the first thing to try)
- ahmadawais/create-guten-block
- zackify/gutenblock
Build Tooling
WP-Scripts
wp-scripts
(full package name @wordpress/scripts
) is a distributed package that contains scripts and tools that make WordPress development, building, and testing easier.
The domain of the wp-scripts
is massive, and includes things like:
- Transpiling, optimizing, and bundling JavaScript
- Linting CSS, JS, and other files
- Auto-formatting selected languages
- Orchestrating webpack, and running a live-reload JS server
- Supporting E2E tests, and orchestrating Puppeteer + Jest
- And more!
It can be a little overwhelming at first, but again, a tool that you don't want to leave out of your WP arsenal.
You can find the entire source code of wp-scripts here
WP-Scripts: Extending Webpack
Create a webpack.config.js
in the root. Import the default config, then extend it as you please, and re-export it:
// For better intellisense, I'm asserting that the imported object is a webpack config type
// @ts-ignore
const wpConfig = require('@wordpress/scripts/config/webpack.config');
/** @type {import('webpack').Configuration} */
const defaultConfig = wpConfig;
/** @type {import('webpack').Configuration} */
const finalConfig = {
...defaultConfig,
// Whatever else you want
}
// Don't forget to re-export!
module.exports = finalConfig;
Gutenberg Block Development - React and JavaScript Files
What's the Deal with All the IIFEs?
If you look through the source code of a lot of plugins that are out there, you will probably notice that a lot of them use IIFEs (Immediately-Invoked Function Expression) in various JavaScript files. Why?
The #1 reason is to provide closure; since WordPress's enqueue system doesn't use modules or anything that would guarantee closure, you have to make sure to not pollute the global JS space with your plugin's variables, functions, etc. IIFEs do this automatically, with or without a build system. However, if you use a standard build system, such as webpack, you shouldn't need to use IIFEs, since the bundler can automatically provide closure for you when it builds the final distributed files. In fact, using them if you can avoid them is probably a pattern to be avoided.
File Organization
A good approach to organizing blocks is to have each block isolated in its own folder, with each block stage (edit
vs save
) broken out into its own file. Each block folder can have a index.js
that exports everything from the folder upwards, so in the parent folder that contains all your blocks, you can import them and register them together.
This StackOverflow response does a good job of summarizing.
TypeScript Support
- Modify WebPack config to use
ts-loader
- I would turn on
transpileOnly
, for perf improvement, as well as avoidingcheckJs
conflict issue (it will try to checknode_modules
, regardless of exclude settings)- You can add a separate step as part of
build
or whatever that runs the type-checker specifically
- You can add a separate step as part of
- You also need to tell WebPack to resolve
.ts
and.tsx
filetypes.
- I would turn on
- Modify
tsconfig
:
{
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"jsx": "react-jsx",
"module": "ESNext",
"target": "ES5"
}
- Modify loader scripts
ts-loader
will cause the final files in/build
to bemain.js
,main.asset.php
, instead ofindex.js
,index.asset.php
- You could probably also fix this by monkey-patching the webpack config... For example, changing the
entry
to be map of names to entry points, instead of a single entry point, will emit different files
- You could probably also fix this by monkey-patching the webpack config... For example, changing the
Gutenberg Block Development - Random Tips
- Try out the
wp-scripts
package! It is clear WordPress is putting a lot of work into it, and I actually found it pretty enjoyable to work with, especially considering the alternative of rolling all my own config files, build tool setups, etc.- You still have to do some setup and tweaking, especially if you use TypeScript like I do, but it seems worth the effort.
- Take advantage of Gutenberg's pre-build React UI components!
- You can preview them all thanks to this hosted Storybook instance
- These are great for things like the settings sidebar, admin panels, and inputs, as they are pre-styled to match the WordPress admin style.
- There is usually documentation in each component sub-folder
Testing
Here are some guides on E2E testing with WP + Gutenberg Blocks:
- There is a great guide by Hunziker: "How to test your Gutenberg blocks with Jest and Puppeteer"
- @wordpress/scripts:
test-e2e
command - WordPress Core Announcements: "Introducing the WordPress e2e tests"
Testing - Tips and Troubleshooting
- Take advantage of
e2e-test-utils
!- You can also browse the source code
- How to disable the popup welcome-guide on the editor pages?
- This approach worked for me! For using with Puppeteer, just put that JavaScript inside of a call to
page.evaluate()
instead ofwp_add_inline_script
.
- This approach worked for me! For using with Puppeteer, just put that JavaScript inside of a call to
WordPress Gutenberg Troubleshooting
WordPress Gutenberg - Troubleshooting Dynamic Rendering
Error loading block: Invalid parameter(s): attributes
- Make sure that when you register the block in PHP, you pass in the associative array of attributes (even if you already passed them in when registering the block in JavaScript)
- This is noted here
Error loading block: The response is not a valid JSON response.
- Be careful about how you are returning the server-side generated HTML from your PHP function / method.
- Although it will work (generally) for the front-end of the site if you simply echo out raw HTML (e.g. by stopping and starting PHP interpretation -
... ?> <h1>Hello</h1> <?php ...
), this will break the API endpoint for previews, as it ends up with a non-serializable value. - The best-practice approach might be to manually build the string up, but a faster and easier approach might be to use output buffering to capture the generated HTML (like this)
- WordPress takes the return value of your php
render_callback
method and tries to stringifiy / serialize it as part of a JSON object, which it returns to the editor UI via the API endpoint.
- Although it will work (generally) for the front-end of the site if you simply echo out raw HTML (e.g. by stopping and starting PHP interpretation -
- Be careful about how you are returning the server-side generated HTML from your PHP function / method.
- You are trying to inject JavaScript via tags (e.g.
<script></script>
), but the code is not getting executed within the edit preview page- This is because the
ServerSideRender
component simply uses React'sdangerouslySetInnerHTML
attribute, which does not eval script content, due to this limitation - This is a hard one to work around... you could use a library component like this one to auto-extract the JS portion and run it, or manually setup a little pattern to extract the JS and run it through
eval()
.
- This is because the