WordPress Template Hierarchy Explained: CPTs, Archives, Taxonomies, and template_include

When WordPress handles a request it determines which PHP template file to load by walking a predetermined lookup list — from the most specific template name to the most generic fallback. For a single post of the custom post type movie with the slug the-godfather, WordPress checks for single-movie-the-godfather.php first, then single-movie.php, then single.php, then singular.php, then index.php, loading the first file it finds in the active theme (child theme takes priority over parent theme at every step). The hierarchy works the same way for every request type — taxonomy archives, author pages, category pages, date archives, search results, and 404 pages each have their own lookup chain. Understanding this hierarchy means you can create precisely targeted templates for individual post types, taxonomies, and even specific terms or authors without writing any PHP conditional code inside the template file.

Problem: Your theme's single.php has grown into a file full of if ( get_post_type() === 'movie' ) and if ( is_page_template('team.php') ) conditionals, making it hard to maintain and extend.

Solution: Split the conditionals into separate template files named according to the hierarchy — single-movie.php, archive-movie.php — and let WordPress load the correct one automatically. Use the template_include filter for plugin-level overrides that must work regardless of the active theme.

Common hierarchy lookup chains — WordPress uses the first file it finds:

SINGLE POST (post type 'movie', slug 'the-godfather'):
  single-movie-the-godfather.php
  single-movie.php
  single.php
  singular.php
  index.php

ARCHIVE (post type 'movie'):
  archive-movie.php
  archive.php
  index.php

TAXONOMY TERM (taxonomy 'genre', term 'action'):
  taxonomy-genre-action.php
  taxonomy-genre.php
  taxonomy.php
  archive.php
  index.php

CATEGORY (slug 'news'):
  category-news.php
  category-3.php         ← term ID fallback
  category.php
  archive.php
  index.php

AUTHOR (login 'john-doe'):
  author-john-doe.php
  author-7.php           ← user ID fallback
  author.php
  archive.php
  index.php

PAGE (template field set to 'contact' in post meta):
  templates/contact.php  ← if declared with Template Name: comment
  page-contact.php
  page-42.php
  page.php
  singular.php
  index.php

Override template selection from a plugin with the template_include filter (runs after the theme hierarchy is resolved):

<?php
add_filter( 'template_include', 'my_plugin_override_template' );

function my_plugin_override_template( $template ) {
    // Override single 'movie' template with a file bundled in the plugin
    if ( is_singular( 'movie' ) ) {
        $plugin_template = plugin_dir_path( __FILE__ ) . 'templates/single-movie.php';
        if ( file_exists( $plugin_template ) ) {
            return $plugin_template;
        }
    }
    return $template; // return unchanged for all other requests
}

NOTE: The template_include filter is the correct tool for plugin-level template overrides because it runs after the theme hierarchy has been resolved and after child theme lookups. It ensures your plugin's template is only used when no theme template matches — you can check get_template_part() in the returned $template path to see which file the theme resolved to, then decide whether to substitute your own.