Template Hierarchy

How WordPress turns a finished query into a template file, from single-{post_type}-{slug}.php down to index.php.

Syntaxget_query_template( string $type, string[] $templates = array() ): string

Once 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.

RequestCandidates in order
Single postsingle-{post_type}-{slug}.php, single-{post_type}.php, single.php, singular.php, index.php
Pageassigned page template, page-{slug}.php, page-{id}.php, page.php, singular.php, index.php
Categorycategory-{slug}.php, category-{id}.php, category.php, archive.php, index.php
Taxonomy termtaxonomy-{taxonomy}-{term}.php, taxonomy-{taxonomy}.php, taxonomy.php, archive.php, index.php
Searchsearch.php, index.php
Not found404.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

Example · php
<?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.

Example · php
<?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.

Example · text
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.

Example · php
<?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

  • Be the first to comment on this lesson.