|

Building Custom Block Theme Templates: A Plugin Developer’s Guide

A user of my lesser-known plugin, WebberZone Knowledge Base, reported an issue with the page header not displaying when using it with the Twenty Twenty-Four or any other block theme.

The Problem

The error message I got: File Theme without header.php is deprecated since version 3.0.0 with no alternative available. Please include a header.php template in your theme. With a similar message for footer.php.

Since I haven’t used block themes personally, I didn’t realise that they don’t have a header.php or footer.php file. Instead, WordPress block themes introduce a new way of handling templates through the Full Site Editing (FSE) system. They use block templates which are simple HTML files with the necessary blocks that are processed on the fly when the site is rendered. That also means that you can’t use the template_include hook as you do with classic themes.

To make matters worse, plugins cannot register custom templates for themes in WordPress versions 6.6 and earlier. However, WordPress 6.7 will be released in a month and will introduce the function register_block_template(). This new feature will greatly simplify the process for plugins to register their block templates.

If you’re a plugin author, and you need to support any version before 6.7, you’ll need to programmatically override these templates for your custom post types and taxonomies. In this guide, I’ll walk you through the steps for overriding templates in a WordPress Block Theme to use custom ones. This is particularly useful when developing custom post types, custom taxonomies, or unique page structures that standard WordPress templates don’t cover.

Using code that I use in my Knowledge Base plugin, we will cover:

  1. Register custom templates for different views (archive, single, taxonomy, search)
  2. Override default WordPress templates with your own block-based templates
  3. Handle template hierarchy properly
  4. Support shortcode placeholders in templates

I’m going to assume that you have a basic understanding of PHP, WordPress and plugin development. You would have already created custom post types.

Step 1: Create the Template Handler Class

This class manages how WordPress selects the custom templates for our custom post types and taxonomies. Here’s a breakdown of how it works.

  1. Filter get_block_templates: This is the most important part of the code below. It registers a callback that we will use to “insert” our block template on the fly.
  2. Filter Template Hierarchy: The constructor registers a filter for each of the template types (e.g., archive, single, taxonomy). This filter allows WordPress to recognise the custom templates and assign them based on the current context.
  3. Function Mapping: We map each template type to a callback function, which processes and assigns a specific template from the theme or plugin directory.

Here’s the initial setup for the Template_Handler class:

<?php

class Template_Handler {
    public function __construct() {
        add_filter( 'get_block_templates', array( $this, 'manage_block_templates' ), 10, 3 );

        $template_types = array(
            'archive'  => 'add_custom_archive_template',
            'index'    => 'add_custom_index_template',
            'single'   => 'add_custom_single_template',
            'taxonomy' => 'add_custom_taxonomy_template',
            'search'   => 'add_custom_search_template',
        );
        foreach ( $template_types as $type => $callback ) {
            add_filter( "{$type}_template_hierarchy", array( $this, $callback ) );
        }
    }
    // Custom template functions follow here.
}

The registration of the {$type}_template_hierarchy was needed because Query Monitor kept throwing up warnings. It’s also good practice as we tell WordPress to include our templates within the hierarchy.

Possible hook names include:

  • 404_template_hierarchy
  • archive_template_hierarchy
  • attachment_template_hierarchy
  • author_template_hierarchy
  • category_template_hierarchy
  • date_template_hierarchy
  • embed_template_hierarchy
  • frontpage_template_hierarchy
  • home_template_hierarchy
  • index_template_hierarchy
  • page_template_hierarchy
  • paged_template_hierarchy
  • privacypolicy_template_hierarchy
  • search_template_hierarchy
  • single_template_hierarchy
  • singular_template_hierarchy
  • tag_template_hierarchy
  • taxonomy_template_hierarchy

For my Knowledge Base plugin, I needed the five as specified above in $template_types.

Step 3: Manage Block Templates

The manage_block_templates method in the Template_Handler class dynamically assigns a block template based on the custom post type or taxonomy.

/**
 * Manage block templates for the wz_knowledgebase custom post type.
 *
 * @param array  $query_result Array of found block templates.
 * @param array  $query        Arguments to retrieve templates.
 * @param string $template_type Template type, either 'wp_template' or 'wp_template_part'.
 * @return array Updated array of found block templates.
 */
public function manage_block_templates( $query_result, $query, $template_type ) {
    if ( 'wp_template' !== $template_type ) {
        return $query_result;
    }

    global $post;
    if ( ( empty( $post ) && ! is_admin() ) || ( ! empty( $post ) && 'wz_knowledgebase' !== $post->post_type ) ) {
        return $query_result;
    }

    $theme        = wp_get_theme();
    $block_source = 'plugin';

    $template_name = null;

    if ( is_singular( 'wz_knowledgebase' ) ) {
        $template_name = 'single-wz_knowledgebase';
    } elseif ( is_post_type_archive( 'wz_knowledgebase' ) ) {
        $template_name = is_search() ? 'wzkb-search' : 'archive-wz_knowledgebase';
    } elseif ( is_tax( 'wzkb_category' ) && ! is_search() ) {
        $template_name = 'taxonomy-wzkb_category';
    }

    if ( $template_name ) {
        $template_file_path = $theme->get_template_directory() . '/templates/' . $template_name . '.html';
        if ( file_exists( $template_file_path ) ) {
            $block_source = 'theme';
        } else {
            $template_file_path = __DIR__ . '/templates/' . $template_name . '.html';
        }

        $template_contents = self::get_template_content( $template_file_path );
        $template_contents = self::replace_placeholders_with_shortcodes( $template_contents );

        $new_block                 = new \WP_Block_Template();
        $new_block->type           = 'wp_template';
        $new_block->theme          = $theme->stylesheet;
        $new_block->slug           = $template_name;
        $new_block->id             = 'wzkb//' . $template_name;
        $new_block->title          = 'Knowledge Base Template - ' . $template_name;
        $new_block->description    = '';
        $new_block->source         = $block_source;
        $new_block->status         = 'publish';
        $new_block->has_theme_file = true;
        $new_block->is_custom      = true;
        $new_block->content        = $template_contents;
        $new_block->post_types     = array( 'wz_knowledgebase' );

        $query_result[] = $new_block;
    }

    return $query_result;
}

