WordPress Template Hierarchy Explained: Which File Renders Which Page

A browser window connected to a hierarchical branching tree of file icons. Text: WP TEMPLATE HIERARCHY

Why is WordPress rendering that page with archive.php when you built a better template for it? The choice is never random: a fixed lookup order, the template hierarchy, decides which file renders every request. This DevAnswers guide maps that order for classic and block themes, proves which template loaded, and overrides it safely.

How WordPress Picks a Template 

On every request, WordPress reads the query to work out what kind of content was asked for: a single post, a static page, a category archive, a search. Each of those query types has an ordered list of candidate template filenames, and WordPress simply searches down the template hierarchy until it finds a matching file in your theme. Three rules cover the whole system:

  • Most specific wins. A file naming the exact slug beats a file for the content type, which beats a generic fallback.
  • First match ends the search. WordPress takes the first file it finds and stops looking.
  • index.php catches everything. It sits at the bottom of every chain, which is why it’s the only template file a classic theme must have. (A block theme’s equivalent bottom-of-chain fallback is templates/index.html.)

The machinery lives in core’s template loader: after the main query resolves, WordPress asks each matching conditional (is it a singular page? an archive? a search?) for its candidate list, checks the theme for each name in turn, and hands the winner to the loader. Nothing about your content changes during template selection; the hierarchy only decides which file gets to present it.

Everything else in this guide is those three rules applied to specific page types.

The Lookup Order, Page Type by Page Type 

The chains below cover the requests you’ll deal with most. Read each row left to right: the first file that exists in your theme renders the page.

RequestWordPress tries, in order
Front pagefront-page.php → then the blog chain or the page chain, depending on your Reading settings
Blog indexhome.php → index.php
Single postsingle-{post-type}-{slug}.php → single-{post-type}.php → single.php → singular.php → index.php
Static pagecustom template → page-{slug}.php → page-{id}.php → page.php → singular.php → index.php
Category archivecategory-{slug}.php → category-{id}.php → category.php → archive.php → index.php
Custom post type archivearchive-{post-type}.php → archive.php → index.php
Author archiveauthor-{nicename}.php → author-{id}.php → author.php → archive.php → index.php
Search resultssearch.php → index.php
404404.php → index.php

Two details in that table do the most damage when missed. front-page.php is checked first for the front page regardless of whether Settings › Reading shows latest posts or a static page; and for a blog index WordPress then looks for home.php, not index.php directly. If your homepage ignores edits, one of those two files is almost certainly rendering it. And when the front page is set to a static page, the chain continues into the normal page lookup after front-page.php, so a page-home.php can still win on a theme without a front-page template.

A Worked Example 

Take a request for /category/linux/ in a theme that ships category.php, archive.php, and index.php. WordPress identifies a category query, then walks the chain: category-linux.php? Not in the theme. category-{id}.php? No. category.php? Found, search over, page rendered. Now create category-linux.php and reload: the Linux archive alone switches to the new file while every other category stays on category.php. That is the entire mechanism, and it means per-slug special cases never need conditionals inside shared templates: a more specific file is the override.

Block Themes: Same Rules, HTML Files 

Block themes (Twenty Twenty-Two onward) keep the exact same hierarchy; the candidates are just .html files in the theme’s templates/ folder instead of PHP files. So a static page looks for templates/page-about.html before templates/page.html, and so on down the same chain.

There is one extra layer: anything you customize in the Site Editor is saved to the database as a wp_template post, and those user-created templates take precedence over the files on disk, ahead of child-theme templates/ files, then the parent theme’s. If a block theme ignores your file edits, check the Site Editor for a saved customization of that template; deleting it there falls back to the file.

Don’t confuse templates with template parts: the files in a block theme’s parts/ folder (header, footer, sidebar) are reusable fragments included by templates, and they sit outside the hierarchy. Only templates/ files compete in the lookup.

Custom Templates Override Everything 

A custom template assigned to a page sits at the highest level of the hierarchy for that page: it beats page-{slug}.php and everything below it. In a classic theme, a custom template is any theme PHP file with a Template Name header:

wp-content/themes/your-child-theme/landing-page.php
<?php
/**
 * Template Name: Landing Page
 */

get_header();
// Your custom layout here.
get_footer();

Once the file exists, the template appears in the page editor’s Template panel for any editor to assign. Block themes do the same job through the Site Editor: create a custom template there and assign it per page. Either way, the assignment travels with the page, which is also why a forgotten assignment is a classic wrong-template culprit (more below). One exception outranks even this: on the site’s front page, front-page.php is still checked first, ahead of an assigned custom template.

Page Hierarchy Is Not Template Hierarchy 

