WordPress Template Hierarchy: The Complete Technical Guide
WordPress's Template Hierarchy is the deterministic resolution system WordPress uses to select which PHP template file renders a given page request. When a visitor loads any URL on your site, WordPress evaluates the query context — post type, taxonomy, slug, ID, and more — then walks down a prioritized list of candidate filenames until it finds a match in your active theme directory. If no specific template exists, it falls back to index.php.
Understanding this system at a deep level is not optional for serious WordPress development. It is the foundation of every custom layout, every theme override, and every performance optimization involving template caching. Whether you are running a content-heavy publication, a WooCommerce store, or a headless WordPress setup, the hierarchy governs what PHP executes on every single page load.
What the Template Hierarchy Actually Is
At its core, the Template Hierarchy is a lookup chain baked into WordPress core (wp-includes/class-wp-query.php and wp-includes/template.php). When WordPress finishes parsing the request and populating the global $wp_query object, it calls get_template_part() equivalents internally to resolve the correct template. The resolution is not random — it is a strict, ordered list of filenames checked against your theme's root directory.
The key architectural insight most tutorials miss: WordPress does not scan your theme directory. It constructs a prioritized array of candidate filenames based on query variables, then checks for each file using locate_template(). This distinction matters when you are debugging missing templates or building programmatic theme generators.
The fallback chain always terminates at index.php. This file is the only template file that WordPress requires every theme to include, per the theme development standards.
Core Template Files Every Theme Must Understand
| Template File | Triggers When | Fallback To |
|---|---|---|
front-page.php | Static front page is set in Settings > Reading | home.php |
home.php | Blog posts index page | index.php |
single-{post-type}.php | Single post of a specific custom post type | single.php |
single.php | Any single post (default post type) | singular.php |
singular.php | Any single post or page (generic catch-all) | index.php |
page-{slug}.php | A specific page by slug | page-{ID}.php |
page-{ID}.php | A specific page by database ID | page.php |
page.php | Any static page | singular.php |
category-{slug}.php | Category archive by slug | category-{ID}.php |
category-{ID}.php | Category archive by term ID | category.php |
category.php | Any category archive | archive.php |
tag-{slug}.php | Tag archive by slug | tag-{ID}.php |
tag.php | Any tag archive | archive.php |
taxonomy-{tax}-{term}.php | Custom taxonomy, specific term | taxonomy-{tax}.php |
taxonomy-{tax}.php | Custom taxonomy, any term | taxonomy.php |
taxonomy.php | Any custom taxonomy archive | archive.php |
author-{nicename}.php | Author archive by user nicename | author-{ID}.php |
author-{ID}.php | Author archive by user ID | author.php |
author.php | Any author archive | archive.php |
archive-{post-type}.php | Custom post type archive | archive.php |
archive.php | Any archive (date, author, taxonomy) | index.php |
date.php | Date-based archive | archive.php |
search.php | Search results page | index.php |
404.php | No matching content found | index.php |
attachment.php | Single attachment page | single.php |
embed.php | oEmbed frame for a post | index.php |
index.php | Universal fallback | — |
Note the singular.php entry — this is a template that many developers overlook entirely. Introduced in WordPress 4.3, it sits between single.php/page.php and index.php in the hierarchy, acting as a unified catch-all for any singular content view. If your theme includes singular.php, it will catch cases where neither single.php nor page.php exists.
The Full Template Resolution Order by Page Type
Single Blog Posts
When a visitor requests a standard post (post_type = 'post'), WordPress checks in this exact order:
single.phpsingular.phpindex.php
single-post-{slug}.php (WordPress 4.4+, e.g., single-post-hello-world.php)
single-post.phpThe slug-based variant at step 1 is rarely documented but extremely useful for giving a single flagship post a completely unique layout without touching any other template.
Custom Post Types
For a custom post type registered as portfolio:
single-portfolio-{slug}.phpsingle-portfolio.phpsingle.phpsingular.phpindex.php
For its archive (requires 'has_archive' => true in register_post_type()):
archive-portfolio.phparchive.phpindex.php
A common pitfall: registering a custom post type with 'has_archive' => false (the default) and then wondering why archive-portfolio.php never loads. The archive URL simply returns a 404 in that case.
Static Pages
- Template file set via Page Attributes meta box (custom page template)
page-{slug}.phppage-{ID}.phppage.phpsingular.phpindex.php
Custom page templates are PHP files in your theme directory that include a file header comment:
<?php
/**
* Template Name: Full Width Layout
* Template Post Type: page
*/The Template Post Type declaration (WordPress 4.7+) restricts which post types can use this template from the editor. Without it, the template appears in the Page Attributes dropdown for pages only.
Category Archives
category-{slug}.phpcategory-{ID}.phpcategory.phparchive.phpindex.php
Custom Taxonomy Archives
For a taxonomy registered as genre with a term slug of thriller:
taxonomy-genre-thriller.phptaxonomy-genre.phptaxonomy.phparchive.phpindex.php
Author Archives
author-{user_nicename}.phpauthor-{user_ID}.phpauthor.phparchive.phpindex.php
The Front Page (Critical Edge Case)
This is the most misunderstood part of the hierarchy. WordPress distinguishes between two front-page scenarios:
Scenario A — Blog posts index as front page (Settings > Reading: "Your latest posts"):
front-page.phphome.phpindex.php
Scenario B — Static page as front page (Settings > Reading: "A static page"):
front-page.php
page.php (if no front-page.php exists)
index.phpThe critical nuance: front-page.php takes precedence in both scenarios. If front-page.php exists in your theme, it always renders the front page regardless of the Reading settings. This surprises many developers who create front-page.php for a static homepage but forget it will also override the blog index if they later switch the setting.
Search Results and 404
Search results:
search.phpindex.php
404 error pages:
404.phpindex.php
A well-crafted 404.php is a conversion asset, not an afterthought. It should include a search form, popular content links, and clear navigation — all of which require understanding the template system to implement correctly.
How WordPress Resolves Templates Internally
Understanding the internal mechanics helps when debugging or extending the system. The resolution process in wp-includes/template.php works as follows:
// Simplified representation of WordPress template resolution
function get_query_template( $type, $templates = array() ) {
$type = preg_replace( '|[^a-z0-9-]+|', '', $type );
if ( empty( $templates ) ) {
$templates = array( "{$type}.php" );
}
// Fires before template resolution — allows plugins/themes to modify the list
$templates = apply_filters( "_{$type}_template_hierarchy", $templates );
$template = locate_template( $templates );
// Fires after template is located — allows final override
$template = apply_filters( "{$type}_template", $template, $type, $templates );
return $template;
}Two filter hooks are critical here:
_{$type}_template_hierarchy — fires before the file lookup, letting you inject additional candidates into the array
{$type}_template — fires after the file is located, letting you swap the resolved template path entirely
These hooks are how page builder plugins, multisite networks, and WooCommerce override templates without touching theme files.
Programmatically Overriding the Template Hierarchy
Injecting a Custom Template Path
add_filter( 'single_template_hierarchy', function( $templates ) {
// Prepend a plugin-directory template before theme templates are checked
if ( is_singular( 'portfolio' ) ) {
array_unshift( $templates, plugin_dir_path( __FILE__ ) . 'templates/single-portfolio.php' );
}
return $templates;
} );
Overriding a Template After Resolution
add_filter( 'template_include', function( $template ) {
if ( is_singular( 'portfolio' ) && current_user_can( 'edit_posts' ) ) {
// Load a debug template for editors
$debug_template = get_stylesheet_directory() . '/debug/single-portfolio-debug.php';
if ( file_exists( $debug_template ) ) {
return $debug_template;
}
}
return $template;
} );
The template_include filter is the last hook before WordPress loads the template file. It receives the fully resolved path and must return a valid file path.
Template Parts and the get_template_part() Function
Template parts are reusable PHP fragments loaded via get_template_part(). They follow their own mini-hierarchy:
// Loads content-video.php if it exists, falls back to content.php
get_template_part( 'template-parts/content', 'video' );
WordPress 5.5 added a third parameter for passing data to template parts:
get_template_part( 'template-parts/card', 'product', array(
'post_id' => get_the_ID(),
'show_price' => true,
) );
Inside the template part, retrieve this data with:
$args = wp_parse_args( $args, array(
'post_id' => 0,
'show_price' => false,
) );
This eliminates the need to use global variables for passing data between templates — a significant improvement for maintainability and testability.
Child Themes and the Template Override System
Child themes extend the hierarchy by prepending the child theme directory to the template search path. When locate_template() runs, it checks:
Child theme directory (get_stylesheet_directory())
Parent theme directory (get_template_directory())
This means you can override any parent theme template by creating a file with the identical name in your child theme. You do not need to copy the entire file — only the parts you want to change — but WordPress loads the file as a complete unit, so you must include all required markup.
Common child theme mistake: Copying functions.php from the parent theme into the child theme and expecting it to replace the parent's functions. Unlike other template files, functions.php in a child theme is loaded in addition to the parent's functions.php, not instead of it. Both files execute.
To create a minimal child theme structure:
my-child-theme/
├── style.css (required — contains theme header comment)
├── functions.php (optional — enqueue parent styles here)
└── single-post.php (overrides parent's single-post.php)
The style.css header must declare the parent:
/*
Theme Name: My Child Theme
Template: parent-theme-directory-name
*/
Debugging Which Template Is Active
Method 1: Query Monitor Plugin
The Query Monitor plugin (free, WordPress.org) displays the resolved template file and the full candidate hierarchy in its admin toolbar panel. This is the most reliable debugging tool available and adds negligible overhead.
Method 2: The template_include Hook
add_filter( 'template_include', function( $template ) {
if ( current_user_can( 'manage_options' ) ) {
echo '<!-- Template: ' . esc_html( str_replace( ABSPATH, '', $template ) ) . ' -->';
}
return $template;
} );
This outputs the template path as an HTML comment visible only to administrators. Remove it before deploying to production.
Method 3: WP_DEBUG and Logging
On a development server, enable debug logging in wp-config.php:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
Then add temporary logging inside template_include:
add_filter( 'template_include', function( $template ) {
error_log( 'Resolved template: ' . $template );
return $template;
} );
On a VPS Hosting environment with root access, you can tail the debug log in real time:
tail -f /var/www/html/wp-content/debug.log
This approach is far more reliable than relying on browser-based debugging tools when troubleshooting template resolution on a live or staging server.
WooCommerce and the Template Override System
WooCommerce ships its own template hierarchy that sits on top of WordPress's native system. WooCommerce templates live in wp-content/plugins/woocommerce/templates/ and are loaded via its own wc_get_template() function, which checks for overrides in:
wp-content/themes/your-theme/woocommerce/wp-content/themes/your-child-theme/woocommerce/To override WooCommerce's single product template, copy woocommerce/templates/single-product.php to your-theme/woocommerce/single-product.php. Never edit plugin template files directly — they are overwritten on every plugin update.
WooCommerce also hooks into woocommerce_template_single_* action hooks, which gives you granular control over individual sections (price, tabs, add-to-cart button) without overriding entire template files. This is the preferred approach for minor modifications.
Block Themes and the Template Hierarchy in Full Site Editing
WordPress 5.9 introduced Full Site Editing (FSE) with block themes, which changes how the template hierarchy works in practice. Block themes store templates as HTML files in a templates/ directory rather than PHP files:
my-block-theme/
├── templates/
│ ├── index.html
│ ├── single.html
│ ├── page.html
│ ├── archive.html
│ └── 404.html
├── parts/
│ ├── header.html
│ └── footer.html
└── theme.jsonThe resolution logic follows the same hierarchy rules, but WordPress now also checks the database for user-customized templates saved via the Site Editor. The lookup order becomes:
- User-saved template in the database (post type
wp_template) - Theme's
templates/directory HTML file - Parent theme's
templates/directory HTML file - WordPress's bundled fallback templates
Classic PHP themes and block themes can coexist in a WordPress installation, but you cannot mix PHP templates and HTML block templates within a single theme. If your theme has a templates/ directory and a valid theme.json, WordPress treats it as a block theme.
For teams running performance-critical workloads on Dedicated Servers, understanding this distinction is essential when evaluating theme frameworks — block themes offload template rendering to the block parser, which has different caching characteristics than PHP template execution.
Performance Implications of the Template Hierarchy
Each template resolution involves filesystem checks via locate_template(). On a high-traffic site, this can add measurable overhead if not cached. Key optimizations:
Object caching: Use a persistent object cache (Redis or Memcached) to cache the results of WP_Query and reduce the number of database queries that feed into template selection.
OPcache: Ensure PHP OPcache is enabled and properly configured. Since templates are PHP files, OPcache compiles them to bytecode on first load and serves subsequent requests from memory. On a properly configured VPS with cPanel, OPcache is typically enabled by default but may require tuning opcache.memory_consumption and opcache.max_accelerated_files for large themes with many template files.
Avoid unnecessary template files: Every template file in your theme directory is a candidate that locate_template() must check. Themes with hundreds of template files (common in bloated commercial themes) increase filesystem I/O on every uncached request. Audit your theme and remove unused templates.
Full-page caching: Tools like WP Rocket, W3 Total Cache, or server-level caching (Nginx FastCGI cache, Varnish) bypass PHP template execution entirely for anonymous users. Template hierarchy resolution only runs when the cache misses.
Practical Customization Patterns
Pattern 1: Category-Specific Layout Without a Plugin
Create category-news.php in your theme directory. WordPress automatically uses it for the "news" category archive. No plugin, no filter hook — just a file with the correct name.
<?php
/**
* Template for the "news" category archive.
* Inherits from: category.php → archive.php → index.php
*/
get_header();
?>
<main class="news-archive">
<h1><?php single_cat_title(); ?></h1>
<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
<?php get_template_part( 'template-parts/card', 'news' ); ?>
<?php endwhile; endif; ?>
<?php the_posts_pagination(); ?>
</main>
<?php get_footer(); ?>Pattern 2: Per-Author Layout for Featured Contributors
// author-jane-smith.php — loads only for the author with nicename "jane-smith"
get_header();
?>
<div class="featured-author-layout">
<?php get_template_part( 'template-parts/author', 'featured' ); ?>
<!-- Custom bio section, social links, etc. -->
</div>
<?php get_footer(); ?>Pattern 3: Conditional Logic Inside index.php
If you are building a minimal theme and want to avoid multiple template files, you can use conditional tags inside index.php:
<?php get_header(); ?>
<?php if ( is_front_page() && is_home() ) : ?>
<?php get_template_part( 'template-parts/home', 'blog-index' ); ?>
<?php elseif ( is_front_page() ) : ?>
<?php get_template_part( 'template-parts/home', 'static' ); ?>
<?php elseif ( is_single() ) : ?>
<?php get_template_part( 'template-parts/content', get_post_type() ); ?>
<?php elseif ( is_archive() ) : ?>
<?php get_template_part( 'template-parts/archive', 'default' ); ?>
<?php else : ?>
<?php get_template_part( 'template-parts/content', 'none' ); ?>
<?php endif; ?>
<?php get_footer(); ?>This pattern is used by themes like Twenty Twenty-One and is perfectly valid. The trade-off is that a single large index.php becomes harder to maintain as complexity grows.
Multisite and Template Hierarchy
In a WordPress Multisite network, each site in the network can use a different active theme. The template hierarchy operates identically per site, but network-activated plugins can use template_include or _{$type}_template_hierarchy filters to inject shared templates across all sites without duplicating theme files.
A common multisite pattern is a "network template" directory outside the web root that is referenced via plugin-level filter hooks. This allows a central design team to push template updates to all sites simultaneously — a significant operational advantage for agencies managing dozens of client sites on a single Shared Web Hosting or VPS environment.
SSL, Security, and Template File Permissions
Template files are PHP and must be treated as executable code. Incorrect file permissions are a common attack vector. On a Linux server, theme template files should be owned by the web server user (typically www-data or nginx) and set to mode 644:
find /var/www/html/wp-content/themes/your-theme -type f -name "*.php" -exec chmod 644 {} ;
find /var/www/html/wp-content/themes/your-theme -type d -exec chmod 755 {} ;Never set PHP files to 777. If a template file requires write access (unusual and generally inadvisable), use 664 with proper group ownership instead.
Pairing your WordPress installation with a valid SSL Certificate ensures that template-rendered content — including dynamically generated pages with sensitive user data — is always transmitted over HTTPS. This is non-negotiable for any site using contact forms, user accounts, or e-commerce.
Template Hierarchy Reference: Visual Flow
Request URL
|
v
WordPress Query Resolution (WP_Query)
|
+-- Is it the front page?
| front-page.php → home.php → index.php
|
+-- Is it a single post?
| single-{type}-{slug}.php → single-{type}.php → single.php → singular.php → index.php
|
+-- Is it a static page?
| [custom template] → page-{slug}.php → page-{ID}.php → page.php → singular.php → index.php
|
+-- Is it a category archive?
| category-{slug}.php → category-{ID}.php → category.php → archive.php → index.php
|
+-- Is it a custom taxonomy?
| taxonomy-{tax}-{term}.php → taxonomy-{tax}.php → taxonomy.php → archive.php → index.php
|
+-- Is it an author archive?
| author-{nicename}.php → author-{ID}.php → author.php → archive.php → index.php
|
+-- Is it a date archive?
| date.php → archive.php → index.php
|
+-- Is it search results?
| search.php → index.php
|
+-- Is it a 404?
404.php → index.phpDecision Matrix: When to Use Which Customization Approach
| Scenario | Recommended Approach | Avoid |
|---|---|---|
| Override one specific category | Create category-{slug}.php in theme | Modifying archive.php directly |
| Override one specific page | Create page-{slug}.php or use custom template header | Editing page.php |
| Modify WooCommerce template | Copy to theme/woocommerce/ directory | Editing plugin files |
| Modify parent theme template | Create identical file in child theme | Editing parent theme files |
| Apply logic to multiple page types | Use conditional tags in a shared template | Duplicating code across templates |
| Inject templates from a plugin | Use _{$type}_template_hierarchy filter | Hardcoding paths in theme files |
| Override any template as last resort | Use template_include filter | Using exit() or die() in templates |
| Block theme customization | Use Site Editor or templates/ HTML files | Mixing PHP and HTML block templates |
Key Technical Takeaways
index.phpis mandatory. Every theme must include it. It is the universal fallback that terminates every resolution chain.singular.phpis the underused middle layer. It catches any single post or page when neithersingle.phpnorpage.phpexists. Use it in minimal themes to reduce file count.front-page.phpoverrides everything for the front page, regardless of the Reading settings. If it exists, it loads — always.- File naming is case-sensitive on Linux servers.
Category-News.phpwill not match when WordPress looks forcategory-news.php. This is a silent failure that is difficult to debug without Query Monitor. - The
template_includefilter is the master override. It fires after all hierarchy resolution is complete and gives you a final opportunity to swap any template for any reason. - Block themes store templates as HTML, not PHP. The hierarchy logic is identical, but the file format and directory structure differ fundamentally from classic themes.
- Child theme
functions.phploads in addition to the parent's, not instead of it. All other template files follow the standard override pattern. - OPcache tuning matters at scale. On high-traffic sites, ensure
opcache.max_accelerated_filesexceeds the total number of PHP files in your WordPress installation, including all theme templates. - WooCommerce templates live outside the WordPress hierarchy. They require a separate override workflow via the
woocommerce/subdirectory in your theme. - Pair your template customization work with a properly configured domain and Domain Registration to ensure canonical URL consistency across all template-rendered pages.
FAQ
What happens if WordPress cannot find any template file in the hierarchy?
WordPress always finds index.php because it is required for every valid theme. If index.php is missing, WordPress throws a fatal error and displays a blank page or server error. This is the only template file whose absence breaks the site entirely.
Can I use the template hierarchy in a plugin without modifying the active theme?
Yes. Use the _{$type}_template_hierarchy filter to prepend a plugin-directory path to the candidate array before locate_template() runs, or use template_include to swap the resolved template path after resolution. This is how WooCommerce, bbPress, and most major plugins inject their own templates without requiring theme modifications.
Why does front-page.php override my blog index even when I set "Your latest posts" in Reading settings?
Because front-page.php takes unconditional precedence for the front page in all Reading configurations. If you want the blog index to use home.php instead, rename or remove front-page.php from your theme. Inside front-page.php, use is_home() to detect whether the front page is also the blog index and render accordingly.
How do I find out which template file WordPress is currently using for a specific page?
Install the Query Monitor plugin. It displays the resolved template path and the full candidate hierarchy in the admin toolbar on every page load. Alternatively, add a temporary template_include filter that outputs the path as an HTML comment visible only to administrators.
Does the template hierarchy work the same way in WordPress Multisite?
The per-site resolution logic is identical. Each site resolves templates against its own active theme. The difference is at the network level: network-activated plugins can use filter hooks to inject shared templates across all sites, and the get_stylesheet_directory() function returns the correct path for each individual site's active theme, not a shared network path.
