Imagine your agency just picked up a new client. This client runs a book review website that needs a complete overhaul. They need to add some custom fields to associate with each book like a rating, author, length, and a fancy link to Goodreads.
Your agency is also transitioning from classic-based WordPress development to fully embracing everything the Block Editor has to offer. But the job parameters aren’t going to let you build custom blocks for every bit of metadata and customization the client needs. You need a solution that’s a bit more efficient.
That’s where the Block Bindings API comes in. I covered a basic introduction to this API in two previous posts:
- Introducing Block Bindings, part 1: Connecting custom fields
- Introducing Block Bindings, part 2: Working with custom binding sources
But the true power of Block Bindings is in combination with various other APIs and systems within WordPress, such as Block Variations, Patterns, and more. Once you start piecing them together, the potential for what you can achieve atop the block system feels nearly limitless.
So let’s build the foundation of your client’s book review site together. Then, I’ll let you take over and fine tune it.
Table of Contents
Prerequisites
This tutorial will cover a lot of ground, and I cannot explain every line of code in detail without it becoming overly complex. Therefore, you’ll need at least a cursory understanding of the following concepts. The goal of this series is to show how the various APIs in WordPress can be used in unison.
- Node and npm: You must have Node.js and npm installed on your local machine to follow this tutorial.
- JavaScript programming: You should have some baseline knowledge necessary to read, write, and understand intermediate JavaScript.
- Block Editor development: You should feel comfortable building code that runs in the WordPress Block Editor.
- Theme development: You should be able to build a block theme, work from its
functions.php
file, and set up a build process. - Build tools: You must know how to write commands from a Command Line Interface (CLI), such as
npm run <command>
.
Setting up your project
For this project, you will create a child theme for Twenty Twenty-Four named TT4 Book Reviews. This is so that you don’t have to recreate every part of the theme just to get things rolling. You can view the fully built example via its repository.
I suggest importing the example content directly into a clean development install. Note that the meta keys used for the posts have the themeslug_
prefix as used throughout this tutorial.
There is also a live demo using WordPress Playground if you want to see the final result:
File structure
Create a new folder named tt4-book-reviews
in your /wp-content/themes
directory. Also, go ahead and add the following sub-folders and files within your theme:
/patterns
/public
/resources
/js
editor.js
meta.js
query.js
variations.js
/templates
functions.php
package.json
style.css
webpack.config.js
This may seem like a lot, but most of the files that you’ll add have very little code. They will just be separated for their specific purpose.
The main stylesheet and functions file
Because you’re building this as a child theme of the default Twenty Twenty-Four theme, you first need to create your style.css
file in your theme folder and add a file header so WordPress recognizes it.
Add this code to your style.css
file:
/*!
* Theme Name: TT4 Book Reviews
* Description: A child theme for a book review site.
* Version: 1.0.0
* Template: twentytwentyfour
* Tags: full-site-editing
* Text Domain: themeslug
* Tested up to: 6.5
* Requires at least: 6.5
* Requires PHP: 7.4
* License: GNU General Public License v2.0 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
*/
You’ll also be working with functions.php
throughout this tutorial, so it’s a good idea to get this file set up too.
Add this code to your theme’s functions.php
file:
<?php
// Add your custom code below here.
Now visit the Appearance > Themes screen in your WordPress admin, locate your new TT4 Book Reviews theme, and activate it.
Build scripts
This tutorial requires some JavaScript, so it’s also a good idea to set up a build process at this early stage. This process has been previously covered in depth in Beyond block styles, part 1: using the WordPress scripts package with themes. It is also described in the Build Process documentation in the Theme Handbook.
Open your theme’s package.json
file to add a project name and these scripts:
{
"name": "your-project-name",
"scripts": {
"start": "wp-scripts start --webpack-src-dir=resources --output-path=public",
"build": "wp-scripts build --webpack-src-dir=resources --output-path=public"
}
}
Now, navigate to your wp-content/themes/tt4-book-reviews
directory on your computer via its command line program and type the following command to install the necessary packages:
npm install @wordpress/scripts @wordpress/icons path webpack-remove-empty-scripts --save-dev
Your package.json
file should now look like this:
{
"name": "your-project-name",
"scripts": {
"start": "wp-scripts start --webpack-src-dir=resources --output-path=public",
"build": "wp-scripts build --webpack-src-dir=resources --output-path=public"
},
"devDependencies": {
"@wordpress/icons": "^9.46.0",
"@wordpress/scripts": "^27.6.0",
"path": "^0.12.7",
"webpack-remove-empty-scripts": "^1.0.4"
}
}
There’s one more step that you’ll need to take to get the build process working with your theme, and that is setting up your theme’s webpack configuration file. For this tutorial, you’ll only need this for building editor scripts.
Open your theme’s webpack.config.js
file and add this code to it:
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' );
const path = require( 'path' );
module.exports = {
...defaultConfig,
...{
entry: {
'js/editor': path.resolve( process.cwd(), 'resources/js', 'editor.js' )
},
plugins: [
// Include WP's plugin config.
...defaultConfig.plugins,
// Removes the empty `.js` files generated by webpack but
// sets it after WP has generated its `*.asset.php` file.
new RemoveEmptyScriptsPlugin( {
stage: RemoveEmptyScriptsPlugin.STAGE_AFTER_PROCESS_PLUGINS
} )
]
}
};
Registering custom fields
Before even thinking about connecting custom fields to blocks, you must first register each meta key that you want to use. For this book review site, let’s stick to four fields that your client would need:
themeslug_book_author
: The book’s author name.themeslug_book_rating
: The user’s star rating for the book.themeslug_book_length
: The number of pages in the book.themeslug_book_goodreads_url
: The URL to the book’s page on Goodreads.com.
With the meta keys decided, it’s time to register them via the standard register_meta()
function. Add this code to your theme’s functions.php
file:
add_action( 'init', 'themeslug_register_meta' );
function themeslug_register_meta() {
register_meta( 'post', 'themeslug_book_author', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'wp_filter_nohtml_kses'
] );
register_meta( 'post', 'themeslug_book_rating', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'wp_filter_nohtml_kses'
] );
register_meta( 'post', 'themeslug_book_length', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'wp_filter_nohtml_kses'
] );
register_meta( 'post', 'themeslug_book_goodreads_url', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'esc_url_raw'
] );
}
For these fields to work alongside the Block Bindings API, you need to ensure that each registered meta key of both of these arguments is set to true
:
show_in_rest
: This is necessary for the custom fields to be usable over the REST API, which the Block Editor uses.single
: Multiple meta values for a single key are not currently supported by the Block Bindings API, so this needs to be set to a single value.
The keen-eyed among you may have noticed that a couple of the meta keys that are integers are defined as strings: themeslug_book_rating
and themeslug_book_length
. This is an unfortunate limitation of getting the values to appear in the editor when used with blocks that require stringed attributes. We’ll work around this later when we get into the meta input controls.
Problems and solutions: Connecting custom fields to blocks
WordPress 6.5+ supports connecting custom fields to blocks via the core/post-meta
binding source by default. This means that you don’t have to worry about registering anything via the Block Bindings API.
But, as described in Introducing Block Bindings, part 1: connecting custom fields, the only way to insert a bound block into the editor is by manually switching to the Code Editor view and typing out the block markup.
For example, here is what the markup looks like when binding the themeslug_book_author
custom field to the Paragraph block:
<!-- wp:paragraph {
"metadata":{
"bindings":{
"content":{
"source":"core/post-meta",
"args":{
"key":"themeslug_book_author"
}
}
}
}
} -->
<p></p>
<!-- /wp:paragraph -->
And the only way to input custom meta values is by enabling the Custom Fields panel and manually typing the keys and values:
As a reputable developer, you would never want to hand this experience over to a client and ask them to put in all that work. But these limitations were expected for the first version of the Block Bindings API. There wasn’t enough time to build a UI and perfect the user experience before the WordPress 6.5 launch.
A more fine-tuned user experience is expected for the Block Bindings API in the future, but it will happen over multiple WordPress releases. Subscribe to the Block Bindings API tracking ticket to follow along or get involved with the process.
As a developer, what can you do to start using—I mean really using—this API for real-world projects that you will pass over to clients? That’s the question that I seek to answer with this tutorial series, and the following sections will dive into solutions.
Using block variations to insert bound blocks
What if you could insert the bound blocks like any other block with no code editing involved? You can absolutely do this by registering a variation via the Block Variations API.
A block variation is nothing more than an alternative version of a block with a different set of default attributes than the original block. And a bound block is simply a block with a defined metadata.bindings
attribute.
That means that it’s possible to combine these two concepts. For example, you can see variations that represent the custom fields you registered earlier in this screenshot:
Instead of manually typing block markup each time you want to bind a block’s attributes, you can set up a variation for inserting it just like any other block. That will be much less of a headache for both you and your client.
Registering variations for bound blocks
To integrate bindings and variations, you must register your variations via JavaScript (the PHP registration method doesn’t support the isActive()
check needed for this technique).
If you haven’t already done so, kick-start the build process by typing the following in your command line program:
npm run start
This will generate the build files needed for the project under the /public
folder that you created earlier.
Before diving into JavaScript, you must first enqueue your newly generated /public/js/editor.js
file in the editor. Add this code to your functions.php
file:
add_action( 'enqueue_block_editor_assets', 'themeslug_editor_assets' );
function themeslug_editor_assets() {
$script_asset = include get_theme_file_path( 'public/js/editor.asset.php' );
wp_enqueue_script(
'themeslug-editor',
get_theme_file_uri( 'public/js/editor.js' ),
$script_asset['dependencies'],
$script_asset['version'],
true
);
}
Now add this code to your editor.js
file in /resources/js
to import the empty variations.js
script:
import './variations';
With this code in place, you can begin registering custom variations for any block that you want to bind to a custom field. For this tutorial, you will bind each of your custom fields to a single block attribute:
Custom Field | Block | Attribute |
---|---|---|
themeslug_book_author | Paragraph | content |
themeslug_book_rating | Paragraph | content |
themeslug_book_length | Paragraph | content |
themeslug_book_goodreads_url | Button | url |
Of course, you can bind your custom fields to any number of blocks that you need for your project.
First, open your variations.js
file and add the following code to import the dependencies you’ll need and to define a Goodreads icon:
import { registerBlockVariation } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { page, pencil, starFilled } from '@wordpress/icons';
const goodreadsIcon = (
<svg width="24" height="24" viewBox="0 0 24 24" version="1.1">
<path d="M17.3,17.5c-0.2,0.8-0.5,1.4-1,1.9c-0.4,0.5-1,0.9-1.7,1.2C13.9,20.9,13.1,21,12,21c-0.6,0-1.3-0.1-1.9-0.2 c-0.6-0.1-1.1-0.4-1.6-0.7c-0.5-0.3-0.9-0.7-1.2-1.2c-0.3-0.5-0.5-1.1-0.5-1.7h1.5c0.1,0.5,0.2,0.9,0.5,1.2 c0.2,0.3,0.5,0.6,0.9,0.8c0.3,0.2,0.7,0.3,1.1,0.4c0.4,0.1,0.8,0.1,1.2,0.1c1.4,0,2.5-0.4,3.1-1.2c0.6-0.8,1-2,1-3.5v-1.7h0 c-0.4,0.8-0.9,1.4-1.6,1.9c-0.7,0.5-1.5,0.7-2.4,0.7c-1,0-1.9-0.2-2.6-0.5C8.7,15,8.1,14.5,7.7,14c-0.5-0.6-0.8-1.3-1-2.1 c-0.2-0.8-0.3-1.6-0.3-2.5c0-0.9,0.1-1.7,0.4-2.5c0.3-0.8,0.6-1.5,1.1-2c0.5-0.6,1.1-1,1.8-1.4C10.3,3.2,11.1,3,12,3 c0.5,0,0.9,0.1,1.3,0.2c0.4,0.1,0.8,0.3,1.1,0.5c0.3,0.2,0.6,0.5,0.9,0.8c0.3,0.3,0.5,0.6,0.6,1h0V3.4h1.5V15 C17.6,15.9,17.5,16.7,17.3,17.5z M13.8,14.1c0.5-0.3,0.9-0.7,1.3-1.1c0.3-0.5,0.6-1,0.8-1.6c0.2-0.6,0.3-1.2,0.3-1.9 c0-0.6-0.1-1.2-0.2-1.9c-0.1-0.6-0.4-1.2-0.7-1.7c-0.3-0.5-0.7-0.9-1.3-1.2c-0.5-0.3-1.1-0.5-1.9-0.5s-1.4,0.2-1.9,0.5 c-0.5,0.3-1,0.7-1.3,1.2C8.5,6.4,8.3,7,8.1,7.6C8,8.2,7.9,8.9,7.9,9.5c0,0.6,0.1,1.3,0.2,1.9C8.3,12,8.6,12.5,8.9,13 c0.3,0.5,0.8,0.8,1.3,1.1c0.5,0.3,1.1,0.4,1.9,0.4C12.7,14.5,13.3,14.4,13.8,14.1z" />
</svg>
);
From this point, you must register variations for each of the bound blocks. Start by creating a variation for the Paragraph block named “Book Author”:
registerBlockVariation( 'core/paragraph', {
name: 'themeslug/book-author',
title: __( 'Book Author', 'themeslug' ),
description: __( 'Displays the book author.', 'themeslug' ),
category: 'widgets',
keywords: [ 'book', 'author' ],
icon: pencil,
scope: [ 'inserter' ],
attributes: {
metadata: {
bindings: {
content: {
source: 'core/post-meta',
args: {
key: 'themeslug_book_author'
}
}
}
},
placeholder: __( 'Book Author', 'themeslug' )
},
example: {},
isActive: (blockAttributes) =>
'themeslug_book_author' === blockAttributes?.metadata?.bindings?.content?.args?.key
});
For the most part, you can define this variation however you like, but there are two properties that you need to pay special attention to:
attributes
:metadata
: You must set thebindings
property in the same way that you’d define it at the block level with thecore/post-meta
source and the associated post meta key (themeslug_book_author
for the above variation).placeholder
: (Optional) When setting a placeholder, the text will appear in the editor whenever the user hasn’t yet set a meta value.
isActive()
: This callback checks if the block has the current variation. Therefore, it checks if the definedmetadata.bindings.content.args.key
value exists and matches the post meta key.
Now repeat the same process for the Book Length, Book Rating, and Book Goodreads Button variations:
registerBlockVariation( 'core/paragraph', {
name: 'themeslug/book-length',
title: __( 'Book Length', 'themeslug' ),
description: __( 'Displays the book length in pages.', 'themeslug' ),
category: 'widgets',
keywords: [ 'book', 'pages', 'length' ],
icon: page,
scope: [ 'inserter' ],
attributes: {
metadata: {
bindings: {
content: {
source: 'core/post-meta',
args: {
key: 'themeslug_book_length'
}
}
}
},
placeholder: __( 'Book Length', 'themeslug' )
},
example: {},
isActive: (blockAttributes) =>
'themeslug_book_length' === blockAttributes?.metadata?.bindings?.content?.args?.key
});
registerBlockVariation( 'core/paragraph', {
name: 'themeslug/book-rating',
title: __( 'Book Rating', 'themeslug' ),
description: __( 'Displays the book rating.', 'themeslug' ),
category: 'widgets',
keywords: [ 'book', 'rating', 'review' ],
icon: starFilled,
scope: [ 'inserter' ],
attributes: {
metadata: {
bindings: {
content: {
source: 'core/post-meta',
args: {
key: 'themeslug_book_rating'
}
}
}
},
placeholder: __( 'Book Rating', 'themeslug' )
},
example: {},
isActive: (blockAttributes) =>
'themeslug_book_rating' === blockAttributes?.metadata?.bindings?.content?.args?.key
});
registerBlockVariation( 'core/button', {
name: 'themeslug/book-goodreads-button',
title: __( 'Book Goodreads Button', 'themeslug' ),
description: __( 'Displays a button with the link to the Goodreads book URL.', 'themeslug' ),
category: 'widgets',
keywords: [ 'book', 'author' ],
icon: goodreadsIcon,
scope: [ 'inserter' ],
attributes: {
text: __( 'View on Goodreads →', 'themeslug' ),
metadata: {
bindings: {
url: {
source: 'core/post-meta',
args: {
key: 'themeslug_book_goodreads_url'
}
}
}
}
},
example: {},
isActive: (blockAttributes) =>
'themeslug_book_goodreads_url' === blockAttributes?.metadata?.bindings?.url?.args?.key
});
From this point forward, you no longer need to manually type block bindings into the Code Editor. You can insert them just like you would any other block.
Adding meta input controls
There’s still one major problem to solve. To enter custom field values, users must know how to turn on the Custom Fields panel in the editor and correctly type both the key and value fields.
Let’s fix that by adding custom controls in the sidebar like this:
First, add this code to your editor.js
file in /resources/js
to import the empty meta.js
script:
import './meta';
Then open your meta.js
file and import the following dependencies:
import { useEntityProp } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { __ } from '@wordpress/i18n';
import { starFilled } from '@wordpress/icons';
import { registerPlugin } from '@wordpress/plugins';
import {
RangeControl,
__experimentalInputControl as InputControl,
__experimentalNumberControl as NumberControl,
__experimentalVStack as VStack
} from '@wordpress/components';
The above code uses a few experimental components, which are needed to add the custom controls for meta input. There is currently an open tracking ticket that would remove the experimental designation from each of those that we’re using. If you’re uncomfortable using experimental components, you can always create the inputs using custom components.
To add controls to the post sidebar, the best option is to use the registerPlugin()
JavaScript function and its render
callback property. Don’t worry, you’re not registering a plugin that users must install and activate. You’re creating an editor plugin.
Open meta.js
and add the following code:
registerPlugin( 'tt4-book-reviews', {
render: () => {
// Add your custom code here.
}
} );
From this point forward, your remaining code will go inside of the render
callback.
Since you wouldn’t want your custom controls to be output for every instance of the editor, you first need to check the current post type to determine if this is the correct editor. You can do this with the useSelect()
hook. In this case, you only want the controls to appear when post
is the current post type. If it’s not, the render
callback will return null
.
You’ll also need to use the useEntityProp()
hook to get and set meta values by assigning a couple of constants: meta
and setMeta
.
Add this code inside your render
callback:
const postType = useSelect(
( select ) => select( 'core/editor' ).getCurrentPostType(),
[]
);
const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
if ( 'post' !== postType ) {
return null;
}
From this point, it’s just a matter of returning the components to output the meta controls. The most important thing is to use the PluginDocumentSettingsPanel
SlotFill so that they are added to the correct location in the editor.
For this sidebar panel, you’ll use the following components and associate them with your meta keys:
<RangeControl>
:themeslug_book_rating
<InputControl>
:themeslug_book_author
<NumberControl>
:themeslug_book_length
<InputControl>
:themeslug_book_goodreads_url
Add the following code inside your render
callback:
return (
<PluginDocumentSettingPanel
title={ __( 'Book Review', 'themeslug' ) }
>
<VStack>
<RangeControl
label={ __( 'Rating', 'themeslug' ) }
beforeIcon={ starFilled }
currentInput={0}
initialPosition={0}
min={ 0 }
max={ 5 }
step={ 1 }
value={ parseInt( meta?.themeslug_book_rating, 10 ) }
onChange={ ( value ) => setMeta( {
...meta,
themeslug_book_rating: `${ value }` || null
} ) }
/>
<InputControl
label={ __( 'Author', 'themeslug' ) }
value={ meta?.themeslug_book_author }
onChange={ ( value ) => setMeta( {
...meta,
themeslug_book_author: value || null
} ) }
/>
<NumberControl
label={ __( 'Total Pages', 'themeslug' ) }
min={ 0 }
value={ parseInt( meta?.themeslug_book_length, 10 ) }
onChange={ ( value ) => setMeta( {
...meta,
themeslug_book_length: `${ value }` || null
} ) }
/>
<InputControl
label={ __( 'Goodreads URL', 'themeslug' ) }
value={ meta?.themeslug_book_goodreads_url }
onChange={ ( value ) => setMeta( {
...meta,
themeslug_book_goodreads_url: value || null
} ) }
/>
</VStack>
</PluginDocumentSettingPanel>
);
There are two values I want you to pay attention to in the above code for the Rating and Total Pages controls:
value
onChange
Those controls handle integer values but pass strings back when setting the meta. Take a look at how these are handled for the Rating:
value={ parseInt( meta?.themeslug_book_rating, 10 ) }
onChange={ ( value ) => setMeta( {
...meta,
themeslug_book_rating: `${ value }` || null
} ) }
Note that it uses parseInt()
for the value
property to pass an integer to the control. For the onChange
property, the value
variable is passed a template literal so that it’s treated as a string. This is necessary for the value to show in the editor and be saved correctly.
As you add data to the meta fields, you should see them automatically appear for any bound blocks in the content area of the editor:
The next steps
I would consider what we’ve accomplished thus far to be the “hard” part. You now have all the pieces in place to do the fun stuff like front-end design work.
Stay tuned into the Developer Blog for the next post in this series. Some items I’ll cover next include:
- Displaying posts via the Query Loop block by meta.
- Integrating custom fields into your theme templates.
- Building patterns for displaying custom fields.
Props to @bph and @ndiego for reviewing this tutorial and @welcher for code examples.
Leave a Reply