Author: Frank Wazeter Author URL: https://wazeter.com
WordPress 5.8 marks a big step towards Full Site Editing and using Block Themes vs. traditional (classic) themes powered primarily by PHP template files. However, using currently experimental features in production is extremely risky due the developmental nature. This code shows how a WordPress theme can support and mimic Gutenberg Plugin's templating system for the block editor/template editor, while not having a dependency on the Gutenberg Plugin itself. Most existing references or documentation have a dependency of some kind on the Gutenberg plugin to make it work.
If full site editing is only fully available in testing, why implement features now in a WordPress classic theme?
The future is undeniably wrapped around full site editing - that everything can be manipulated through blocks and corresponding block patterns and templates. 5.8 introduces enough functionality today in core that we can effectively mimic what theme file structure will ultimately look like - greatly simplifying existing themes with an eye to the future, or by adding this snippet to an existing classic theme, you can be prepared for transitioning seamlessly to full site editing when it's available to core.
Block theming is dramatically simpler - and therefore faster to produce in, than classic themes. This solution allows for someone to effortlessly switch between Gutenberg plugin use and not without losing data or templating in between and without creating duplicate folders (e.g. /template-parts/
in classic + having /block-templates/
& /block-template-parts/
for block themes) that handle both PHP rendering in classic and HTML rendering for post templates in block themes.
aside: I wrote in depth about template behaviors in WordPress Core & Gutenberg here [WordPress/gutenberg#33942]
There are three critical components for using block templates in WordPress.
1 Render a template based on block markup in HTML. This is what's seen/rendered when someone visits a site on the front end.
2 Create a template for a user to create content and have it fill the content according to the theme's template so that, you know, all post pages look the same rather than recreating each one from scratch.
3 Allow a user to create a brand template, based off of that post type's default theme template (e.g. customization), that they can then use once or re-use as many times as they want.
Block themes that use the experimental features of the Gutenberg plugin achieve this rather seamlessly by referencing HTML files as the base for the template, located in /block-templates/
and /block-template-parts/
folders. This mimics how WordPress has traditionally done the task with .php template files. page.php = page.html in this case.
The template editor references the corresponding HTML template file to determine in what order blocks should appear and how they look.
However, classic WordPress themes do not render or reference HTML files as templates, so this is impossible to do by default without the Gutenberg plugin.
The result is that in a WordPress classic theme, unless you pass a block template converted to arrays to the WP_POST_TYPE obj (which is pretty error prone and a bit complicated in structure), there are no templates available in the template editor and no defaults - meaning a user has to create their own template in order to use the feature and the theme itself has no control over it.
By using a new filter hook available in WordPress 5.8, block_editor_settings_all
, we can hook into the block editor "state" and give it instructions on how it should render a template. We can pass either a string of HTML directly, or, more future-proof, reference an HTML file where it can find the HTML markup for blocks. In this case, we can simply use /block-templates/page.html / single.html etc.
I'd recommend the latter option.
The WordPress core team recommendation on how to do this is:
add_filter( 'block_editor_settings_all', function( $settings ) {
// instead of a file_get_contents() you could just pass it a string with block markup...if you really want to...
$settings['defaultBlockTemplate'] = file_get_contents( get_theme_file_path( 'block-template-default.html' ) );
return $settings;
});
The limitation of this example is that there's only one template. That means that every single post/page/custom post type you edit using the block editor will use the exact same file and have no other reference to tell it to use a different template. Considering the most basic implementation, Post's and Page's, a Page is typically more marketing-oriented and is dramatically different in it's style than a standard post. Likewise, an archive page would also be very different than either a post or a page!
That means we have to tell the block editor to only apply certain templates in certain contexts...based on the post type that's being displayed in the block editor.
This is surprisingly a little trickier than you'd think. If you reference where the filter hook is applying, you find this new function in core.
The get_block_editor_settings( array $custom_settings, WP_Block_Editor_Context $block_editor_context )
function returns settings that the block editor should use depending on the context it's given in. This is ultimately intended to extend the block editor to other areas in the admin dashboard.
Starting at line 370, we see the code where the block_editor_settings_all
filter hook is attaching to:
$editor_settings = apply_filters( 'block_editor_settings_all', $editor_settings, $block_editor_context );
if ( ! empty( $block_editor_context->post ) ) {
$post = $block_editor_context->post;
/**
* Filters the settings to pass to the block editor.
*
* @since 5.0.0
* @deprecated 5.8.0 Use the {@see 'block_editor_settings_all'} filter instead.
*
* @param array $editor_settings Default editor settings.
* @param WP_Post $post Post being edited.
*/
$editor_settings = apply_filters_deprecated( 'block_editor_settings', array( $editor_settings, $post ), '5.8.0', 'block_editor_settings_all' );
}
return $editor_settings;
Because we see a reference to $block_editor_context->post, we might assume that if we simply change that to say, ->page that it'll work with a different template on page, or that maybe we should wrap get_post() or something of that nature around the context of the editor.
But, the $block_editor_context variable isn't for filtering the context of the post type - it's for determining the overall context of where the block editor is So in this case we absolutely want it to be referencing to 'post' because...that's where we're putting our content.
So what we really need to do is grab the post type of the post of what we're editing and not worry about the $block_editor_context because we're already in the right context by default - we just need to filter the already established context, and then run a rudimentary if/else on the post_type to see what kind of post_type we're working with.
Here's the implementation I came up with:
function set_html_default_block_templates( $settings ) {
// Get the WP_Post instance (alternatively, can use get_current_screen() but this seemed to be a little finnacky for me)
$post_type = get_post();
// Get the post type from WP_Post.
// If post type == post, 'single.html', else page.html. You can reference any post type here, even custom post type.
$template_file = $post_type->post_type == 'post' ? 'single.html' : 'page.html';
// editor_settings expects an array, so we want to set [ 'defaultBlockTemplate' ] and pass it our custom HTML.
$settings[ 'defaultBlockTemplate' ] = file_get_contents(
// programmatically reference the template, rather than writing out every url.
// I set it to use /block-templates/ dir since we can seamlessly switch gutenberg on/off that way but you can use any dir.
get_theme_file_path( "/block-templates/{$template_file}" )
);
return $settings;
}
add_filter( 'block_editor_settings_all', 'set_html_default_block_templates', 10, 2 );
And voila! Add this to functions.php (or more appropriately, a dedicated file like /inc/block-editor-default.templates.php
) Now if the post_type that we're editing / creating new post for is a post, the block editor will use the Post template we created in single.html. If we're editing a page, it'll render the page.html file. If you're using more than 2 or 3 post types, you should probably convert this to a class rather than stacking a bunch of if/elseif statements.
I chose to use page for the default fallback because generally, 'posts' used for their most common purpose, blog posts, will look more or less the same, but other custom_post_types are more likely to be closer to the page template than the post template and I wanted a generalized solution.
Interestingly, with this implementation, the default template isn't really a true template - it's kind of a 'phantom' template. The template exists, is useable appropriately for the Template Editor and everything - but isn't actually recognized as a template-template (meaning it handles no front-end viewing). If there are no user templates in core, under the "Template" settings you'll only see an option for "New" because...the template doesn't actually exist...and at the same it does exist - as a defaultBlockTemplate
for the
template editor to reference to pre-fill the data for a user to create a template within the template editor.
The above set_html_default_block_templates() function won't actually render anything on the front end (remember, it's kind of like a phantom template). It's doing nothing on the front end at all. If a user in WordPress 5.8 Core w/o Gutenberg makes a user template (stored as CPT wp_template by default in WP) and uses that user built template for their content, then that post type will automatically render on the front end like it does with the Gutenberg plugin because in both core and gutenberg plugin, a user defined template always takes precedence in loading over a theme defined template.
That means that user templates that are wp_template CPTS (again, this happens by default no special configuration needed) in use are going to load up on the front end and completely ignore any and all theme set template code in .php or .html files - and that includes completely replacing the header / footer pieces.
So to render the default theme template that we want to use in our .html file, we need to echo out the contents of that html file within the php file, that looks like this:
// single.php (or any post-template.php file)
<?php
get_header();
echo do_blocks( file_get_contents(
get_theme_file_path( '/block-templates/single.html' ) ) );
get_footer();
What this is doing is mimicking the behavior of Gutenberg - but it's really just 'embedding' the markup content of the single.html
file inside the .php template. WordPress then reads the block mark up and renders the blocks - just like it would in any other context.
The header.php will load, then the content, then the get_footer like a normal PHP template. This is basically just like calling the_content() and having the php template pull the block editor stuff.
If you really want to mimic the Gutenberg plugin / FSE, what you'd want to do is in your html template, put in markup code to represent the header & footer you want, and only have the get_header() and get_footer() calling basic HTML and wp_head() stuff and closing tags in the footer.
This implementation allows for complete seamless transition between Gutenberg plugin & a WordPress core theme - it's pretty much indistinguishable for a user and all templates / renderings are saved.
If Gutenberg is active, it'll simply reference the .html file. That's why it's also a good practice to include an "index.html" file in /block-templates/ folder. This tells WordPress it's meant as a block theme and to use those folders instead of PHP. In WordPress 5.8, this doesn't happen - it'll just use the PHP and doesn't care at all that those folders exist or that an index.html file exists.
But in Gutenberg, and presumably later versions of WP, that check will happen (if /block-templates/index.html exists, then use .html template files to put it simply), so it'll just default to using the /block-templates/ and /block-template-parts/ directories and using the .html template files directly and your PHP template files will be completely ignored.
In Gutenberg there's a block that handles template-parts. That's ultimately where you'll put your header and footer and things like that. WP Core right now doesn't have that, so to duplicate it you'd have to replicate all that process...which would mean that all that gets duplicated if you switch to Gutenberg.
To prevent that, here's what we can do:
// header.php
<?php ?>
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<?php wp_head(); ?>
</head>
<body>
<?php
// Reference the header.html block-template parts
echo do_blocks( file_get_contents(
get_theme_file_path( '/block-template-parts/header.html' ) ) );
?>
// footer.php
<?php
echo do_blocks( file_get_contents(
get_theme_file_path( '/block-template-parts/footer.html' ) ) );
wp_footer(); ?>
</body>
</html>
Now, we can add markup to the header and footer.html files, and it'll render in the correct places.
if we add:
// /block-template/single.html or other block-template html file.
<!-- wp:template-part {"slug":"header","tagName":"header","align":"full"} /-->
// the rest of our content
<!-- wp:template-part {"slug":"footer","tagName":"footer","layout":{"inherit":true}} /-->
What will happen is, in WordPress core, these will be completely ignored because the blocks don't exist. But in Gutenberg, it'll render those template part files, while completely ignoring the .php files. (Remember, must have a file 'index.html' in /block-templates/ dir).
In this way, we're only designing default theme templates once and mimicing the logic of what will come with FSE.
Note: a drawback of this implementation is that, until the core/template-part block exists, the Template editor will throw a warning at the top and bottom and say: "Your site doesn't include support for the "core/template-part" block. You can leave this block intact or remove it entirely. You could probably get a little more creative to remove this, as it could be potentially alarming for the user, but it won't actually impact any rendering or functionality.
With a simple solution, this pretty much works perfectly out of the box between themes, but there may be unique configurations that cause some goofiness when Gutenberg is active (such as an open ended else that appliess a default template to any other post type), so we should go ahead and disable it if Gutenberg is active.
This could be done by wrapping the function around something like this:
Code referenced from: [https://wordpress.stackexchange.com/questions/309862/check-if-gutenberg-is-currently-in-use]
add_action( 'admin_enqueue_scripts', 'wpse_gutenberg_editor_action' );
function wpse_is_gutenberg_editor() {
if( function_exists( 'is_gutenberg_page' ) && is_gutenberg_page() ) {
return true;
}
$current_screen = get_current_screen();
if ( method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {
return true;
}
return false;
}
function wpse_gutenberg_editor_action() {
if( wpse_is_gutenberg_editor() ) {
// your gutenberg editor related CODE here
}
else {
// this is not gutenberg.
// this may not even be an editor, you need to check the screen if you need to check for another editor.
}
}