The two names sound alike and mean different things. Page hierarchy is a content feature: a child page organised under a parent page, inheriting the parent’s slug in its URL (/services/hosting/). Template hierarchy is the file lookup this guide covers. The overlap trips people in one specific way: the parent’s slug plays no part in template selection, so /services/hosting/ never resolves through a page-services.php. For child pages, the unambiguous routes are a custom template assigned to the page or page-{id}.php; if a slug-named file seems to be ignored on a nested page, the debug logger below will show you exactly which candidate won.

Prove Which Template Rendered the Page 

Stop guessing; measure. The quickest tool is the free Query Monitor plugin: its admin-toolbar panel reports the template file used for the current request, alongside the queries and hooks behind it.

In Query Monitor’s toolbar menu, the template filename is shown right on the overview panel. It is the fastest answer to ‘which file is this?’ and needs no code.

For a code-level answer, log what WordPress selected. Drop this in your child theme’s functions.php, load the page, and read the site’s PHP error log:

wp-content/themes/your-child-theme/functions.php
add_filter( 'template_include', function ( $template ) {
    error_log( 'Template used: ' . $template );
    return $template;
} );

template_include runs immediately before WordPress includes the template it settled on, and it receives the absolute path as its parameter, so the log line names the winning file exactly. One scope note: this snippet is for classic themes; in a block theme the filter reports the block template canvas rather than your .html template, so lean on Query Monitor there.

A third clue costs nothing: view the page source and read the <body> tag’s classes. WordPress stamps template context there, so page-template-landing-page-php means the landing-page.php custom template is active, while a bare page-template-default tells you the normal chain resolved it.

Change the Choice with Filters 

The same hook that reveals the decision can override it. Return a different path from template_include and WordPress loads that file instead:

wp-content/themes/your-child-theme/functions.php
add_filter( 'template_include', function ( $template ) {
    if ( is_page( 'pricing' ) ) {
        $custom = locate_template( 'templates/pricing-2026.php' );
        if ( $custom ) {
            return $custom;
        }
    }
    return $template;
} );

For surgical changes earlier in the process, every query type also exposes a {$type}_template_hierarchy filter (page_template_hierarchy, category_template_hierarchy, and so on) that lets you edit the candidate list itself before the search runs. That is the tool for adding a naming convention rather than forcing one file. For example, to let any category use a shared category-tech.php before the generic fallbacks:

wp-content/themes/your-child-theme/functions.php
add_filter( 'category_template_hierarchy', function ( $templates ) {
    // Insert immediately before category.php, the final candidate THIS filter sees (archive.php and index.php come later in the chain).
    array_splice( $templates, -1, 0, 'category-tech.php' );
    return $templates;
} );

The difference in altitude matters: hierarchy filters shape the candidate list and still respect anything more specific the theme provides, while template_include is the final word after the search has already happened. Prefer the hierarchy filter when both would work; it keeps normal theme overrides functioning.

And sometimes the right answer is no filter at all. A dedicated file suits a layout that genuinely differs; a conditional tag such as is_page( 'pricing' ) inside a shared template suits a small variation, one banner, one class, one extra block. If you find a template that is 90% identical to another, fold them together and let a conditional handle the difference; if the conditionals start stacking, split the file. The hierarchy rewards whichever shape keeps each template doing one clear job.

Why the Wrong Template Loads 

When the rendered page ignores the template you expected, resist editing blind. Identify the actual file first with Query Monitor or the logging filter above, then work through the usual suspects; each is a two-minute check, and together they cover nearly every wrong-template report we see.

  • A child-theme file is shadowing the parent. A template in the child theme with the same name takes precedence during resolution. If edits to the parent’s page.php do nothing, look for a page.php in the child.
  • Slug mismatch. page-about-us.php renders /about-us/, not /about/. Check the page’s real slug in the editor before renaming files.
  • A custom template is still assigned. The Template panel assignment beats your new page-{slug}.php silently.
  • Page caching. If the server or a plugin cached the old render, your template change is live but invisible; purge and retest in a private window.
  • WooCommerce pages play by their own rules. Shop templates ship inside the plugin and don’t follow the page chain; to change one, copy it from the plugin’s templates/ directory into a woocommerce/ folder in your theme, where your copy overrides the plugin’s.

Next Steps 

You can now predict the file, prove the file, and override the file. Put that to work safely: template overrides belong in a child theme so theme updates can’t erase them.

From there, two natural extensions: pull custom fields into your templates, and add per-template body classes to style each layout without touching its markup.

Support DevAnswers
Written, tested and maintained by one developer. Your support keeps these guides accurate and ad-light.
Buy me a coffee

Comments

Leave a reply