WordPress has always been incredibly flexible and customizable. The Block and Site Editors are expanding our abilities to create custom experiences, and the ability to create custom blocks only enhances that ability.
Currently, the create-block
utility only allows for creating one block at a time and doesn’t give the option for a multi-block setup. For example, what if you want to create two or more related blocks, especially where one custom block would use another to curate a user’s experience? What if you aim to use post meta to provide extra context to a user? These are common use cases and the area in which WordPress has always excelled.
Below, you’ll learn how to structure a multi-block approach using the create-block
utility.
Table of Contents
Let’s keep that going by setting up the ability to review a post or custom post type (CPT) with a rating system and include that in another custom block that can be used in a Query Loop. This could be used with your content when creators publish reviews (books, movies, products) and need a rating system to display with their posts. The end result should look like this:
Specifically, you’ll build:
- A Review Card block with three specific inner blocks:
- Title (
core/post-title
) - Rating block (a block we’ll make as well)
- Excerpt (
core/post-excerpt
)
- Title (
- A Rating block that will:
- Rate a post (or CPT) from one to five stars or hearts
- Use post meta to store the rating
- Use the block in the Post Query block to show the rating of each post.
You’ll need a local environment set up and a terminal with NodeJS installed to run commands. If you don’t already have a local environment set up, you can use wp-now, LocalWP, or another option listed here that would suit your needs.
If you would like to follow along with the finished code, it is available in this Github repo and will be updated as needed; PRs welcome!
Setting up a multi-block plugin
Let’s start with setting up our project structure. In your terminal, cd into your wp-content/plugins
directory. If you’re using wp-now
you can work from whichever directory you like.
Scaffold the plugin files
Run npx @wordpress/create-block@latest post-review-blocks
. This will scaffold a block plugin called “Post Review Blocks” in a directory named post-review-blocks
. Go into that directory and open it in your code editor of choice. You should see a post-review-blocks.php
file. Open that, and your code should look like the following (without the default comments):
/**
* Plugin Name: Post Review Blocks
* Description: Example block scaffolded with Create Block tool.
* Requires at least: 6.1
* Requires PHP: 7.0
* Version: 0.1.0
* Author: The WordPress Contributors
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: post-review-blocks
*
* @package CreateBlock
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
function create_block_post_review_blocks_block_init() {
register_block_type( __DIR__ . '/build );
}
add_action( 'init', 'create_block_post_review_blocks_block_init' );
The plugin itself should have a file structure like this:
- build/
- src/
- .editorconfig
- .gitignore
- .package-lock.json
- .package.json
- post-review-blocks.php
- readme.md
You will be working in the post-review-blocks.php
file and src
directory for this tutorial, and the build
directory will get built automatically.
Scaffold the blocks
Next, delete all the files in the src
directory so that the src
is empty, and cd
into the src
if you’re not already there.
Run the following two commands:
npx @wordpress/create-block@latest review-card-block --no-plugin
npx @wordpress/create-block@latest rating-block --no-plugin --variant dynamic
This will create the two custom blocks needed for our multi-block setup. Note the --no-plugin
to both commands. The flag indicates that this is just creating the block files, not all the plugin files needed. Also, you can see the rating-block
will be a “dynamic” block, rendering with PHP. Why? This allows you to get practice with both static and dynamic blocks.
Now, there are two blocks in our src
folder:
rating-block
review-card-block
You can take care of a few more things:
- Go into the
block.json
file for therating-block
and change the “icon
” property from “smiley
” to “star-filled
”. - In both of the
block.json
files for each block, add the “keywords
” property with"keywords": ["rating", "review"]
. Your users will find the new blocks easier when searching. - In the
post-review-blocks.php
file, updatecreate_block_post_review_blocks_block_init
to register both blocks, like this:
function create_block_post_review_blocks_block_init() {
register_block_type( __DIR__ . '/build/rating-block' );
register_block_type( __DIR__ . '/build/review-card-block' );
}
add_action( 'init', 'create_block_post_review_blocks_block_init' );
Build the blocks and activate the plugin
Now, from the root of the post-review-blocks
plugin (the “root” is at the same level as the package.json
file), run npm start
,and the blocks should build into their own sub-directories in build
. You can leave this script running for the remainder of the tutorial. Alternatively, you can stop and restart it if you want to run other commands or when you make changes to files like block.json
. The script needs to be running for any changes to appear in the editor.
Once the build is successful, activate the plugin either in the WordPress dashboard or via WP-CLI with wp plugin activate post-review-blocks
.
At this point, you can check that the blocks are registering by creating a new post and checking the inserter for the blocks by typing in one of the keywords we used:
Success! 🎉
You now know how to set up the structure of a multi-block plugin. You can add new blocks to the src
folder with create-block
and register their generated scripts in the build
directory.
Now it’s time to add post meta functionality, assign inner blocks, and limit where the blocks can be used.
Registering post meta
Open up the post-review-blocks.php
again, and paste the following after the create_block_post_review_blocks_block_init
function:
// Add some post meta
function register_review_rating_post_meta() {
$post_meta = array(
'_rating' => array( 'type' => 'integer' ),
'_ratingStyle' => array( 'type' => 'string' ),
);
foreach ( $post_meta as $meta_key => $args ) {
register_post_meta(
'post',
$meta_key,
array(
'show_in_rest' => true,
'single' => true,
'type' => $args['type'],
'auth_callback' => function() {
return current_user_can( 'edit_posts' );
}
)
);
}
}
add_action( 'init', 'register_review_rating_post_meta' );
This function registers two post meta keys you will need:
_rating
_ratingStyle
These need to be registered. Otherwise, the data won’t be saved when you update our Rating block. You’ll also notice the two meta keys are prefixed with an underscore: _
. This “protects” the meta from being used in the post’s custom fields section and potentially overwritten by the value in that meta box.
Finally, note that show_in_rest
is set to true, and the auth_callback
checks to make sure the user has at least edit_posts
privileges. If the meta does not show up in the WordPress REST API, it cannot be edited in the block editor.
A quick note beyond the scope of this tutorial: Be cautious with the data shown in the REST API. If you need to filter data out of the public API, do so in PHP elsewhere or choose a different method for working with sensitive data.
Building the Rating block
The Rating block allows your users to rate a post on a scale from one to five and choose between displaying a star emoji (⭐) or a heart emoji (❤️). You can copy these emojis into your code. With this functionality, the block accomplishes two objectives:
- Demonstrate how to save and pull from post meta
- Allow the post meta to be used in a Query Loop Post Template
There are many applications for this kind of functionality with CPTs, including:
- Staff directories with
email
orposition
meta - Book catalogs with
rating
orISBN
meta - Recipe indexes with
tastiness
orprep_time
meta
The possibilities are numerous!
Ok, all the edits in this section will be within the src/rating-block
directory.
Add basic CSS
The following CSS is for the styles.scss
file. Open that file, remove any CSS in there, and paste in the following, which adds padding around the block and ensures the star is yellow and the heart is red.
.wp-block-create-block-rating-block {
padding: 1rem 0;
}
.wp-block-create-block-rating-block span.rating-star {
color: yellow;
}
.wp-block-create-block-rating-block span.rating-heart {
color: red;
}
Adjust these to your liking and save the file.
Add attributes and usesContext to the block.json file
Open up the block.json
file for the Rating block and add the following properties:
"usesContext": ["postId", "postType"],
"attributes": {
"rating": {
"type": "integer",
"default": 5
},
"ratingStyle": {
"type": "string",
"default": "star"
}
},
"example": {
"attributes": {
"rating": 4,
"ratingStyle": "star"
}
}
The JSON above does the following:
usesContext
: allows us to get the values of the post’s ID and typeattributes
: identifies the properties to be saved on the blockexample
: gives a preview of what the block could look like when added
Click here to see the final block.json
file.
Update the edit.js file
There is a lot to tackle. You can break it into three sections:
- Import and assignments
- The functionality of retrieving and storing the post meta
- Return method with the components in the editor sidebar
The full file can be found on GitHub.
Delete what is currently in the file. Then, at the top of the edit.js
file, add the following:
import { __ } from "@wordpress/i18n";
import { useEffect } from "@wordpress/element";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import { PanelBody, RangeControl, SelectControl } from "@wordpress/components";
import { useEntityProp } from "@wordpress/core-data";
import "./editor.scss";
This section sets up our imports:
__
is our internationalization methoduseEffect
allows you to update metadatauseBlockProps
gives you the block properties to work withInspectorControls
allows you to add controls to the Inspector sidebarPanelBody
,RangeControl
, andSelectControl
are all components to set up the user controls for the properties of the blockuseEntityProp
provides access to the post meta- And finally, the SCSS file is imported
Next, add the following:
export default function Edit( {
attributes: { rating, ratingStyle },
setAttributes,
context: { postType, postId },
} ) {
const [meta, updateMeta] = useEntityProp(
"postType",
postType,
"meta",
postId,
);
// Add functionality code here
// Add return() method here
// Other code will go here, don't forget or delete the closing curly brace!
}
This is the Edit
method, which controls what shows up in the block editor. First, you pass in and assign the following:
rating
andratingStyle
get passed in and assigned to the attributes state objectsetAttributes
is the method by which theattributes
state object gets updatedpostType
and postId are passed in with context from theusesContext
you defined above
Next, you see that the state object for meta
and the updateMeta
method are getting assigned from the useEntityProp
method, which is used by the block to get or change meta values.
Ok, so far so good? Now for the functionality!
In place of the “Add functionality code here” comment, add the following methods:
useEffect(() => {
const initStyle = meta?._ratingStyle ? meta?._ratingStyle : "star";
setAttributes({
rating: meta?._rating || 0,
ratingStyle: initStyle,
});
}, []);
const onChangeRating = (val) => {
updateMeta({
...meta,
_rating: val,
});
setAttributes({
rating: val
});
};
const onChangeRatingStyle = (val) => {
updateMeta({
...meta,
_ratingStyle: val,
});
setAttributes({
ratingStyle: val
});
};
const getRatingEmojis = () => {
let ratingEmojis = "";
for (let i = 0; i < rating; i++) {
ratingEmojis += ratingStyle === "star" ? "⭐" : "❤️";
}
return ratingEmojis;
};
What do each of these methods do?:
useEffect
runs on load (given the second parameter is[]
) and checks for and assigns values from the stored post meta forrating
andratingStyle
, defaulting to the star (⭐)onChangeRating
will update the value of the rating when it’s changed in the InspectoronChangeRatingStyle
does the same for theratingStyle
getRatingEmojis
is a loop that returns the correct number of and style of stars or hearts for the rating.
Finally, add the following in place of the comment “Add return() method here”:
return (
<>
<InspectorControls>
<PanelBody title={ __( "Rating", "multiblock-plugin" ) }>
<RangeControl
label={ __( "Rating", "multiblock-plugin") }
value={ rating }
onChange={ onChangeRating }
min={ 1 }
max={ 5 }
/>
<SelectControl
label={ __( "Rating Style", "multiblock-plugin" ) }
onChange={ onChangeRatingStyle }
value={ ratingStyle }
options={ [
{
label: __( "Star" , "multiblock-plugin" ),
value: "star",
},
{
label: __( "Heart", "multiblock-plugin" ),
value: "heart",
},
] }
/>
</PanelBody>
</InspectorControls>
<div { ...useBlockProps() }>
<p>
<strong>Rating:</strong>{ " " }
<span className={ `rating-${ratingStyle}` }> { getRatingEmojis() }</span>
</p>
</div>
</>
);
The return function here has two main parts: the InspectorControls
and the actual output of the block for the editor.
Within InspectorControls
, we create a PanelBody
that contains a RangeControl
and a SelectControl
. The RangeControl
determines the value one to five of the ratings given to the post or CPT. The SelectControl
determines whether to show star or heart emojis.
The final div
is the output where we show the label “Rating:” and then pass in the correct style and number of stars or hearts.
Once you save this and add the block to a post, you should see the following:
If you slide the range slider or switch between hearts and stars, you should see the stars or hearts update in real time.
For more info on adding and using post meta, check out this great tutorial.
Update the render.php file
Since you created this block as a dynamic block, you need to implement how the rating will display on the front end. You can delete the code in the render.php
file and add the following:
<?php
global $post;
$ratingEmojis = '';
// Get the rating and rating style from the post meta
$rating = get_post_meta( get_the_ID(), '_rating', true );
$ratingStyle = get_post_meta( get_the_ID(), '_ratingStyle', true );
// If the rating style is not set, default to star
if ( ! $ratingStyle ) {
$ratingStyle = 'star';
}
// Generate the rating emojis.
for ( $i = 0; $i < $rating; $i++ ) {
$ratingEmojis .= $ratingStyle === 'star' ? '⭐️' : '❤️';
}
?>
<p <?php echo get_block_wrapper_attributes(); ?>>
<?php echo wp_kses_post( '<strong>Rating:</strong> <span class="rating-' . $ratingStyle . '">' . $ratingEmojis . '</span>', 'multiblock-plugin' ); ?>
</p>
A couple of things to note here:
- This comes in the context of a post, so we can use
get_the_ID()
- The block gets wrapped with classes and other attributes with
get_block_wrapper_attributes()
This means you now have a functional Rating block on the front end as well:
Fantastic!
If you would like to check your work, you can see the final file on GitHub. Otherwise, it’s time to move on to the next block in our multi-block setup.
Building the Review Card block
This block will involve a lot less code and is more about configuration. We want to do a few things with this block:
- Assign specific blocks as “inner blocks” to the Review Card
- Only allow certain blocks within the Review Card
- Limit where this card can be used, namely, as a post template in the Query Loop block
There are no styles needed for this block, so delete the contents of the .scss
files or style them to your liking.
All the code in this section will be edited in the src/review-card-block
directory.
Updating the block.json file
You need to start by setting a few properties in the block.json file
. Open that up and add the following:
"parent": ["core/post-template"],
"allowedBlocks": [
"core/post-title",
"create-block/rating-block",
"core/post-excerpt"
]
First, add a parent block property indicating that the Review Card can only be used in the core/post-template
block. Second, only three blocks are allowed to be used in the Review Card block itself. You will see this in action below.
If needed, compare your settings here with the file in the Github repo.
InnerBlocks in edit.js
In the edit.js
file, replace all the code with the following:
import { __ } from "@wordpress/i18n";
import { InnerBlocks, useBlockProps } from "@wordpress/block-editor";
import "./editor.scss";
const REVIEW_TEMPLATE = [
[ "core/post-title", { isLink: true, placeholder: "The Post Title" } ],
[ "create-block/rating-block", {} ],
[ "core/post-excerpt", { placeholder: "The pose content..." } ],
];
export default function Edit() {
return <InnerBlocks template={REVIEW_TEMPLATE} templateLock="all" />;
}
The main difference in this file is that you are importing InnerBlocks
and assigning a template of blocks as inner blocks to the Review Card block.
Next, REVIEW_TEMPLATE
is an array of arrays, the first index being the block you want to use and the second being an object with any configuration you wish to pass through. For example, you can see the core/post-title
block is taking in two defaults:
isLink
is set totrue
, meaning the title is clickable and will take you through to the postplaceholder
if, for some reason, there’s no title
Then, you can see that the InnerBlocks
component is used in the Edit
function, passing in the REVIEW_TEMPLATE
and also locking the blocks. This is just to show you how to lock the blocks in the template if the situation calls for it, but it is not required.
Edit the save.js file
Since the post-review-block
is a static block, you’ll need to edit the save.js
file to allow saving the block to the database. Open up that file, delete the contents, and add the following:
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
}
In this case, since the InnerBlocks
are set up and used, all that is needed is to pass in InnerBlocks.Content
. After saving your file, return to the block editor.
Testing the blocks
If you haven’t already, create two or three posts with different ratings for each post using the new ratings block. Publish those posts.
Then, open a new page in the dashboard and add a title for the page. “A review of posts…” is what is in the screenshots below. Next, add a Query Loop block. Select Start Blank and then pick an option like Title and Date.
Open the List View, and you should see something like this:
Now:
- Click on the Title block in the Post Template so that it is highlighted
- Then click on the Inserter (the blue “+” button) and search for Review Card block
- Once you see it in the list, click on it and go back to the List View, where you should see the Review Card block
Save or publish the page.
You will notice that no rating is showing up in the block editor. This is because the Rating block is rendered with PHP and is built dynamically, so a refresh is required. On refreshing the page, you should see the different reviews for the different posts:
Awesome!
One thing to note, if you open the List View, you will see the locks on the inner blocks of the Review Card block. These were set earlier with templateLock="all"
and cannot be moved around or deleted.
Similarly, if you try moving the Review Card block out of the Post Template block, you will see that it is also not allowed since the block requires its parent to be a Post Template block.
Excellent! You now have a working multi-block plugin that uses post meta, InnerBlocks
, and is explicit about which blocks are allowed within the InnerBlocks. Nice work!
One more thing to cover, and you should be on your way to block customization bliss.
Limiting the usage of the Rating block
You may have already been thinking this, but the current setup of the Rating block should really be limited to use on posts or other CPTs. To achieve this, open up the post-review-blocks.php
one more time and add the following:
function limit_rating_block_to_post_type( $allowed_block_types, $editor_context ) {
// Only allow paragraphs, headings, lists, and the rating block in the post editor for Posts.
if ( 'post' === $editor_context->post->post_type ) {
return array(
'core/paragraph',
'core/heading',
'core/list',
'create-block/rating-block'
);
}
return $allowed_block_types;
}
add_filter( 'allowed_block_types_all', 'limit_rating_block_to_post_type', 10, 2 );
With this filter, you can specify when and where the Rating block will be available to insert. In this case, the code gives the example that only Paragraph, Heading, List, and Rating blocks can be used in posts. If you open a new post and click on the blue + inserter or type “/” to insert a block, you will see the options are now very limited:
This is something to consider, especially for CPTs where you may only want a limited number of core or custom blocks to show up as options for users.
Wrapping up
There is a lot to consider when building multiple custom blocks. Hopefully, this tutorial has helped organize some approaches and given you a multi-block blueprint for moving forward with your own customizations.
This is the repository with the code if you would like to clone it or fork it for your needs.
Good luck and have fun!
Resources to learn more
These are some helpful resources used to develop this tutorial. Explore these to deepen your block knowledge:
- @wordpress/create-block
- Core Blocks Reference
- Metadata in block.json
- Nested Blocks: Using InnerBlocks
- Creating a custom block that stores post meta
Props @bph, @rmartinezduque , @ndiego, and @welcher for proofreading and all the helpful suggestions!
Leave a Reply