Using block.json to register a custom Gutenberg block

The Block Editor is beginning to mature, as core contributors find creative ways to simplify block creation and improve performance. In WordPress 5.8, a new file called block.json was introduced to solve the frustration of having to register blocks in two different languages.

Before 5.8, if you wanted to create a dynamic block with attributes, you had to define the block’s attributes in both PHP and in JS. One slight difference in the attribute definitions, and your block wouldn’t work.

Benefits of block.json

  • You only have to edit one file to change attributes, icons, or other block settings.
  • PHP and JS automatically stay in sync.
  • Core registers and enqueues your scripts and styles for you.
  • Core automatically creates a version number for you, so you don’t have to use tricks like filemtime() to specify the version in PHP.

Example: register a block in two places

Let’s start with an example block. First, we’ll look at the old way of registering it in two places, and then we’ll see how to update it, using block.json to register it in a single place.

This is a really simplified block to make it easier to see the differences between the two methods. It allows the editor to enter a message, and then on the front end, it just states “You said (message).” But the principle is the same whether you’re outputting a small div or running complex queries and outputting intricately-styled content.

First let’s create a folder on a local site: /wp-content/plugins/old-dynamic-block/. Inside that folder, let’s add a plugin.php file:

<?php
/**
 * Plugin Name: Old Dynamic Block
 * Description: An example block that shows how to register a dynamic block separately, in PHP and JS.
 */

add_action( 'init', 'register_old_dynamic_block' );
function register_old_dynamic_block() {
	// First, we'll register our script.
	wp_register_script(
		'old-dynamic-block',
		plugins_url( 'build/index.js', __FILE__ ),
		array( 'wp-blocks', 'wp-components', 'wp-element', 'wp-editor' ),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
	);
	// Next, we'll register a stylesheet.
	wp_register_style(
		'old-dynamic-block-style',
		plugins_url( 'build/index.css', __FILE__ ),
		array(),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
	);
	// Now, let's register the block on the server.
	register_block_type(
		'happyprime/old-dynamic-block',
		array(
			'editor_script'   => 'old-dynamic-block',
			'style'           => 'old-dynamic-block-style',
			'attributes'      => array(
				'message' => array(
					'type'     => 'string',
					'default'  => 'nothing yet',
				),
			),
			'render_callback' => 'old_dynamic_callback',
		)
	);
}

function old_dynamic_callback( $attributes ) {
	return '<div class="old-dynamic-block">You said:<p>' . $attributes['message'] . '.</p></div>';
}
Code language: PHP (php)

Because using block.json later requires a build step, we’re also going to set up a short build step with this old-style block. So, let’s create a package.json file:

{
	"name": "old-dynamic-block",
	"version": "0.0.1",
	"description": "An example block that shows how to register a dynamic block separately, in PHP and JS.",
	"dependencies": {
		"@wordpress/scripts": "^18.0.1"
	},
	"scripts": {
		"build": "wp-scripts build src/*.js --output-path=build/"
	}
}
Code language: JSON / JSON with Comments (json)

Once that file is in place, you’ll need to open up a command line with Node and npm installed, go to your /wp-content/plugins/old-dynamic-block/ folder, and enter npm install.

Next, we’ll set up the block’s editor script. Let’s name this file /wp-content/plugins/old-dynamic-block/src/index.js:

import { registerBlockType } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';
import './index.css';

registerBlockType( 'happyprime/old-dynamic-block', {
	title: 'Old Dynamic Block',
	icon: 'star-half',
	category: 'widgets',
	attributes: {
		message: {
			type: 'string',
			default: 'nothing yet'
		},
	},
	edit: ( props ) => {
		const {
			attributes: { message },
			setAttributes,
			className,
		} = props;
		return (
			<RichText
				tagName="p"
				className={ className }
				onChange={ ( message ) => setAttributes( { message } ) }
				value={ message }
			/>
		);
	}
} );Code language: JavaScript (javascript)

If you’re wondering why there’s no save() function, it’s because this is a dynamic block. Our PHP render_callback will handle block output. Normally you wouldn’t create a dynamic block for something simple like this – instead, you’d create one for content that could change. For example, if you want to show recent content on your site, you could have the user select a post type in the Editor and then dynamically output the most recent posts of that type in your block. That way, the block will always display the most recently published posts – not the most recent posts at the time the block was added.

Finally, let’s add some very simple CSS in /wp-content/plugins/old-dynamic-block/src/index.css:

.old-dynamic-block {
	background: orange;
	color: white
}Code language: CSS (css)

Now that all the files are in place, if you go to /wp-content/plugins/old-dynamic-block/ in the command line and enter npm run build, you can then activate the plugin and try out the block.

Example: register a block with block.json

