How to Register Gutenberg Blocks for Some Custom Post Type only, using PHP (not JS!)
When creating a WordPress site, we may need to create a custom post type and have it reference other entities from the database. For instance, a Product
custom post, which displays how many stars it has been rated by the users, needs to reference a list of users or a list of ProductReview
custom posts.
WordPress provides custom meta fields, which enable to add any arbitrary piece of data to posts, users, comments, categories and tags, such as references to any other entity from the database. However, WordPress doesn’t provide a built-in solution to create the relationships across entities in the admin panel in a user-friendly way.
This void has been filled by plugin Advanced Custom Fields. Whenever we can control the environment where the site runs (such as when building a website for a client), we can install ACF and depend on it for creating all the relationships across entities.
However, that is not the case when submitting a theme or plugin to the WordPress directory. Since we don’t know in advance if the user installing the theme or plugin will also have ACF installed, we must then find a different way to solve the problem.
Luckily, Gutenberg can replace ACF (even though is not as simple, since it requires plenty of coding) by attaching custom blocks to the editor to let the user define the relationships across entities. For instance, the custom block can fetch the list of database entities that can be referenced using the WP REST API, display them on a select input, and save the selected entries through a block attribute.
Gutenberg will store the data within the post content, and we can retrieve it using function parse_blocks
, as we explained on article Exposing WordPress site data for mobile apps.
This custom Gutenberg block makes sense only when editing the intended custom post type, so we need to remove it everywhere else. In this article, we will explore how to achieve this. We will first analyze the official solution, which is based on JavaScript, and then an alternative (and better) approach using PHP.
The official (unsatisfying) solution: Unregistering blocks through JavaScript code
The official solution to this problem is to unregister the block by executing JavaScript function wp.blocks.unregisterBlockType
:
wp.domReady( function() {
wp.blocks.unregisterBlockType( 'my-namespace/my-custom-block' );
} );
Please notice that there is also an alternative proposed approach, which is to use a whitelist:
var allowedBlocks = [
'core/paragraph',
'core/image',
'core/html',
'core/freeform'
];
wp.blocks.getBlockTypes().forEach( function( blockType ) {
if ( allowedBlocks.indexOf( blockType.name ) === -1 ) {
wp.blocks.unregisterBlockType( blockType.name );
}
} );
However, this approach will not work for our situation, because we only know which is the block type that is forbidden from all other custom post types, instead of knowing which are all the allowed block types. Sure, we could calculate the whitelist by removing the blacklisted block type from the list of all block types, but then we’d rather already use the simpler blacklisting approach.
The blacklisting approach works, however I find this solution quite unsatisfactory, because of several reasons:
- First registering the block in PHP as to then unregister it in JavaScript is not very sensible
- The name of the post type is defined in PHP and referenced in JavaScript, so the code is not DRY (Don’t Repeat Yourself), which can potentially create bugs down the road if the developer is not careful and renames the post type in one place only
- Even though it won’t be executed, the script for the blacklisted block type is still loaded, affecting performance
- JavaScript requires extra steps (compilation, minifying, bundling, etc) over PHP, meaning more complexity and extra bureaucracy
Resolving this problem in PHP, if possible, shold lead to a neater solution. Let’s explore how to do this.
First (failed) approach with PHP: Using hook "allowed_block_types"
WordPress provides filter "allowed_block_types"
to modify what block types are allowed for a post.
Whitelisting blocks can be easily accomplished, like this:
function wpdocs_allowed_block_types( $allowed_block_types, $post ) {
if ( $post->post_type !== 'post' ) {
return $allowed_block_types;
}
return array( 'core/paragraph' );
}
add_filter( 'allowed_block_types', 'wpdocs_allowed_block_types', 10, 2 );
Unfortunately, this filter does not work for the blacklisting approach, which is what we need. This is because the filter must return the whitelisted list of block types, to be calculated as all block types minus the blacklisted ones. However, this filter initially does not receive the array with all registered block types, but a true
boolean value. Hence, it must first obtain the array of all blocks from somewhere in the system.
Yet, this information is not available on the PHP-side of the admin panel! There is function get_all_registered()
from WP_Block_Type_Registry
, however it returns only blocks registered through register_block_types
, and many core blocks (such as "core/paragraph"
) are not registered this way. Then, we would need to discover the list of all core blocks not registered with register_block_types
and manually add them to the list, which leads to plenty of bureaucracy (such as having to watch future releases of WordPress to discover new unregistered core blocks and add them to the list), and bugs are bound to happen.
So this approach doesn’t work. What else can we do?
Second (successful) approach with PHP: Register a block type only if the custom post type in editor is the right one
The WordPress editor knows which is the post type of the post being edited. Then, the solution is initially simple:
- In the editor, check which is the post type of the post being created or edited
- If it is the right CPT, only then register the block
Unfortunately, there is no function similar to is_singular($post_type)
for the admin panel, as to determine the post type of the object being edited. And even though the post type information is stored under global variable $typenow
, this variable is set late in the process, and not before hook "init"
is executed (which is where we normally place the logic to register the block scripts), so we can’t use this variable for our purpose.
To solve this issue, we can recreate the logic to calculate the value for global variable $typenow
. The steps to follow are:
- Check if we are on the admin panel
- Get the value of global variable
$pagenow
(which has been set by the time the"init"
hook is invoked) - Obtain the post type like this:
- If the value of
$pagenow
is"post-new.php"
, then the post type of the new post is indicated under URL parameter'post_type'
- If instead it is
"post.php"
, then the post type can be deduced from the object being edited
- If the value of
- If this is the intended custom post type, only then execute
register_block_type
Here is the code:
add_action('init', 'maybe_register_custom_block');
function maybe_register_custom_block()
{
// Check if this is the intended custom post type
if (is_admin()) {
global $pagenow;
$typenow = '';
if ( 'post-new.php' === $pagenow ) {
if ( isset( $_REQUEST['post_type'] ) && post_type_exists( $_REQUEST['post_type'] ) ) {
$typenow = $_REQUEST['post_type'];
};
} elseif ( 'post.php' === $pagenow ) {
if ( isset( $_GET['post'] ) && isset( $_POST['post_ID'] ) && (int) $_GET['post'] !== (int) $_POST['post_ID'] ) {
// Do nothing
} elseif ( isset( $_GET['post'] ) ) {
$post_id = (int) $_GET['post'];
} elseif ( isset( $_POST['post_ID'] ) ) {
$post_id = (int) $_POST['post_ID'];
}
if ( $post_id ) {
$post = get_post( $post_id );
$typenow = $post->post_type;
}
}
if ($typenow != 'my-custom-post-type') {
return;
}
}
// Register the block
$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');
wp_register_script(
'my-custom-block',
plugins_url( 'build/block.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
register_block_type( 'my-namespace/my-custom-block-name', array(
'editor_script' => 'my-custom-block',
) );
}
This solution is a bit hacky, but it works well.
Conclusion
Registering plenty of blocks in the Gutenberg editor can bog down the user experience. Then, it is a good practice to avoid registering blocks whenever they are not needed, such as when creating a custom block that needs be accessed for a specific custom post type only.
The official way to do it, based on JavaScript code, is unsatisfactory, because it loads the unwanted scripts so performance still takes a hit, requires extra effort, and can lead to bugs.
In this article we learnt that we can achieve it with PHP code, by deciding if to register the block type or not depending on the current post type in the editor. This method is more effective: performance improves since scripts are never loaded, and it is simpler to execute, overall making the application be more resilient.
I’d been looking for a way to solve this. In my own test I found the condition “post_type_exists( $_REQUEST[‘post_type’] )” failed at first.
Plugins may use the init hook to register_post_type too and the order of callback execution is than an issue. In my case it was the events calendar that made it fail. Easy to solve, of course, by adding a higher prio, but one needs to think of it.
Perhaps the “post_type_exists( $_REQUEST[‘post_type’] )” condition also isn’t that important, since it will only be used to match my cpt in order to decide if the block is allowed.
Not sure when the post_id query var is used (legacy cases?), but the code works for me like this, so thanks for sharing this solution.