Using Block Templates with Gutenberg

Block templates are one of my favorite new features in Gutenberg. You can specify a list of blocks that automatically appear in the content editor, and you can customize what appears in each block by default.

You can also lock the template so no additional blocks can be added. This is a great replacement for metaboxes in many cases. You can control the content structure without sacrificing the visual editing experience. 

Block templates are currently set for an entire post type, but you will soon have more granular control to define them in page templates and other contexts.

Quick tip on implementation

I first build the page in the Gutenberg block editor. Then I add the code below to the theme, which prints out an escaped version of post_content. This shows me the blocks and parameters I need to build the block template.

I’m using ea_pp() below (code here) but you could also use print_r().

/**
 * Display Post Blocks 
 *
 */
function ea_display_post_blocks() {
	global $post;
	ea_pp( esc_html( $post->post_content ) );
}
add_action( 'wp_footer', 'ea_display_post_blocks' );

That helped me build the following block template:

'template' => array(
	array( 'core/heading', array( 'level' => 5, 'content' => 'Role' ) ),
	array( 'core/paragraph' ),
	array( 'core/heading', array( 'level' => 5, 'content' => 'Responsibilities' ) ),
	array( 'core/paragraph' ),
	array( 'core/heading', array( 'level' => 5, 'content' => 'Qualifications' ) ),
	array( 'core/list' ),
	array( 'core/heading', array( 'level' => 5, 'content' => 'Highlights' ) ),
	array( 'core/paragraph' ),
)

Which looks like this in the editor:

Post template with ads

Our client is a publisher who needs at least two ads in each post. There’s a few approaches we’ve used in the past:

  • Automatic insertion after X paragraphs. It’s simple to maintain but the ads often don’t follow the natural breaks in article.
  • Manual insertion using a shortcode. The content editor can ensure the ads work well with the content, but it’s more difficult to manage and easy to forget.

With Gutenberg, we simply pre-populate the content area with two ad blocks (built with Advanced Custom Fields) and three paragraph blocks. Content editors can then dive right into content creation around the ad units.

Here’s how you set the block template for posts:

/**
 * Block template for posts
 * @link https://www.billerickson.net/gutenberg-block-templates/
 *
*/
function be_post_block_template() {

  $post_type_object = get_post_type_object( 'post' );
  $post_type_object->template = array(
    array( 'core/paragraph' ),
    array( 'acf/ad' ),
    array( 'core/paragraph' ),
    array( 'acf/ad' ),
    array( 'core/paragraph' ),
  );
}
add_action( 'init', 'be_post_block_template' );

Testimonial block template

We feature testimonials throughout the website, and we have a Testimonial post type for managing them. In the past we would have disabled the editor and added a custom metabox on this post type to collect just the quote and byline.

With Gutenberg, we can limit the editor to just the “Quote” block. This allows the client to use the same UI for managing their blockquotes site-wide.

When registering your post type, use the template parameter to specify an array of which blocks should appear.

Including 'template_lock' => 'all' will prevent any changes to the layout. If you set 'template_lock' => 'insert' it will prevent new blocks from being inserted but still allow the writer re-arrange the existing blocks.

We’ll be featuring these quotes using the “Large” quote style in the theme, so I’ve added is-style-large to the block attributes.

<?php
/**
 * Testimonials
 *
 * @package      CoreFunctionality
 * @author       Bill Erickson
 * @since        1.0.0
 * @license      GPL-2.0+
**/

class EA_Testimonials {

	/**
	 * Initialize all the things
	 *
	 * @since 1.2.0
	 */
	function __construct() {

		// Actions
		add_action( 'init', array( $this, 'register_cpt' ) );
		add_filter( 'wp_insert_post_data', array( $this, 'set_testimonial_title' ), 99, 2 );
	}

