Creating a Gutenberg block using the official new package
I have finally started creating blocks for Gutenberg, the (not-so-new anymore) editor for WordPress. Since I have not coded with JavaScript in several years, and have not ventured into React yet, I had been wondering how easy it would be to create a new block.
As luck would have it, a new tool to scaffold Gutenberg blocks was released just a few days before my work started: the @wordpress/block package (also called @wordpress/create-block), which allows to create a new WordPress plugin containing all the necessary JavaScript, CSS and PHP files to register a block, with zero configuration.
It is not strictly needed to use a scaffolding project to create a new Gutenberg block. However, modern JavaScript-based projects depend on a great amount of tools to function adequately, and their set-up can be overwhelming: webpack to bundle the JavaScript files for distribution, Babel to compile the latest JavaScript code into code that can run in older browsers, ESLint to find problems in JavaScript code, and others.
Moreover, because these tools are being actively developed and new releases made available every few months, configuring them into our project is not a one-time task, but an ongoing problem. For this reason, a scaffolding tool which can set-up the initial configuration for the JavaScript project, and keep it always up-to-date using the latest improvements, is deeply needed (more for JavaScript newbies, like me). The @wordpress/block package was launched to accomplish this goal.
We also have other tools to create blocks, such as Ahmad Awais’ create-guten-block developer toolkit, and the WP-CLI scaffold block
command. However, the @wordpress/block package is an official tool by the WordPress project, so we can expect it to be supported in the long term, reflecting the needs of the Gutenberg project as steered by the team working on it.
Hence I decided to create my block using this new package. Following is my experience using this tool, all the problems I came across, how I solved them, and how my set-up looks like for my own development needs.
Scaffolding a new project
To create a new project, open a terminal window, browse to the folder where to create the new project, and then execute this command:
$ npm init @wordpress/block my-block
(You must have installed Node.js version 10.0.0 or above, and npm version 6.9.0 or above.)
Once installed, you can observe the changes done to your block’s JavaScript files and have them automatically compiled, by running this command:
$ cd my-block
$ npm start
Depending on your internet connection speed, the process may take several minutes (during which all dependencies are downloaded and installed in the node_modules
folder). In my case, it took around 4 minutes, hence the GIF below is so slow-moving (the official demo is much faster):
Inspecting the created project
We have by now scaffolded a new project. Let’s take a look at what files it is composed of, and what content they include:
my-block/
├──build/
│ ├── index.asset.php
│ └── index.js
├── src/
│ └── index.js
├── .gitignore
├── .editorconfig
├── package.json
├── package-lock.json
├── editor.css
├── style.css
└── my-block.php
File src/index.js
registers and renders the block in JavaScript, and this file gets compiled and included in the website as build/index.js
(this process is done automatically when running npm start
, or when running npm run build
for building the code for production). Files editor.css
and style.css
contain the block styles, just for the editor in the first case, and for both editor and client-side in the second case. Finally, file my-block.php
(which has the name we chose for the block when executing the npm init @wordpress/block
command) is the WordPress plugin file, which registers the block in the admin. All other files contain meta-data, and are not directly related to the block.
The content of the WordPress plugin file my-block.php
is the following:
/**
* Plugin Name: My Block
* Description: Example block written with ESNext standard and JSX support – build step required.
* Version: 0.1.0
* Author: The WordPress Contributors
* License: GPL-2.0-or-later
* Text Domain: create-block
*
* @package create-block
*/
/**
* Registers all block assets so that they can be enqueued through the block editor
* in the corresponding context.
*
* @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/applying-styles-with-stylesheets/
*/
function create_block_my_block_block_init() {
$dir = dirname( __FILE__ );
$script_asset_path = "$dir/build/index.asset.php";
if ( ! file_exists( $script_asset_path ) ) {
throw new Error(
'You need to run `npm start` or `npm run build` for the "create-block/my-block" block first.'
);
}
$index_js = 'build/index.js';
$script_asset = require( $script_asset_path );
wp_register_script(
'create-block-my-block-block-editor',
plugins_url( $index_js, __FILE__ ),
$script_asset['dependencies'],
$script_asset['version']
);
$editor_css = 'editor.css';
wp_register_style(
'create-block-my-block-block-editor',
plugins_url( $editor_css, __FILE__ ),
array(),
filemtime( "$dir/$editor_css" )
);
$style_css = 'style.css';
wp_register_style(
'create-block-my-block-block',
plugins_url( $style_css, __FILE__ ),
array(),
filemtime( "$dir/$style_css" )
);
register_block_type( 'create-block/my-block', array(
'editor_script' => 'create-block-my-block-block-editor',
'editor_style' => 'create-block-my-block-block-editor',
'style' => 'create-block-my-block-block',
) );
}
add_action( 'init', 'create_block_my_block_block_init' );
There are a few observations we can make here:
We must execute a search/replace to add our own namespace
To make it unique upon registration, every block is composed of two parts, a namespace and a block name, with shape namespace/block-name
. However, the npm init @wordpress/block
command only allows to specify the block name, but not the namespace! (Executing npm init @wordpress/block my-namespace/my-block
doesn’t work, I tried). Instead, it creates the block using the "create-block"
namespace, which is certainly not very helpful (I wonder if there is a plan to address this issue).
Hence, we need to do a search and replace of all files in the project, from strings "create-block"
and "create_block"
to our own namespace.
We may need to update the plugin name
Similarly to the previous case, we can’t indicate the plugin’s title when scaffolding the new project. Instead, it is created by transforming the block name into title-case form. For instance, since my block was called my-block
, the plugin title was called My Block
. If we need to update it, we must also do a search/replace in all files in the project (it appears in more than 1 file).
We must review the license
The default license is GPL-2.0-or-later, which may not apply for our plugin, so beware to change it before publishing the plugin in the directory.
It is a single-block plugin
The project is a plugin registering a single block. Let’s analyze this issue in detail in the section below.
Reconsidering the single-block plugin distribution model
The scaffolded new project has the shape of a single-block plugin, which is expected to become the default packaging for blocks for the upcoming block directory. This approach has benefits and drawbacks: on the positive side, it enables users to only install the required blocks, avoiding the bloat from installing unneeded blocks contained in bundles; on the negative side, bundling blocks often makes sense, since they may reuse functionality/code that can be shipped together in the same plugin, and having to enable many plugins (one per block) may be inconvenient.
There is an approach we can use to have the best of both worlds, being able to decide if to ship our blocks through single-block plugins, or through a block-bundle plugin, on a project-by-project basis: decouple the block logic from its registration in the WordPress admin into 2 separate entities, and merge them together through the PHP dependency manager Composer. This strategy is supported by the philosophy of a block: it must be an autonomous unit and context-agnostic, so it can be used, as much as possible, in a variety of different contexts.
(If you haven’t used Composer yet, I strongly recommend you learn about this tool first, since it can greatly ease setting-up and managing any PHP project. I wrote a guide on how to use it with WordPress here.)
Let’s re-architect our project into 2 separate entities next.
Re-architecting the structure of the new project
We will split the project into two:
- The Gutenberg block, autonomous of any plugin
- The WordPress plugin, which includes the Gutenberg block through Composer
Working demos for these 2 projects are located in these 2 repos (which I am currently developing): a GraphiQL block, accessible through the GraphiQL plugin.
Having the WordPress plugin project include the Gutenberg block project is best achieved through autoloading, enabled by following the PSR-4 convention. Using this architecture, the scaffolded files dealing with the block will live under the block project, and the single PHP file dealing with registering the WordPress plugin will live under the plugin folder, like this:
# Block project hierarchy
my-block/
├──build/
│ ├── index.asset.php
│ └── index.js
├── src/
│ ├── Block.php
│ └── index.js
├── .gitignore
├── .editorconfig
├── package.json
├── package-lock.json
├── editor.css
├── style.css
└── composer.json
# Plugin project hierarchy
my-plugin/
├── src/
│ └── Plugin.php
├── .gitignore
├── .editorconfig
├── my-plugin.php
└── composer.json
Notice the addition of file composer.json
to both projects. While this file for the block may be empty of dependencies, the plugin one will include the block as a dependency:
"require": {
"my-namespace/my-block": "^1.0"
}
The block registration logic is under class Block
:
namespace Leoloso\GraphiQLWPBlock;
class Block {
private $urlPath;
public function __construct(string $urlPath)
{
$this->urlPath = \trailingslashit($urlPath);
}
public function init(): void
{
// Initialize the GraphiQL
\add_action('init', [$this, 'initBlock']);
}
public function initBlock(): void
{
$dir = dirname(dirname( __FILE__ ));
$script_asset_path = "$dir/build/index.asset.php";
if ( ! file_exists( $script_asset_path ) ) {
throw new Error(
'You need to run `npm start` or `npm run build` for the "leoloso/graphiql" block first.'
);
}
// Load the block scripts and styles
$index_js = 'build/index.js';
$script_asset = require( $script_asset_path );
\wp_register_script(
'leoloso-graphiql-block-editor',
$this->urlPath.$index_js,
$script_asset['dependencies'],
$script_asset['version']
);
$editor_css = 'editor.css';
\wp_register_style(
'leoloso-graphiql-block-editor',
$this->urlPath.$editor_css,
array(),
filemtime( "$dir/$editor_css" )
);
$style_css = 'style.css';
\wp_register_style(
'leoloso-graphiql-block',
$this->urlPath.$style_css,
array(),
filemtime( "$dir/$style_css" )
);
\register_block_type( 'leoloso/graphiql', array(
'editor_script' => 'leoloso-graphiql-block-editor',
'editor_style' => 'leoloso-graphiql-block-editor',
'style' => 'leoloso-graphiql-block',
) );
}
}
Please notice that, because the block doesn’t live in a plugin anymore, it doesn’t know the full URL path to its own assets. Hence, it needs to receive this information by the plugin through variable urlPath
when initializing the block.
A Plugin
class can instantiate a new Block
by executing this code:
namespace Leoloso\GraphiQLBlockWPPlugin;
class Plugin {
public function init(): void
{
$urlPath = \plugins_url('vendor/leoloso/graphiql-wp-block', dirname(__FILE__));
(new \Leoloso\GraphiQLWPBlock\Block($urlPath))->init();
}
}
And finally, the WordPress plugin registration file will load Composer’s files and then create a new Plugin
instance:
/**
* Plugin Name: GraphiQL block
* Description: It adds a block to add a GraphiQL client, to query the GraphQL server
* Version: 0.1.0
* Author: Leonardo Losoviz
* License: MIT
* Text Domain: leoloso
*
* @package leoloso
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
define('GRAPHIQL_BLOCK_PLUGIN_URL', plugin_dir_url(__FILE__));
define('GRAPHIQL_BLOCK_VERSION', '0.1');
// Load Composer’s autoloader
require_once (__DIR__.'/vendor/autoload.php');
// Initialize this plugin
(new \Leoloso\GraphiQLBlockWPPlugin\Plugin())->init();
That’s it. Now, through Composer, we have been able to decouple the plugin from the block. If I ever need to include my GraphiQL block inside of a bundle, I only have to replicate the files inside the plugin project.
Observing changes in the source project and testing site
As mentioned earlier on, the block file that is included in the website is not src/index.js
but build/index.js
, which is automatically generated when running npm start
and modifying the source file. When developing the block, we must have build/index.js
be updated in our testing environment, as to be able to test the changes. However, our folder containing the block’s source files will most likely be a different one, where we store our repository files. Hence, we have a problem here: how can we modify file src/index.js
in our repository folder, and have it compiled to build/index.js
under the testing site’s folder?
The first solution that comes to mind is to depend on Composer: update the source files, commit/push them to the repo, and execute composer update
on the testing site to download the changes. The problem with this approach is that it makes development much slower, because Composer caches dependencies and refreshes them every 10 minutes or so, so we would have to wait up to 10 minutes to test a change (and, in addition, running composer update
may also take a minute to execute).
A better solution is to have the npm start
process compile the source files from their original location, but output them in the testing site folder. Luckily, script wp-scripts start
(which is the one executed when doing npm start
) can accept parameter --output-path
, indicating the folder on which to save the compiled files.
For this, we will need to modify file package.json
(which defines the JavaScript dependencies and allows to configure script shortcuts) in the block project, from this:
{
"scripts": {
"start": "wp-scripts start"
}
}
to this:
{
"scripts": {
"start": "wp-scripts start --output-path=${OUTPUT_PATH:=build/}"
}
}
In the latter case, we tell wp-scripts start
to place the compiled files under the folder defined in environment variable OUTPUT_PATH
or, if not provided, use "build/"
(which is the default case).
Now, we can tackle the two required possibilities (which can also be executed simultaneously, in 2 different terminal consoles):
- Run
npm start
to compilesrc/index.js
intobuild/index.js
in the source repo - Run
OUTPUT_PATH=/path/to/testing/site/build/ npm start
to compilesrc/index.js
into the testing site’sbuild/
folder, as to be able to test the changes
Enabling hot reloading
So far so good: we can modify our source files stored in the repository folder, and be able to test them immediately. However, “immediately” is not so immediate, because we need to refresh the browser in the WordPress editor screen to see the changes applied to the block. It would be much nicer to have the browser react to the changes automatically, without us having to press the reload button.
Luckily this is doable too! Even though not documented in the package’s site, the @wordpress/block package supports hot reloading, i.e. the ability to reflect the changes immediately whenever modifying a block’s JavaScript code.
To enable it, the WordPress editor must load script under http://localhost:35729/livereload.js
, like this:
if (is_admin() && defined('ENABLE_HOT_RELOADING_FOR_DEV') && ENABLE_HOT_RELOADING_FOR_DEV) {
wp_register_script('livereload', 'http://localhost:35729/livereload.js');
wp_enqueue_script('livereload');
}
And then, just for the development environment, add a constant ENABLE_HOT_RELOADING_FOR_DEV
to file wp-config.php
:
define('ENABLE_HOT_RELOADING_FOR_DEV', true);
Important: we may run npm start
several times to observe changes. Port 35729
will be tied to the first one of these instances. Hence, because we want to observe the changes on the testing site, we must first execute OUTPUT_PATH=/path/to/testing/site/build/ npm start
, and the hot reloading will react on these changes.
Now, whenever updating the block, the changes will be automatically visible in the browser:
Conclusion
Creating a new Gutenberg block is now easier than ever using the new @wordpress/block package. This tool is not perfect yet: for instance, it doesn’t allow to specify the block’s namespace (so we need to resort to a hacky search and replace), and we may want to modify the project’s output as to decouple the block and the plugin into 2 different entities. However, this tool is certainly good enough to quickly scaffold the new project, keep it updated, and speed up development with hot reloading.
I just learned that you can specify a namespace!
npx @wordpress/create-block block-name –namespace block-namespace
Thank you for a great article!
Here’s an alternative (and simpler way) to create more than one block in the generated project.
Create one directory for each block – e.g
src/blocks/a/
src/blocks/b/
etc
Implement each block as the generated example with edit.js, save,js, editor.scss, style.scss and an index.js but let index.js default export an init function that makes the registerBlockType( … ) call instead.
Remove the top level src/edit.js and src/save.js files
Modify src/editor.scss by removing the original code and then add
@import “blocks/a/editor”;
@import “blocks/b/editor”;
Same thing for src/style.scss
@import “blocks/a/style”;
@import “blocks/b/style”;
Modify src/index.js, remove the original code and add
import aInit from ‘./blocks/a’;
import bInit from ‘./blocks/b’;
aInit();
bInit();
That’s it! npm run build the usual way
🙂
/johan