20
Create a Custom Gutenberg Block Plugin with Underpin
This tutorial covers how to set up a custom Gutenberg block plugin in WordPress using Underpin. This plugin can hold all of your site’s custom Gutenberg blocks, and allows you to bring your custom blocks with you when you change themes in the future.
If you haven’t already, the first thing you need to-do is actually install Underpin on your site. Underpin uses Composer to handle its dependencies, and it can be loaded in as a Must-Use plugin, or directly inside of your plugin. It really just depends on where you have Composer set up on your site. I like to install it as a mu-plugin for personal sites, and directly inside the plugin when I’m building a plugin that is intended to be distributed. In this lesson, I’ll assume you’re going to use it as a must-use plugin.
If you already have Underpin installed, you can skip this entirely.
Inside the wp-content
directory, create a new directory called mu-plugins. If mu-plugins
already exists, then skip this step.
Now create a composer.json
file inside the mu-plugins
directory. Most of this is the default settings for a composer.json
file, the key difference is that extra.installer-paths
is tweaked to force wordpress-muplugins
to simply be installed directly in the vendor
directory. This is necessary because Underpin is considered a mu-plugin
by Composer, and will install in an improper directory, otherwise.
{
"name": "wpda/muplugin",
"type": "plugin",
"require": {},
"extra":{
"installer-paths": {
"vendor/{$vendor}/{$name}": ["type:wordpress-muplugin"]
}
}
}
Next, create a new PHP file inside the mu-plugins
directory. It can named be whatever you want it to be. WordPress will automatically include this file, and run it on every page load. This happens really early in WordPress’s runtime so there are some limitations to this, but for our needs it’s perfect.
Use this code to include composer’s autoloader. This will automatically install and set up Underpin so you can use it anywhere else. This includes any plugins custom to this site, or your theme. Essentially, this makes Underpin as close to core functionality as possible. The only caveat is you have to remember to upload the mu-plugins directory to your live site.
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Load Underpin, and its dependencies.
$autoload = plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';
require_once( $autoload );
Now, let’s install Underpin. Open up your command-line, navigate to your site’s mu-plugins
directory, and then run this command:
composer require underpin/underpin
Booya! You now have Underpin installed. Now, let’s use it in a plugin.
A fundamental practice of WordPress is to create WordPress plugins for each piece of custom functionality for your site. This allows you to enable/disable these plugins as-needed, and potentially re-use the plugin on other sites in the future.
We’re going to use Underpin’s plugin boilerplate to help set up this plugin quickly. This plugin does a few key things for us:
- It sets up our Underpin plugin instance
- It comes with a WordPress-ready Webpack config
- It sets up the file headers that WordPress needs in-order to recognize the file as a plugin
To use this, navigate to wp-content/plugins
and clone the boilerplate. Then you’ll need to-do a few find/replaces in the boilerplate.
- Replace
plugin-name-replace-me
withcustom-blocks
(it can be whatever you want, just make sure spaces use dashes, and it’s all lowercase) - Replace
Plugin Name Replace Me
withCustom Blocks
(Again, whatever you want just has to use spaces and Title Case) - Replace
plugin_name_replace_me
withcustom_blocks
(Same thing applies here, but you should use snake_case) - Replace
Plugin_Name_Replace_Me
withCustom_Blocks
(using Upper_Snake_Case)
At this point, your plugin’s bootstrap.php
file should look something like this:
<?php
/*
Plugin Name: Custom Blocks
Description: Plugin Description Replace Me
Version: 1.0.0
Author: An awesome developer
Text Domain: custom_blocks
Domain Path: /languages
Requires at least: 5.1
Requires PHP: 7.0
Author URI: https://www.designframesolutions.com
*/
use Underpin\Abstracts\Underpin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Fetches the instance of the plugin.
* This function makes it possible to access everything else in this plugin.
* It will automatically initiate the plugin, if necessary.
* It also handles autoloading for any class in the plugin.
*
* @since 1.0.0
*
* @return \Underpin\Factories\Underpin_Instance The bootstrap for this plugin.
*/
function custom_blocks() {
return Underpin::make_class( [
'root_namespace' => 'Custom_Blocks',
'text_domain' => 'custom_blocks',
'minimum_php_version' => '7.0',
'minimum_wp_version' => '5.1',
'version' => '1.0.0',
] )->get( __FILE__ );
}
// Lock and load.
custom_blocks();
With Underpin, everything is registered using an Underpin loader. These loaders will handle actually loading all of the things you need to register. Everything from scripts, to blocks, even admin pages, can all be added directly using Underpin’s loaders. Loaders make it so that everything uses an identical pattern to add items to WordPress. With this system, all of these things use nearly exact same set of steps to register.
To build a Gutenberg block, we need to add at least two loaders, but you usually end up needing three.
- A block loader
- A script loader
- A style loader (optional)
First things first, install the block loader. In your command line, navigate to your mu-plugins
directory and run this command:
composer require underpin/block-loader
This will install the loader necessary to register blocks in Underpin. Now that it’s installed, you can register your block by chaining custom_blocks
like so:
// Registers block
custom_blocks()->blocks()->add( 'my_custom_block', [
'name' => 'My Custom Block', // Names your block. Used for debugging.
'description' => 'A custom block specific to this site.', // Describes your block. Used for debugging
'type' => 'custom-blocks/hello-world', // See register_block_type
'args' => [], // See register_block_type
] );
Let’s break down what’s going on above.
-
custom_blocks()
actually retrieves this plugin’s instance of Underpin -
blocks()
Retrieves the loader registry for this instance of Underpin -
add()
actually adds this block to the registry
Behind the scenes, Underpin will automatically create an instance of Block, which then automatically runs register_block_type
using the provided args
and type
.
At this point, your plugin’s bootstrap.php
will look like this:
<?php
/*
Plugin Name: Custom Blocks
Description: Plugin Description Replace Me
Version: 1.0.0
Author: An awesome developer
Text Domain: custom_blocks
Domain Path: /languages
Requires at least: 5.1
Requires PHP: 7.0
Author URI: https://www.designframesolutions.com
*/
use Underpin\Abstracts\Underpin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Fetches the instance of the plugin.
* This function makes it possible to access everything else in this plugin.
* It will automatically initiate the plugin, if necessary.
* It also handles autoloading for any class in the plugin.
*
* @since 1.0.0
*
* @return \Underpin\Factories\Underpin_Instance The bootstrap for this plugin.
*/
function custom_blocks() {
return Underpin::make_class( [
'root_namespace' => 'Custom_Blocks',
'text_domain' => 'custom_blocks',
'minimum_php_version' => '7.0',
'minimum_wp_version' => '5.1',
'version' => '1.0.0',
] )->get( __FILE__ );
}
// Lock and load.
custom_blocks();
// Registers block
custom_blocks()->blocks()->add( 'my_custom_block', [
'name' => 'My Custom Block', // Names your block. Used for debugging.
'description' => 'A custom block specific to this site.', // Describes your block. Used for debugging
'type' => 'custom-blocks/hello-world', // See register_block_type
'args' => [], // See register_block_type
] );
Next up, install the script loader. In your command line, navigate to your mu-plugins
directory and run this command:
composer require underpin/script-loader
Exactly like blocks, this will install the loader necessary to register scripts in Underpin. With it, you can register scripts like so:
custom_blocks()->scripts()->add( 'custom_blocks', [
'handle' => 'custom-blocks', // Script Handle used in wp_*_script
'src' => custom_blocks()->js_url() . 'custom-blocks.js', // Src used in wp_register_script
'name' => 'Custom Blocks Script', // Names your script. Used for debugging.
'description' => 'Script that loads in the custom blocks', // Describes your script. Used for debugging.
] );
Let’s break down what’s going on above.
-
custom_blocks()
actually retrieves this plugin’s instance of Underpin -
scripts()
Retrieves the loader registry for this instance of Underpin -
add()
actually adds this script to the registry -
custom_blocks()->js_url()
is a helper function that automatically gets the javascript url for this plugin. This is configured in thecustom_blocks
function directly, and defaults tobuild
Behind the scenes, Underpin will automatically create an instance of Script, which then automatically runs wp_register_script
using the arguments passed into the registry.
Now that the script is registered, you actually have to enqueue the script as well. We could manually enqueue the script, but instead we’re going to use Underpin’s Middleware functionality to automatically enqueue this script in the admin area.
custom_blocks()->scripts()->add( 'custom_blocks', [
'handle' => 'custom-blocks', // Script Handle used in wp_*_script
'src' => custom_blocks()->js_url() . 'custom-blocks.js', // Src used in wp_register_script
'name' => 'Custom Blocks Script', // Names your script. Used for debugging.
'description' => 'Script that loads in the custom blocks', // Describes your script. Used for debugging.
'middlewares' => [
'Underpin_Scripts\Factories\Enqueue_Admin_Script', // Enqueues the script in the admin area
],
] );
Your bootstrap.php
file should now look something like this:
<?php
/*
Plugin Name: Custom Blocks
Description: Plugin Description Replace Me
Version: 1.0.0
Author: An awesome developer
Text Domain: custom_blocks
Domain Path: /languages
Requires at least: 5.1
Requires PHP: 7.0
Author URI: https://www.designframesolutions.com
*/
use Underpin\Abstracts\Underpin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Fetches the instance of the plugin.
* This function makes it possible to access everything else in this plugin.
* It will automatically initiate the plugin, if necessary.
* It also handles autoloading for any class in the plugin.
*
* @since 1.0.0
*
* @return \Underpin\Factories\Underpin_Instance The bootstrap for this plugin.
*/
function custom_blocks() {
return Underpin::make_class( [
'root_namespace' => 'Custom_Blocks',
'text_domain' => 'custom_blocks',
'minimum_php_version' => '7.0',
'minimum_wp_version' => '5.1',
'version' => '1.0.0',
] )->get( __FILE__ );
}
// Lock and load.
custom_blocks();
// Registers block
custom_blocks()->blocks()->add( 'my_custom_block', [
'name' => 'My Custom Block', // Names your block. Used for debugging.
'description' => 'A custom block specific to this site.', // Describes your block. Used for debugging
'type' => 'underpin/custom-block', // See register_block_type
'args' => [], // See register_block_type
] );
custom_blocks()->scripts()->add( 'custom_blocks', [
'handle' => 'custom-blocks', // Script Handle used in wp_*_script
'src' => custom_blocks()->js_url() . 'custom-blocks.js', // Src used in wp_register_script
'name' => 'Custom Blocks Script', // Names your script. Used for debugging.
'description' => 'Script that loads in the custom blocks', // Describes your script. Used for debugging.
'middlewares' => [
'Underpin_Scripts\Factories\Enqueue_Admin_Script', // Enqueues the script in the admin area
],
] );
First, you need to modify your webpack.config.js
to create a new entry file. It should look like this:
/**
* WordPress Dependencies
*/
const defaultConfig = require( '@wordpress/scripts/config/webpack.config.js' );
/**
* External Dependencies
*/
const path = require( 'path' );
module.exports = {
...defaultConfig,
...{
entry: {
"custom-blocks": path.resolve( process.cwd(), 'src', 'custom-blocks.js' ) // Create "custom-blocks.js" file in "build" directory
}
}
}
This instructs Webpack to take a JS file located in your plugin’s src
directory, and compile it into build/custom-blocks.js
. From here, we need to create a new file in the src
directory called custom-blocks.js
.
Now we have to register the block in our Javascript, as well. This will allow us to customize how this block behaves in the Gutenberg editor. In this lesson, we’re going to just create a very simple “Hello World” block.
// Imports the function to register a block type.
import { registerBlockType } from '@wordpress/blocks';
// Imports the translation function
import { __ } from '@wordpress/i18n';
// Registers our block type to the Gutenberg editor.
registerBlockType( 'custom-blocks/hello-world', {
title: __( "Hello World!", 'beer' ),
description: __( "Display Hello World text on the site", 'beer' ),
edit(){
return (
<h1 className="hello-world">Hello World!</h1>
)
},
save() {
return (
<h1 className="hello-world">Hello World!</h1>
)
}
} );
Okay, so what’s going on here?
- We’re importing
registerBlockType
so we can use it in this file - We’re also importing
__
so we can make translate-able strings - We are running
registerBlockType
to register our “Hello World” block to the editor.
Now run npm install
and npm run start
. This will create two files in your build
directory:
- custom-blocks.js – This is your compiled Javascript file that gets enqueued by Underpin’s script loader.
- custom-blocks-asset.php – This asset file tells WordPress what additional scripts need enqueued in-order for this script to work properly.
You will notice that we did not install @wordpress/blocks
or @wordpress/i18n
. That’s not a mistake. Since these are internal WordPress scripts, we need to tell WordPress to enqueue those scripts before our script. Fortunately, WordPress and Underpin make this pretty easy to-do.
Back in bootstrap.php
, update your script’s add
function to include a deps
argument. Since this argument is a path, it will automatically require the file, and use it to tell WordPress which scripts need enqueued. Since Webpack automatically generates this file for us, we no-longer need to worry about adding dependencies every time we want to use a WordPress library.
custom_blocks()->scripts()->add( 'custom_blocks', [
'handle' => 'custom-blocks', // Script Handle used in wp_*_script
'src' => custom_blocks()->js_url() . 'custom-blocks.js', // Src used in wp_register_script
'name' => 'Custom Blocks Script', // Names your script. Used for debugging.
'description' => 'Script that loads in the custom blocks', // Describes your script. Used for debugging.
'deps' => custom_blocks()->dir() . 'build/custom-blocks.asset.php', // Load these scripts first.
'middlewares' => [
'Underpin_Scripts\Factories\Enqueue_Admin_Script', // Enqueues the script in the admin area
],
] );
From your admin screen, if you navigate to posts>>Add New, you will find that you can use a new block called “Hello World”, which will simply display “Hello World” In gigantic letters on the page.
With this script, you can create as many blocks as you need by simply creating another registerBlockType
call, and registering the block though Underpin using custom_blocks()->blocks()->add
.
Stylesheets need a bit of extra thought in-order for them to work-as expected. Normally, you would simply enqueue the script much like you enqueue a script. The catch is that this stylesheet also needs to be used in the block editor in-order to accurately display the block output. Let’s get into how to set that up.
Just like everything else with Underpin, the first step is to install the appropriate loader, the register the style.
In your mu-plugins
directory, run:
composer require underpin/style-loader
From there, register a style in your bootstrap.php
file:
custom_blocks()->styles()->add( 'custom_block_styles', [
'handle' => 'custom-blocks', // handle used in wp_register_style
'src' => custom_blocks()->css_url() . 'custom-block-styles.css', // src used in wp_register_style
'name' => 'Custom Blocks Style', // Names your style. Used for debugging
'description' => 'Styles for custom Gutenberg blocks', // Describes your style. Used for debugging
] );
Then, update webpack.config.js
to include custom-block-styles.css
, like so:
/**
* WordPress Dependencies
*/
const defaultConfig = require( '@wordpress/scripts/config/webpack.config.js' );
/**
* External Dependencies
*/
const path = require( 'path' );
module.exports = {
...defaultConfig,
...{
entry: {
"custom-blocks": path.resolve( process.cwd(), 'src', 'custom-blocks.js' ), // Create "custom-blocks.js" file in "build" directory
"custom-block-styles": path.resolve( process.cwd(), 'src', 'custom-block-styles.css' )
}
}
}
Next, update your registered block to use the style to specify the stylesheet to be used with this block like so:
// Registers block
custom_blocks()->blocks()->add( 'my_custom_block', [
'name' => 'My Custom Block', // Names your block. Used for debugging.
'description' => 'A custom block specific to this site.', // Describes your block. Used for debugging
'type' => 'custom-blocks/hello-world', // See register_block_type
'args' => [ // See register_block_type
'style' => 'custom-blocks', // Stylesheet handle to use in the block
],
] );
That will update your block to enqueue the stylesheet in the block editor automatically, and will reflect the styles in the stylesheet. This will work both on the actual site and the block editor.
With the style set as-such:
.hello-world {
background:rebeccapurple;
}
You’ll get this in the block editor, and the front end:
This is all fine and dandy, but there’s one problem with how this is built – what happens if a theme needs to change the markup of our block? Or, what if, for some reason, it makes more sense to use PHP to render this block instead of Javascript?
A fundamental problem with blocks is that it will hardcode the saved block result inside of the WordPress content. In my opinion, it’s better to render using server-side rendering. This tells WordPress that, instead of saving the HTML output, to instead create a placeholder for the block, and just before the content is rendered, WordPress will inject the content from a PHP callback. This allows you to update blocks across your site quickly just by updating a PHP callback whenever you want.
Call me old fashioned, but I think that’s a lot more maintain-able, and thankfully it’s pretty easy to-do.
First, update your registered block so that save
returns null
. This instructs the editor to simply not save HTML, and just put a placeholder there instead.
// Registers our block type to the Gutenberg editor.
registerBlockType( 'custom-blocks/hello-world', {
title: __( "Hello World!", 'beer' ),
description: __( "Display Hello World text on the site", 'beer' ),
edit(){
return (
<h1 className="hello-world">Hello World!</h1>
)
},
save: () => null
} );
Now, if you specify a render_callback
in your registered block arguments, it will use the callback instead of what was originally in the save
callback.
// Registers block
custom_blocks()->blocks()->add( 'my_custom_block', [
'name' => 'My Custom Block', // Names your block. Used for debugging.
'description' => 'A custom block specific to this site.', // Describes your block. Used for debugging
'type' => 'custom-blocks/hello-world', // See register_block_type
'args' => [ // See register_block_type
'style' => 'custom-blocks', // Stylesheet handle to use in the block
'render_callback' => function(){
return '<h1 class="hello-world">Hey, this is a custom callback!</h1>';
}
],
] );
Now if you look in your editor, you’ll still see “Hello World”, because that’s what the Javascript’s edit
method returns, however, if you save and look at the actual post, you’ll find that the actual post will show “Hey, this is a custom callback” instead. This is because it’s using PHP to render the output on the fly. Now, if you change the content of the render_callback
, it will automatically render this output.
What happens if you have a WordPress theme, and you want to actually override the render callback? A good way to approach this is to use Underpin’s built-in Template loader system. This system allows you to specify file locations for PHP templates that render content, and also has baked-in support for template overriding by themes.
Underpin’s template system is a PHP trait. It can be applied to any class that needs to output HTML content. The tricky part is, we haven’t made a class yet, have we?
…Have we?
Well, actually, we have. Every time we run the add
method in WordPress, it automatically creates an instance of a class, and it uses the array of arguments to construct our class for us. However, now, we need to actually make the class ourselves so we can apply the Template trait to the class, and render our template. So, next up we’re going to take our registered block, and move it into it’s own PHP class, and then instruct Underpin to use that class directly instead of making it for us.
First up, create a directory called lib
inside your plugin directory, and then inside lib
create another directory called blocks
. Inside that, create a new PHP file called Hello_World.php
. Underpin comes with an autoloader, so the naming convention matters here.
├── lib
│ └── blocks
│ └── Hello_World.php
Inside your newly created PHP file, create a new PHP class called Hello_World
that extends Block
, then move all of your array arguments used in your add
method as parameters inside the class, like so:
<?php
namespace Custom_Blocks\Blocks;
use Underpin_Blocks\Abstracts\Block;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Hello_World extends Block {
public $name = 'My Custom Block'; // Names your block. Used for debugging.
public $description = 'A custom block specific to this site.'; // Describes your block. Used for debugging
public $type = 'custom-blocks/hello-world'; // See register_block_type
public function __construct() {
$this->args = [ // See register_block_type
'style' => 'custom-blocks', // Stylesheet handle to use in the block
'render_callback' => function(){
return '<h1 class="hello-world">Hey, this is a custom callback!</h1>';
}
];
parent::__construct();
}
}
Then, replace the array of arguments in your add
callback with a string that references the class you just created, like so:
// Registers block
custom_blocks()->blocks()->add( 'my_custom_block', 'Custom_Blocks\Blocks\Hello_World' );
By doing this, you have instructed Underpin to use your PHP class instead of creating one from the array of arguments. Now that we have a full-fledged PHP class in-place, we can do a lot of things to clean this up a bit, and use that template Trait I mentioned before.
Add use \Underpin\Traits\Templates
to the top of your PHP class, and add the required methods to the trait as well, like so:
<?php
namespace Custom_Blocks\Blocks;
use Underpin_Blocks\Abstracts\Block;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Hello_World extends Block {
use \Underpin\Traits\Templates;
public $name = 'My Custom Block'; // Names your block. Used for debugging.
public $description = 'A custom block specific to this site.'; // Describes your block. Used for debugging
public $type = 'custom-blocks/hello-world'; // See register_block_type
public function __construct() {
$this->args = [ // See register_block_type
'style' => 'custom-blocks', // Stylesheet handle to use in the block
'render_callback' => function(){
return '<h1 class="hello-world">Hey, this is a custom callback!</h1>';
}
];
parent::__construct();
}
public function get_templates() {
// TODO: Implement get_templates() method.
}
protected function get_template_group() {
// TODO: Implement get_template_group() method.
}
protected function get_template_root_path() {
// TODO: Implement get_template_root_path() method.
}
}
Now, we’re going to fill out each of these functions. get_templates
should return an array of template file names with an array declaring if that template can be manipulated by a theme, or not, like so:
public function get_templates() {
return ['wrapper' => [ 'override_visibility' => 'public' ]];
}
get_template_group
should return a string, that indicates what the template sub directory should be called. In our case, we’re going to make it hello-world
.
protected function get_template_group() {
return 'hello-world';
}
get_template_root_path
should simply return custom_blocks()->template_dir()
, as we don’t need to use a custom template directory or anything.
protected function get_template_root_path() {
return custom_blocks()->template_dir();
}
Finally, we have the option to override the template override directory name into something specific to our own plugin. Let’s do that, too:
protected function get_override_dir() {
return 'custom-blocks/';
}
With these three items in-place, you can now create a new file in templates/hello-world
called wrapper.php
. Inside your theme, this template can be completely overridden by adding a file in custom-blocks/hello-world
called wrapper.php
. Let’s start by adding our template in the plugin file.
The first thing your template needs is a header that checks to make sure the template was loaded legitimately. You don’t want people to load this template outside of the intended way, so you must add a check at the top level to make sure it was loaded properly, like so:
<?php
if ( ! isset( $template ) || ! $template instanceof \Custom_Blocks\Blocks\Hello_World ) {
return;
}
?>
Underpin automatically creates a new variable called $template
and assigns it to the class that renders the actual template. So inside your template file $template
will always be the instance of your registered block. This allows you to create custom methods inside the block for rendering purposes if you want, but it also gives you access to rendering other sub-templates using $template->get_template()
, plus a lot of other handy things that come with the Template
trait. As you can see above, this also provides you with a handy way to validate that the required file is legitimate.
Now, simply add the HTML output at the bottom, like this:
<?php
if ( ! isset( $template ) || ! $template instanceof \Custom_Blocks\Blocks\Hello_World ) {
return;
}
?>
<h1 class="hello-world">Hey, this is a custom callback, and it is inside my template!</h1>
From there, go back into your Hello_World
class, and update the render callback to use your template. This is done using get_template
, like so:
public function __construct() {
$this->args = [ // See register_block_type
'style' => 'custom-blocks', // Stylesheet handle to use in the block
'render_callback' => function () {
return $this->get_template( 'wrapper' );
},
];
parent::__construct();
}
This instructs the render_callback
to use get_template
, which will then retrieve the template file you created. If you look at your template’s output, you’ll notice that your h1 tag changed to read “Hey, this is a custom callback, and it is inside my template!”.
Now, go into your current theme, create a php file inside custom-blocks/hello-world
called wrapper.php
. Copy the contents of your original wrapper.php
file, and paste them in. Finally, change the output a little bit. When you do this, the template will automatically be overridden by your theme.
Now that you have one block set-up, it’s just a matter of registering new blocks using Underpin, and inside your Javascript using registerBlockType
. If necessary, you can create a block class for each block, and use the template system to render the content.
This post barely scratches the surface of what can be done with Underpin, the template loader, and Gutenberg. From here, you could really flesh out your block into something more than a trivial example. If you want to go deeper on these subjects, check out my WordPress plugin development course, where we create a block much like how I describe it here, and then build out a fully-functional Gutenberg block, as well as many other things.
Join WP Dev Academy’s Discord server, and become a part of a growing community of WordPress developers.
20