Now let’s see how to register the same block without having to duplicate its setup. We’ll start with a new folder: /wp-content/plugins/new-dynamic-block/.

First, let’s create the new block.json file.

{
	"apiVersion": 2,
	"name": "happyprime/new-dynamic-block",
	"title": "New Dynamic Block",
	"icon": "star-filled",
	"category": "widgets",
	"attributes": {
		"message": {
			"type": "string",
			"default": "nothing yet"
		}
	},
	"editorScript": "file:./build/index.js",
	"style": "file:./build/index.css"
}Code language: JSON / JSON with Comments (json)

Once again, you could make this more complex. If you need a script that runs on the front end, you can add a script definition. If you want a stylesheet just for the editor, you can add an editorStyle definition. You can also prevent people from editing your block as HTML with a supports definition. Whatever you need to define about this block, you do it here – with the exception of a render_callback since this is a dynamic block.

Next is our updated plugin.php file:

<?php
/**
 * Plugin Name: New Dynamic Block
 * Description: An example block that shows how to register a dynamic block in a single file, block.json.
 */

add_action( 'init', 'register_new_dynamic_block' );
function register_new_dynamic_block() {
	register_block_type_from_metadata(
		__DIR__,
		array(
			'render_callback' => 'new_dynamic_callback',
		)
	);
}

function new_dynamic_callback( $attributes ) {
	return '<div class="new-dynamic-block">You said:<p>' . $attributes['message'] . '.</p></div>';
}
Code language: HTML, XML (xml)

By calling register_block_type_from_metadata() instead of register_block_type(), WP knows to look for our block.json file. It parses it and adds all that information to the server-side registration. No more need to register our script or style – this is being done through block.json now – and no more need to redefine our block name and attributes.

Our package.json file is identical to the one for the old block – it just references the name and description of this block:

{
	"name": "new-dynamic-block",
	"description": "An example block that shows how to register a dynamic block in a single file, block.json.",
	"dependencies": {
		"@wordpress/scripts": "^18.0.1"
	},
	"scripts": {
		"build": "wp-scripts build src/*.js --output-path=build/"
	}
}
Code language: JSON / JSON with Comments (json)

Next, our new /wp-content/plugins/new-dynamic/block/src/index.js:

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

registerBlockType( metadata, {
	edit: ( props ) => {
		const {
			attributes: { message },
			setAttributes,
			className,
		} = props;
		const blockProps = useBlockProps();
		return (
			<div { ...blockProps }>
				<RichText
					tagName="p"
					className={ className }
					onChange={ ( message ) => setAttributes( { message } ) }
					value={ message }
				/>
			</div>
		);
	}
} );Code language: JavaScript (javascript)

We use the same call to registerBlockType(), but by passing in the metadata argument we’re telling WP to look for that same block.json file. WP parses block.json again, this time using it to register the block on the client side.

Here’s a touch of CSS to make it easier to tell our old block and our new block apart. In /wp-content/plugins/new-dynamic-block/src/index.css:

.new-dynamic-block {
	background: purple;
	color: white
}Code language: CSS (css)

Make sure to visit your /wp-content/plugins/new-dynamic-block/ folder in the command line and enter npm run build to package up the script and stylesheet, and make sure you activate this separate plugin.

If you add both of these blocks to a page, and enter the same message in both, you should see the same thing in each – with a different background color so you can tell which was registered with block.json (purple background) and which was registered in two separate places (orange background).

Drawbacks of block.json

At least, as of WP 5.8.1:

  • Requires a build step. (Most blocks use them these days, but without block.json it’s still possible to have JS that doesn’t require transpiling.)
  • Front end scripts are output in the <head>. It would be nice to have a way to enqueue them at the end of the page instead.

Conclusion

In my opinion, using block.json is a great way to simplify block registration and keep related code all in one place. I look forward to seeing how Core developers continue to add functionality to it.

4 Comments

Nice tutorial. I’d like to clarify one of the drawbacks listed:

> Requires a build step. (Most blocks use them these days, but without block.json it’s still possible to have JS that doesn’t require transpiling.)

In practice, you can always skip the build step and register the script with PHP function `wp_register_script` like in the first code example. This way, you would provide a script handle in the `block.json` file. The main benefit of using the build step is that you don’t have to maintain yourself the list of script dependencies and a unique version is generated each time the content of the script changes that will ensure that the browser always server the most recent content.

Nice! Thanks for correcting my wrong assumption. Now that I’ve built a number of blocks with webpack, I prefer having a build step for the reasons you mentioned, but when I first started out it felt easier not to have to set that up.

There is nothing wrong having the scripts in header aslong you use defer or async. Sadly Gutenberg does not. I think put script before in 2022 is outdated practise.

Leave a Reply