How to Customize the Gutenberg Block Editor When Developing WordPress Themes

The Gutenberg editor in WordPress is designed for composing content quickly, and once you are used to it I think it does that job very well.

For those just starting out though, opening up the “add block” panel can be overwhelming with the amount of choices, especially if there are a bunch of custom blocks an agency developed to choose from as well.

In this post we’ll go through a couple steps to optimize your editor experience for yourself, and especially for clients when developing custom themes.

Allow/Disallow

Gutenberg comes with two options for controlling blocks displayed within the editor, but they are managed a bit differently.

Option 1 – PHP Allow list

Allow listing explicitly specifics what blocks are allowed so you can choose just a subset to display. You can specify using a standard WordPress-style php function and have access to the post variable which allows you to check things like post type and conditionally allow different sets of blocks. This is the simplest method, but I would argue not the best.

function wpdocs_allowed_post_type_blocks( $allowed_block_types, $editor_context ) {
	if ( 'sponsors' === $editor_context->post->post_type ) {
		return array(
			'core/paragraph',
		);
	}

	if ( 'news' === $editor_context->post->post_type ) {
		return array(
			'core/paragraph',
			'core/list',
			'core/image',
			'core/buttons',
			'core/quote',
		);
	}

	if ( 'faqs' === $editor_context->post->post_type ) {
		return array(
			'core/paragraph',
			'core/list',
			'core/image',
			'core/buttons',
		);
	}

	return $blocks;
}

add_filter( 'allowed_block_types_all', 'wpdocs_allowed_post_type_blocks', 10, 2 );

I’ve found most agency made themes use this approach, but I mentioned this is not the best way to manage blocks for a couple reasons:

  1. As Gutenberg develops blocks can change handle and suddenly a block used all across the site may disappear from the editor panel.
  2. As Gutenberg adds more blocks in the future, all legacy themes will have to be updated to support them.
  3. Any time you add a custom block, you must also remember to add it to the allow list for it to show up for editors.

Option 2 – Javascript Unregister

This approach allows for the opposite logic. You can filter through blocks and remove ones you don’t want. 

But this approach offers much more as well. In javascript, you have access to all the block attributes and can filter by category or variant, and have extremely granular control over blocks with the ability to choose whether to allow by default and selectively unregister or the other way around!

That said, it is just a little bit more complex to set up:

First, you have to register an editor script in the functions.php file or an include.

function custom_gutenberg_scripts() {

    wp_enqueue_script(
        'custom-editor', 
        get_stylesheet_directory_uri() . '/src/js/editor.js', 
        array( 'wp-blocks', 'wp-dom' ), 
        filemtime( get_stylesheet_directory_uri() . '/src/js/editor.js' ),
        true
    );
}
add_action( 'enqueue_block_editor_assets', 'custom_gutenberg_scripts' );

If you want to do filtering by post type, you will also need to pass the post information into this script, which can be done with WordPress’ localize script function (here we are passing both the post type and post ID).

function custom_gutenberg_scripts() {

    wp_enqueue_script(
        'custom-editor', 
        get_stylesheet_directory_uri() . '/src/js/editor.js', 
        array( 'wp-blocks', 'wp-dom' ), 
        filemtime( get_stylesheet_directory_uri() . '/src/js/editor.js' ),
        true
    );

    // Add custom data as a variable in JS
    wp_localize_script( 'custom-editor', 'postData',
        array( 
            'postType' => get_post_type( get_the_id() ),
            'postId' => get_the_id(),
        )
    );
}
add_action( 'enqueue_block_editor_assets', 'custom_gutenberg_scripts' );

Now, you can hook into the block registration within your editor.js file and set up switch statements or conditionals to remove blocks. 

Below is an example with several different types of logic for different post types that you can grab from for your own use.