Here’s how the function works:

  • Check Template Type:
    • Ensures the function runs only when $template_type is wp_template.
  • Post Type Validation:
    • Check if $post exists globally.
    • Verifies the $post->post_type matches wz_knowledgebase, skipping if not. Change this to your custom post type name.
  • Identify Template Name:
    • Determines the template file name based on the current page context:
      • single-wz_knowledgebase for single pages.
      • archive-wz_knowledgebase for archives.
      • taxonomy-wzkb_category for category archives.
      • wzkb-search for search results specific to the post type.
  • Template Source and Path:
    • Attempts to load the template from the theme’s templates directory, defaulting to the plugin’s templates directory if unavailable. This allows a theme to override the templates included in the plugin.
  • Create WP_Block_Template Object:
    • This is the most important part of the code as we create a new block template object on the fly.
    • Builds a new block template object with details like themeslugtitle, and source. Modify the various properties to fit your custom post type.
    • Populates content with template file contents, including any custom shortcode processing.
  • Update Query Results:
    • Appends the new template object to $query_result and returns it.

Step 3: Define Custom Template Loading Methods

For each template type, define a function to load a specific template when relevant conditions are met. Here are examples of archivesingle, and taxonomy templates.

public function add_custom_archive_template( $templates ) {
	if ( is_tax( 'wzkb_category' ) ) {
		return $this->add_custom_template( $templates, 'archive', 'wzkb_category', 'taxonomy-wzkb_category' );
	}
	if ( is_singular( 'wz_knowledgebase' ) ) {
		return $this->add_custom_template( $templates, 'single', 'wz_knowledgebase', 'single-wz_knowledgebase' );
	}
	return $templates;
}

private function add_custom_template( $templates, $type, $post_type, $template_name ) {
	if ( ( in_array( $type, array( 'archive', 'index', 'search' ), true ) ) ||
		( 'single' === $type && is_singular( $post_type ) ) ) {
		array_unshift( $templates, $template_name );
	}
	return $templates;
}

The add_custom_template function updates the template hierarchy based on custom conditions. For example, if viewing a taxonomy page, it’ll use taxonomy-wzkb_category.html if available.

Step 4: Replacing Placeholders with Shortcodes (optional)

This additional step allows dynamic shortcodes in block templates by finding placeholders and converting them to shortcodes. In my case, I needed to process shortcodes to display the custom search form and the knowledge base.

public static function replace_placeholders_with_shortcodes( $template_contents ) {
	// Regular expression to match placeholders like {{shortcode param="value"}}.
	$pattern = '/\{\{([a-zA-Z_]+)(.*?)\}\}/';

	// Callback function to process each match.
	$callback = function ( $matches ) {
		$shortcode = trim( $matches[1] ); // Extract the shortcode name.
		$params    = trim( $matches[2] ); // Extract any parameters.

		// Construct the shortcode with the parameters.
		if ( ! empty( $params ) ) {
			$shortcode_output = '[' . $shortcode . ' ' . $params . ']';
		} else {
			$shortcode_output = '[' . $shortcode . ']';
		}

		// Run the shortcode and return the output.
		return do_shortcode( $shortcode_output );
	};

	// Run the preg_replace_callback to find and replace all placeholders.
	return preg_replace_callback( $pattern, $callback, $template_contents );
}

The above function uses regex to find the shortcodes with their parameters and then process them using do_shortcode.

Step 5: An example template

You’ll need to create the various block templates to display the archive, single post, etc. I used the Twenty Twenty Four templates as a base as I wanted to support this out of the box.

Here’s a basic block template you can use. It includes placeholders that our function will swap out with dynamic shortcode output.

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","style":{"spacing":{"margin":{"top":"var:preset|spacing|50"}}},"layout":{"type":"constrained"}} -->
<main class="wp-block-group" style="margin-top:var(--wp--preset--spacing--50)">
    <!-- wp:group {"align":"wide","layout":{"type":"default"}} -->
    <div class="wp-block-group alignwide">
        <!-- Search placeholder -->
        {{kbsearch}}

        <!-- Breadcrumb placeholder -->
        {{kbbreadcrumb}}

        <!-- Knowledge base content placeholder (this will display only once) -->
        {{knowledgebase}}
    </div>
    <!-- /wp:group -->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

I found creating the template the most complex part especially as the plugin needs to support multiple themes.

Step 6: Initialise the Template Handler

Either in the same class file or somewhere else in your plugin initialise the Template Handler.

new Template_Handler();

Closing words

This tutorial demonstrates how to integrate custom templates into WordPress block themes. While template handling will be simplified in WordPress 6.7, this approach remains valuable for plugins needing to provide custom layouts while supporting modern WordPress features.

The code showcases how easily you can hook into WordPress’s templating system and extend it with your custom templates. Adapt these patterns to match your specific project requirements.

Check out the plugin’s GitHub repository for a complete implementation, including the full Template_Handler class and corresponding templates.

Leave a Reply

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