Overriding Which Template WordPress Loads
This is the last filter before a template is loaded, and it is one of the few where returning nothing produces a blank white page rather than a default. Whatever the callback returns is loaded, including an empty string.
Whatever this filter returns gets included, unchecked. template_include is the final say on which template file renders the request, after the entire template hierarchy has already been resolved. There is no validation between your return value and the include that follows, so a mistyped filename does not fall back to the default template. It produces a blank white page with a 200 status and nothing in the error log.
The shape, with the check that matters
add_filter( 'template_include', 'ti_template_include', 99 );
function ti_template_include( $template ) {
if ( ! is_singular( 'post' ) ) {
return $template;
}
if ( 'longread' !== get_post_meta( get_the_ID(), '_ti_layout', true ) ) {
return $template;
}
$custom = get_stylesheet_directory() . '/templates/single-longread.php';
return file_exists( $custom ) ? $custom : $template;
}
The file_exists check is the difference between a feature and an outage. Without it, renaming the template file later takes the site's post pages down silently, and the symptom gives no clue where to look.
Returning $template unchanged for every case you did not handle is equally load-bearing. A callback with a branch that falls through to an implicit null return hands include an empty string, with the same blank result.
Path, not URL
The value is a server path such as /var/www/site/wp-content/themes/child/single.php. Two substitutions produce a blank page:
- A URL, because
get_stylesheet_directory_uri()was used instead ofget_stylesheet_directory(). The names differ by four characters and the mistake is common. - A relative path, which resolves against the PHP include path rather than the theme directory.
locate_template() with the third argument set to false returns a full path and searches the child theme before the parent, which is usually a better source than string concatenation:
$found = locate_template( array( 'templates/single-longread.php' ), false, false );
return $found ? $found : $template;
Priority, and why 99 is not automatically right
Plugins that change templates use this filter too. A page builder, an ecommerce plugin and a membership plugin may all be here, and the one running last wins. Choosing 99 to guarantee that is a reasonable default and also a way to override a plugin doing something necessary — a membership plugin redirecting a restricted post to a login template, for instance, loses that fight and the restriction silently stops working.
Before raising the priority, check what else is attached. If another callback is deliberately overriding the template, the right fix is usually to cooperate with it rather than outrank it.
When a different hook is the right one
Reach for something else in these cases:
- Changing the query rather than the template belongs in pre_get_posts. Loading a different file to show different posts is the long way round.
- Adding markup around existing content belongs in a theme hook or in the_content, not in a template swap.
- Serving a different template per post is what a page template already does, selectable in the editor with no code at all.
- Redirecting is a redirect, not a template. Returning a template that calls
wp_redirect()sends headers after output has begun, and the result is a headers-already-sent warning rather than a redirect.
Block themes take a different route
A block theme resolves templates from HTML files in the theme's template directory, assembled through the block template system rather than by including a PHP file. The filter may not be consulted at all for those requests. A theme that mixes both will behave inconsistently, which is confusing until the reason is known. The distinction is set out in what a WordPress block theme is.
Verify by breaking it on purpose
Load the template you targeted and confirm the right file renders. Then load a post that should not match, an archive, the front page and a 404 URL, confirming each still renders its normal template.
Finally, rename your custom template file and reload. With the file_exists check in place the page falls back to the default and the site stays up. Without it you get a blank page — which is exactly the failure this check exists to prevent, and much better discovered deliberately than during a white screen incident months later.
Frequently asked
- Because the callback returned a path that does not exist, or returned an empty value. WordPress includes whatever it is given without checking, so a typo in a filename produces a page with no content and no error.
- Yes. The return value is passed to include, so it must be a full server path. A URL or a path relative to the theme will not resolve and the result is a blank page.
- Only partly. A block theme resolves its templates from HTML files through a different path, so a filter returning a PHP file may be bypassed entirely. Check how the theme builds its templates before relying on it.