	/**
	 * Register the custom post type
	 *
	 * @since 1.2.0
	 */
	function register_cpt() {

		$labels = array(
			'name'               => 'Testimonials',
			'singular_name'      => 'Testimonial',
			'add_new'            => 'Add New',
			'add_new_item'       => 'Add New Testimonial',
			'edit_item'          => 'Edit Testimonial',
			'new_item'           => 'New Testimonial',
			'view_item'          => 'View Testimonial',
			'search_items'       => 'Search Testimonials',
			'not_found'          => 'No Testimonials found',
			'not_found_in_trash' => 'No Testimonials found in Trash',
			'parent_item_colon'  => 'Parent Testimonial:',
			'menu_name'          => 'Testimonials',
		);

		$args = array(
			'labels'              => $labels,
			'hierarchical'        => true,
			'supports'            => array( 'editor' ),
			'public'              => true,
			'show_ui'             => true,
			'show_in_rest'        => true,
			'publicly_queryable'  => false,
			'exclude_from_search' => true,
			'has_archive'         => false,
			'query_var'           => true,
			'can_export'          => true,
			'rewrite'             => array( 'slug' => 'testimonial', 'with_front' => false ),
			'menu_icon'           => 'dashicons-format-quote',
			'template'            => array( array( 'core/quote', array( 'className' => 'is-style-large' ) ) ),
			'template_lock'      => 'all',
		);

		register_post_type( 'testimonial', $args );

	}


	/**
	 * Set testimonial title
	 *
	 */
	function set_testimonial_title( $data, $postarr ) {
		if( 'testimonial' == $data['post_type'] ) {
			$title = $this->get_citation( $data['post_content'] );
			if( empty( $title ) )
				$title = 'Testimonial ' . $postarr['ID'];
			$data['post_title'] = $title;
		}

		return $data;
	}

	/**
	 * Get Citation
	 *
	 */
	function get_citation( $content ) {
		$matches = array();
		$regex = '#<cite>(.*?)</cite>#';
		preg_match_all( $regex, $content, $matches );
		if( !empty( $matches ) && !empty( $matches[0] ) && !empty( $matches[0][0] ) )
			return strip_tags( $matches[0][0] );
	}
}
new EA_Testimonials();

There’s no need for writers to include a post title for these quotes, but WordPress’  auto-generated titles weren’t very descriptive.  I’m using the wp_insert_post_data filter to modify the post title to match the value in <cite>. If there is no byline, it sets the title to “Testimonial {ID}”

I don’t like parsing HTML for this data, but it works given the simple content structure. For more advanced layouts I recommend using something like Gutenberg Object Plugin to save the Gutenberg data as an array in the database so you can access it easier.

Nested Templates

You can create nested block templates using container blocks. For instance, here’s an example from the Templates section of the Gutenberg Handbook.

