.html in theme/templates) it should load when a user accesses a given URL in a WordPress-powered website. Understanding this should help you understand how to write WordPress plugins and customize it to use different templates or perform different actions for special URLs in your website.In a typical modern web framework, to add a custom handler for a URL (sometimes called a route or endpoint), it only takes one function call. In WordPress, it takes several steps. The code is really all over the place, but once you understand it you can trivially create your own function to do it in a single function call. Let's see how the algorithm works step by step:
add_rewrite_rule
First WordPress parses a URL into a set of query_vars using a set of "rewrite rules." In order to add our own custom URL patterns, we use the add a handler to the init action that calls add_rewrite_rule. WordPress has its own number of rewrite rules, and any plugins or a theme's functions.php can add theirs as well.
flush_rewrite_rules
Unlike web frameworks of other languages like Python's Flask and Django, or Node, a PHP program doesn't stay running on the web server waiting for requests to handle. Instead, it gets started from scratch every time a HTTP request is made. This means that every single time a URL is accessed, every single handler for init is executed, and when this happens the set of rewrite rules may change. In order to optimize the Regex-based rewrite rules, WordPress will create a cache of them that's only updated when flush_rewrite_rules is called.
This means that every time you change your rewrite rules, you NEED to call flush_rewrite_rules or WordPress will keep using the old, stale rewrite rules before the change.
On the other hand, you MUST NOT call flush_rewrite_rules on init, since that makes the cache unusable.
The solution to this is to use an activation hook in WordPress so that you only call flush_rewrite_rules when your plugin or theme is installed and activated.
Tip: when you go to Settings -> Permalinks on WordPress and you press the Save button, that calls flush_rewrite_rules, so that's a way to flush the rules manually.
Pretty URLs to Query Vars with Regex
What a rewrite rule does is practically take a "pretty" URL like /posts/123 and turn it into a URL with query parameters like /?p=123. For example:
add_rewrite_rule("^posts/([0-9]+)/([^/]+)/?$", 'index.php?p=$matches[1]&my_slug=$matches[2]', 'top');
The regex above has two capture groups, ([0-9]+) captures only digits, and ([^/]+) captures everything but a forward slash. The rule above turns a URL such as /posts/123/cool-post into index.php?p=123&my_slug=cool-post.
add_query_var
WordPress only considers query parameters that are registered using $wp->add_query_var or through the query_vars filter. This means that in order to add a custom query_var we need to either call the add_query_var function in an init hook from the global $wp variable or filter the array passed to the query_vars filter to add a new query var to it.
global $wp;
$wp->add_query_var('my_slug');
template-loader.php
The final major step of the template loading pipeline is template-loader.php. This is the file that decides that template to load. In it, the block template is chosen, loaded, parsed, and rendered, so it starts here and ends here.
The way it decides which templates to use is very simple.
First, it runs the action template_redirect. This allows plugins to implement their own template loader. If template_redirect doesn't exit the program, the rest of template-loader.php will run, which handles the template loading the normal WordPress way. Note that you can also use the filter template_include instead of template_redirect to override which template template-loader.php will use after it runs its normal algorithm. See [How to Use a Custom Template for a URL in WordPress] for a tutorial.
The main code is found in associative array that looks like this: tag_templates = array( 'is_embed' => 'get_embed_template', ...). Essentially, template-loader.php loops through this key-value map, calls the key, and if it returns true, calls the value. In other words, it will call is_embed(), and if that returns true, it will call get_embed_template() to decide which template to use.
The functions is_embed(), is_tag(), is_post(), etc., use query vars to decide what type of page the current URL is. The code is REALLY all over the place. is_embed() calls $wp_query->is_embed(), which is a getter that just returns the $is_embed property of WPQuery. This property is set to true in parse_query when the embed query var is not an empty string ('').
In other words, if a rewrite rule generates a URL such as index.php?embed=yep, the embed query var won't be an empty string, which means $is_embed is set to true in parse_query, which means $wp_query->is_embed() will return true, which means get_embed_template() will be called.
All other templates work similarly. For example, if the p query var is set, the template will be for a post (e.g. single.html).
If no template is matched, WordPress will fallback to get_index_template. That's why you sometimes see the homepage of your website if you make a mistake configuring these.
After getting the template, template-loader.php will call the template_include filter, and then simply use include $template to render the template.
Our next clue of how this works is what is the value of the $template variable. After all, if we include templates/my-custom-template.html, what is going to happen is that you'll get some HTML that should be in the <body> without the code from wp_head, so the stylesheets for the blocks won't be loaded, for example.
get_query_template
The get_*_template functions call get_query_template to actually get the template from a file. For example, get_index_template calls get_query_template('index'). This "index" is the same "index" of templates/index.html.
$templates = apply_filters( "{$type}_template_hierarchy", $templates );
$template = locate_template( $templates );
$template = locate_block_template( $template, $type, $templates );
In the code above, locate_template finds a non-block template that is passed as a fallback for locate_block_template, i.e. if locate_block_template can't find the block template we're looking for, the fallback will be used.
locate_block_template
This function is where the magic really happens, and there is a LOT of magic happening here, for better or for worse.
This function does four separate things.
It calls resolve_block_template to actually find the block template.
If the call succeeds, it sets the global variables $_wp_current_template_content and $_wp_current_template_id, so it has side effects.
More surprisingly, this is the function that calls the wp_head hook. Despite its name, it isn't just locating templates, it's setting up the whole block template system.
And finally, it returns the path to wp-includes/template-canvas.php.
template-canvas.php
When you take a look at the code for template-canvas.php, it becomes very clear what is happening.
$template_html = get_the_block_template_html();
?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<?php echo $template_html; ?>
<?php wp_footer(); ?>
</body>
</html>
This is the page. It's everything from <html> to </html>.
Observe that WordPress calls get_the_block_template_html() before calling wp_head() so that if a block contains code in its render callback that adds or removes stylesheets and script files that subprogram is executed before the contents of <head> are outputted.
get_the_block_template_html
The final piece of the puzzle in this code:
global $_wp_current_template_content;
$content = $wp_embed->run_shortcode( $_wp_current_template_content );
Global Variable Usage Overview
Essentially, this is what is happening:
1: template-loader.php calls get_index_template that calls get_query_template that calls locate_block_template.
2: locate_block_template returns the path to template-canvas.php if a block template is found, and sets the global variables $_wp_current_template_id and $_wp_current_template_content.
3: template-loader.php includes template-canvas.php.
4: template-canvas.php calls get_the_block_template_html.
5: get_the_block_template_html returns do_blocks($_wp_current_template_content).
In other words, the global variable set in locate_block_template 3 functions call deep in template-loader.php is what changes the behavior of the included template-canvas.php afterwards.
This is the reason why we don't like global variables. A function call in one place has side effects in a part of the program that at first glance should be completely separate.
In WordPress' defense, though, it clearly has a lot of legacy functionality to maintain in its templating system, so I guess there isn't much you can do about it.
do_blocks
This final function is what actually generates the HTML code from the block template. As we've learned already, this function has side effects, since a block being rendered may add or remove script and stylesheet files that will later be outputted in the wp_head() function call in template-canvas.php.
Conclusion
As we've seen, WordPress' templating system is incredibly convoluted making it rather difficult to wrap your head around it for the first time. Once you learn how it works, however, it becomes clear what you need to do to modify it.
For example, if you want to render a custom block template, you need to short circuit template-loader.php by exiting the program before template_redirect returns, call get_query_template yourself, and include template-canvas.php yourself.