wp.domReady(function () {
    /**
    * Manage Block Types By Post Type
    */


    // FILTER BLOCKS FOR PAGES
    if(postData.postType == 'page'){
        wp.blocks.getBlockTypes().forEach(function(block){

            // UNCOMMENT TO CHECK BLOCK DATA IN BROWSER
            // console.log(block) 

            // ALLOW LIST BY CATEGORY
            if(block.category == 'custom') return

            if(block.category == 'text') return
            
            if(block.category == 'media') return
            
            // REMOVE VARIATIONS
            if(block.name === 'core/embed'){
                block.variations.forEach(variant => {
                    wp.blocks.unregisterBlockVariation( block.name, variant.name );
                })
                return
            }
            
            // ALLOW BY NAME
            if(block.name == 'core/columns' 
              || block.name == 'core/separator' 
              || block.name == 'core/spacer' ) return

            // Unregister everything that does not match
            wp.blocks.unregisterBlockType(block.name);
        })
    }


    // DEFINE THE BLOCKS WE WANT ON POSTS 
    else if(postData.postType == 'post'){
        wp.blocks.getBlockTypes().forEach(function(block){
            
            // Unregister all blocks not in text category
            if(block.category !== 'text'){
              wp.blocks.unregisterBlockType(block.name);
            }
            
        })
    }

    // SAFE DEFAULTS FOR NEW POST TYPES

    else {

        wp.blocks.getBlockTypes().forEach(function(block){
            
            // Just selectively unregister blocks we don't want - leave everything else alone
            if(block.name == 'core/columns'){
             wp.blocks.unregisterBlockType(block.name);
            }

            
        })
    }
});

You might be wondering about how to figure out the block handle, category handle, etc. There are many blog posts that attempt to list these out, but inevitably they go out of date as Gutenberg evolves.

Instead, I recommend you add a console.log in this script and just log out every block. Load the editor and look in the browser inspector and you can examine all the data associated with each block the editor enqueues and set up your filtering based on this. 

Logging like this is exactly how I figured out the structure of the embed block variations you see getting unregistered in the example code above.

You’ll notice a wide variety of black data available to you in the console – you could even do something like unregister blocks based on a certain block support they have!

Block Organization

Once you’ve tamed the core blocks in a way that makes sense, the next step is organizing custom blocks.

I see a lot of agency-developed themes, where all custom blocks share the same icon, are registered in the “layout” category, and have no block preview. 

Obviously this is not a great experience, so let’s walk through some steps to improve this.

Firstly, just use the dashicons reference to pick an icon that roughly approximates the function of the block. That’s an easy win.

Custom Category

Next, it is very simple to register custom block categories. Depending on how many custom blocks are being built you may be able to just get away with a single one like “Custom Components” or even better use the site brand name like “<brand> Components”.

// ADD BLOCK CATEGORIESfunction custom_block_category( $categories, $post ) {
    return array_push($categories, array(
                'slug' => 'custom',
                'title' => __( 'Custom Blocks', 'textdomain' ),
            ));
}
add_filter( 'block_categories_all', 'custom_block_category', 10, 2);

If you have dozens of custom blocks, it might be better to break this into “Custom – CTAs”, “Custom – Banners”, etc. Though if you are registering dozens of custom blocks, it may be worth reconsidering if you can take a more atomic approach with less blocks and more patterns.

Category order

By default your custom category will show up at the bottom of the add block pane. Again, I see many themes where this is the case, but it’s extremely easy to pop it up to the top instead, which in most cases is much better for the content editors since most of the time they will be reaching for bespoke brand blocks when building landing pages. 

All you need to do is use array_merge and put the custom categories first:

// ADD BLOCK CATEGORIES
function custom_block_category( $categories, $post ) {
    return array_merge(
        
        array(
            array(
                'slug' => 'custom',
                'title' => __( 'Custom Blocks', 'textdomain' ),
            ),
        ),
        $categories
    );
}
add_filter( 'block_categories_all', 'custom_block_category', 10, 2);

Block Previews

There are a couple ways to handle block previews, and the best one to choose largely depends on the block in question, but below are a few options for both native blocks and blocks built with ACF:

Make use of the “example” key and add some default content that will be rendered in the preview pane:

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 2,
    "name": "my-plugin/notice",
    "title": "Notice",
    "category": "text",
    "parent": [ "core/group" ],
    "icon": "star",
    "description": "Shows warning, error or success notices…",
    "attributes": {
        "message": {
            "type": "string",
            "source": "html",
            "selector": ".message"
        }
    },
    // This gets inputted for the preview
    "example": {
        "attributes": {
            "message": "This is a notice!"
        }
    },
    "editorScript": "file:./index.js",
    "script": "file:./script.js",
    "editorStyle": "file:./index.css",
    "style": [ "file:./style.css", "example-shared-style" ],
    "render": "file:./render.php"
}

