Template Hierarchy
How WordPress turns a finished query into a template file, from single-{post_type}-{slug}.php down to index.php.
get_query_template( string $type, string[] $templates = array() ): stringOnce the main query has run, wp-includes/template-loader.php fires template_redirect and then asks a fixed list of conditional tags, in this order, what kind of request this is: embed, 404, search, front page, home, privacy policy, post type archive, taxonomy, attachment, single, page, singular, category, tag, author, date, archive. The first tag that matches and yields a file wins; if none does, index.php is used.
Candidates, most specific first
Each matching tag calls a get_*_template() function that builds a list of filenames from most specific to least.
| Request | Candidates in order |
|---|---|
| Single post | single-{post_type}-{slug}.php, single-{post_type}.php, single.php, singular.php, index.php |
| Page | assigned page template, page-{slug}.php, page-{id}.php, page.php, singular.php, index.php |
| Category | category-{slug}.php, category-{id}.php, category.php, archive.php, index.php |
| Taxonomy term | taxonomy-{taxonomy}-{term}.php, taxonomy-{taxonomy}.php, taxonomy.php, archive.php, index.php |
| Search | search.php, index.php |
| Not found | 404.php, index.php |
Finding the file
locate_template() takes that list and, for each name in turn, checks the child theme and then the parent. Two filters sit on the result — {$type}_template_hierarchy (WordPress 4.7) edits the candidate list, {$type}_template changes the chosen path — and template_include (WordPress 3.0) has the last word for every request type. Block themes run the same hierarchy against HTML files in templates/ first.
Example
<?php
/**
* Plugin Name: Acme Doc Templates
* Ships templates for a post type the active theme knows nothing about.
*/
add_filter( 'template_include', 'acme_doc_template' );
function acme_doc_template( $template ) {
// $template is whatever the hierarchy already resolved to. Bail early and
// return it untouched for every request that is not ours.
if ( ! is_singular( 'acme_doc' ) ) {
return $template;
}
// Always let the theme win if it has its own file. locate_template()
// checks the child theme first, then the parent.
$from_theme = locate_template( array( 'single-acme_doc.php', 'single.php' ) );
if ( $from_theme ) {
return $from_theme;
}
return plugin_dir_path( __FILE__ ) . 'templates/single-acme_doc.php';
}
// Narrower alternative: add a candidate to the list instead of replacing the
// result, so a theme can still override it just by creating that filename.
add_filter(
'single_template_hierarchy',
static function ( array $templates ) {
if ( is_singular( 'acme_doc' ) && '' !== get_query_var( 'doc_version' ) ) {
array_unshift( $templates, 'single-acme_doc-versioned.php' );
}
return $templates;
}
);When to use it
- Working out which theme file is rendering a page before editing it
- Shipping fallback templates from a plugin for a custom post type
- Giving one category or one page a different layout from the rest of the archive
- Porting a classic theme's template files to a block theme's templates directory
More examples
A named page template
The one entry in the hierarchy that an editor controls rather than a developer. Use it when the choice of layout belongs to whoever writes the content; use page-{slug}.php when the layout belongs to one specific known page and must not be switchable.
<?php
/**
* Template Name: Full Width
* Template Post Type: page, acme_doc
*
* Saved as page-templates/full-width.php in the theme. WordPress reads this
* header, offers the template in the editor's sidebar, and a page assigned to
* it uses this file ahead of every other candidate in the hierarchy — before
* page-{slug}.php, before page.php.
*
* The Template Post Type header (WordPress 4.7) opens the template up to post
* types other than pages. Without it, only pages may be assigned it.
*/
get_header();
while ( have_posts() ) {
the_post();
the_content();
}
get_footer();The block theme equivalent
Shows that block themes did not replace the hierarchy, only the file format and location. Reading this side by side with a classic theme is the fastest way to port one, because the filenames map one to one.
my-block-theme/
templates/
index.html <- required; the final fallback
single.html
single-acme_doc.html <- wins for any acme_doc post
archive-acme_doc.html
category-news.html
404.html
parts/
header.html
footer.html
patterns/
theme.json
style.css <- still required for the theme header
# The hierarchy is identical: same candidate names, same order, same
# child-then-parent lookup. Only the extension and the directory change.
# A block theme may still ship PHP templates in the theme root, but they are
# not simply outranked: locate_block_template() runs the PHP hierarchy first,
# then only considers block templates of equal or higher specificity than the
# PHP file it found. An HTML template beats a PHP file of the same name, while
# a more specific single-acme_doc.php still beats a less specific single.html.Splitting a template into parts
get_template_part() applies the same child-then-parent lookup as the hierarchy itself, which is what makes partial overrides in a child theme possible. The $args array, added in WordPress 5.5, arrives in the part as $args and is the supported way to pass data down without globals.
<?php
// archive-acme_doc.php
get_header();
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
// get_template_part( $slug, $name = null, $args = array() ) looks for
// template-parts/card-{post_format}.php, then template-parts/card.php,
// checking the child theme before the parent for each.
get_template_part( 'template-parts/card', get_post_format(), array( 'compact' => true ) );
}
} else {
get_template_part( 'template-parts/content', 'none' );
}
get_footer();
Discussion