$template = array(
    array( 'core/paragraph', array(
        'placeholder' => 'Add a root-level paragraph',
    ) ),
    array( 'core/columns', array(), array(
        array( 'core/column', array(), array(
            array( 'core/image', array() ),
        ) ),
        array( 'core/column', array(), array(
            array( 'core/paragraph', array(
                'placeholder' => 'Add a inner paragraph'
            ) ),
        ) ),
    ) )
);

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. Merrill says

    Hi Bill. I am wondering if you have tried putting a reusable block into a template for a custom post?
    I have tried this and while it appears to work fine and I can publish the post, when I edit, I receive the message “Block has been deleted or is unavailable.” This means I must remove and re-add the block. Just as an aside, if I edit and save the reusable block, it will work correctly.

    Do you have any ideas about this? I am including a code snippet in case I am missing something obvious:

    ‘template’ => array(
    array( ‘core/block’, array(
    ‘ref’ => ‘547’,

    ) ),
    array( ‘core/paragraph’, array(
    ‘placeholder’ => ‘Add Bio…’,
    ) ),
    array( ‘core/block’, array(
    ‘ref’ => ‘353’,

    ) ),
    ),

    Thanks

    • Bill Erickson says

      No, I have not tried using reusable blocks in a template. It seems potentially unstable since the reusable blocks are created/managed in the database and could be removed at any time.

    • Bill Erickson says

      Here’s an article I wrote on Core Functionality plugins, which is what you (and the article you linked to) are describing.

      It’s a custom plugin that holds your site-specific code that is not theme-dependent, so doesn’t belong in the theme. Anything customizations you would expect to keep after changing themes belongs in a core functionality plugin.

  2. Brian DiChiara says

    Bill… You’re the best. Also @Merrill, thank you for the reusable block code. Y’all are awesome!

  3. Andrew says

    Hey there! I’m trying to use this with The Events Calendar. I’ve been able to use the template lock no prob with post types I make, but I haven’t been able to lock down a template from a post type like the events CPT that comes with the plugin. Any ideas how to accomplish that?

    • Bill Erickson says

      Have you tried using the approach shown above in the be_post_block_template() example? That shows how to extend an already registered post type.

      If it’s not working, try reaching out to The Events Calendar support and see if they have a recommended way for customizing the post type settings.

  4. Kelsey Barmettler says

    HI Bill, this tutorial is great and exactly what I’m looking for. I’m streamlining my build process with a custom starter theme and a plugin to contain all my custom ACF blocks, including this header block. I’m stuck on this line, from the function to recursively search for duplicate h1s:

    // Custom header block
    if( ‘acf/header’ === $block[‘blockName’] ) {

    I know I need to change acf/header to the name of my header, but I just named it ‘header’ and that’s not working. I assume it should be different since the block is stored in a plugin file rather than the same directory as functions.php?

    Thanks for this tutorial!

    • Bill Erickson says

      All ACF block names are prepended with acf/, so if you created a block with a name of foo then it is actually acf/foo. It sounds like you should change yours back to acf/header.

      • Kelsey Barmettler says

        Thanks so much! Works now. Sorry about posting on the wrong page, had both tabs open. Thanks again for the tutorials, they’ve really changed my process for the better.

  5. Jonathan Eberle says

    Hey Bill, i am building custom blocks with guten-block. Is there a way to define own attributes list “placeholder” for the core/paragraph block, to pass data down to the templates nested blocks?

    And where do i get them in the nested block? props.attributes, arguments?

    • Bill Erickson says

      I’m sorry but I’m not the right person to ask. I haven’t built a custom block with React since before Gutenberg was merged into WP core. Once ACF added block building capabilities I switched to that.

  6. Nathan C says

    Hi,
    thanks for the well written tutorial. It cleared a lot of doubts.

    You mentioned that “you will soon have more granular control to define them in page templates” so I wanted to know if there’s already a way. I haven’t found any.

    We’re trying to build a site for the school, where each teacher will have his page and some sub-pages for each subject he teaches, as Children of his “teacher page”.
    As you can imagine, the “Teacher” pages will have a template and the “Subject” (sub)pages will have a different one. Ideally, they should also have different “edit” pages (block templates) so the teachers can simply fill fields. There lies my interest.

    Thanks in advance, stay safe 🙂

    • Bill Erickson says

      When I first wrote this post there was a GitHub issue for block templates on page templates, but I can’t find it anymore.

      I believe this will likely be addressed by the Full Site Editing feature under active development.

  7. Antal Tettinger says

    Hello Bill! Great tutorial. Thanks as always. What you wrote of replacing the method of inserting ads after X paragraphs is interesting with ACF ad blocks.

    However as I understand from the code, these acf/ad block ads will be meta boxes that will be linked to a specific post.

    In the case of inserting ads after x posts there is a way to change the ad banners from one place, but it seems to me in this case you would have to change by updating each meta box field ad somehow. Is there a way to mass “control” or update the content of these acf/ad blocks that you have inserted.

    For example update all the ad blocks to contain “Black Friday discount” for all the ad blocks in the “Video games” category.

    Thank you for the article, hope you could give me some feedback with your opinion.

    Regards,

    Antal

    • Bill Erickson says

      The ACF ad block I inserted in the above example is not a metabox linked to a specific post. It’s a custom block that executes a custom function, ex: be_display_ad();. Within that function I can display ads based on any criteria I want, and easily change the ad code that runs site-wide.

      • Antal says

        Thank you Bill! Sorry I haven’t noticed your reply. Thanks for the clarification, it was a bit confusing for me because I am new to the block editor but I can see how you could execute any kind of code in the block template, it is much more flexible and efficient than breaking apart the whole content and reassembling each paragraph to insert some ads.

        Have a nice day!