If this renders strangely because of more complex logic in the block, it may be easier to just display a photo of what the final block should look like on the frontend. 

Make sure these photos are small (400px wide or less) and minified, because this adds extra weight in the editor which is generally not a good thing, but in this case may be worth it for the improved experience.

You can do this by setting up the “example” key like so:

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 2,
    "name": "my-plugin/notice",
    "title": "Notice",
    "category": "text",
    "parent": [ "core/group" ],
    "icon": "star",
    "description": "Shows warning, error or success notices…",
    "attributes": {
        "message": {
            "type": "string",
            "source": "html",
            "selector": ".message"
        }
    },
    // This gets inputted for the preview
    "example": {
        "attributes": {
            "is_preview_mode": true,
            "preview_image_url": "/wp-content/uploads/example-image.jpg"
        }
    },
    "editorScript": "file:./index.js",
    "script": "file:./script.js",
    "editorStyle": "file:./index.css",
    "style": [ "file:./style.css", "example-shared-style" ],
    "render": "file:./render.php"
}

NOTE: This shows a relative url, but you should really use localize script to pass the absolute url from php using get_stylesheet_directory_uri() in case your install is not at the root web directory

Then in the render template simply have a conditional that checks if Gutenberg should be displaying the example (because it is previewing the block) and if so renders just a photo. Otherwise it falls back the normal block output.

if(is_preview_mode === true){
  return <img src={preview_image_url} />
} 

// ... Rest of block logic

Wrapping up

By using this approach your block inserter pane will be a much better experience for content editors who will be able to grab the blocks they need easily, and not be distracted with choice paralysis, or lost looking for a specific block floating in a random category.

There is a lot more you can do to streamline content creation however, and I think the future of agency theme creation is block patterns with content-locking (available in WP 6.1).

If you’re interested in how to optimize the editor experience further with patterns and more, consider signing up for my once-monthly newsletter, as I have a lot of content around this planned, especially surrounding the 6.1 WordPress release.

Responses

  1. Ingo Avatar
    Ingo

    Thanks for this really useful post. However, when using ‘allowed_block_types_all’, custom patterns are no longer displayed. I can’t find anything about this in the WP documentation. Do you know how to get them displayed?

    The slug used in the header of the pattern file does not work:
    ‘my-theme/the-pattern-name’

  2. Ingo Avatar
    Ingo

    Sorry, for posting again. I have tried it this way. Probably the wrong place for the custom pattern, but the other way around, this code completely prevents the pattern from being output.

    function wpdocs_allowed_block_types($block_editor_context, $editor_context) {
    if (!empty($editor_context->post)) {
    return array(
    ‘core/paragraph’,
    ‘core/heading’,
    ‘core/list’,
    ‘core/list-item’,
    ‘core/image’,

    ‘acf/comparison-table’,

    ‘my-blog/banner-image-and-text-with-button’
    );
    }
    return $block_editor_context;
    }
    add_filter(‘allowed_block_types_all’, ‘wpdocs_allowed_block_types’, 10, 2);

  3. Ingo Avatar
    Ingo

    Sorry so much, I posted german text. It seems I was really confused yesterday. If possible, please replace my last post with this text….

    >> I think I figured out why my patterns are not showing:

    By the way, it works now when all blocks are enabled that are inside a pattern.

    1. Anton P. Avatar
      Anton P.

      Hi!

      Sorry for just now getting back to you – I was away from the office yesterday. But yes, it looks like you’ve got it figured out. Patterns are really nothing more than inserting several blocks at once in a particular arrangement with some predefined settings. So for them to work you have to enable all blocks they contain. Let me know if you have any other questions!

  4. Ingo Avatar
    Ingo

    Hi,
    no problem, thanks for your answer. The problem was also that I took a pattern from the WP library where it wasn’t really easy to see in the code what blocks it all contained. I came up with the “solution” after using a pattern with only 3 blocks. If you work with the “PHP Allow list”, it can happen quickly that you don’t have e.g. “core/group” in the list and then many patterns from the library don’t work.

Leave a Reply

Your email address will not be published. Required fields are marked *