Template Hierarchy

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages.

Every front-end request ends with WordPress picking one template to render it: the file (or block template) that draws a single book, the blog index, a genre archive or the "not found" page. The template hierarchy is the rule that makes that choice. For each kind of request it lists candidate template names from the most specific (this one book) to the most generic (any single post), takes the first one that exists, and falls back to index when nothing else matches. The same rule serves block themes, whose templates are templates/*.html files or templates edited in the Site Editor, and classic themes, whose templates are PHP files. Theme developers need it to know which file to create; plugin developers need its filters to add candidates or supply templates of their own.

This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the version to run in production), PHP 7.4 minimum and 8.3+ recommended, theme.json version 3 and WP-CLI 2.12.0, and checked on 2026-10-28. WordPress 7.2 is in beta and due in December 2026; no change to the hierarchy has been announced for it. Every candidate list on this page was logged from real requests to a 7.1.3 site running the Lumen Press block theme lumen and classic theme lumen-classic (see Seeing the hierarchy on a real site).

The files a block theme ships and the blocks inside them are described in Templates and Template Parts; classic template files and The Loop in Classic Theme Anatomy and The Loop. This page covers only how one of them is chosen.

How the template loader works

index.php loads wp-blog-header.php, which runs the main query (wp()) and then includes wp-includes/template-loader.php. The loader runs in four steps:

  1. It fires the template_redirect action. Redirects belong here (with wp_safe_redirect() and exit); loading a different template here does not, because any exit skips later template_redirect callbacks.

  2. Feeds, robots.txt, favicon.ico and trackbacks are handled and the loader returns; a HEAD request exits here too (filter exit_on_http_head).

  3. It tests seventeen conditionals in a fixed order: is_embed(), is_404(), is_search(), is_front_page(), is_home(), is_privacy_policy(), is_post_type_archive(), is_tax(), is_attachment(), is_single(), is_page(), is_singular(), is_category(), is_tag(), is_author(), is_date(), is_archive(). For the first that is true it calls the matching getter (get_embed_template(), get_404_template() and so on; the post type archive getter returns nothing unless the post type has has_archive). If the getter finds nothing, the loader keeps going down the list; that is how a front page with no front-page template ends up with the page template. When nothing is found, it calls get_index_template().

  4. It passes the result through the template_include filter, resolves it with realpath() and includes it only if it is an existing, readable .php or .html file. The action wp_before_include_template (6.9) fires just before the include.

Each getter builds an array of candidate file names and hands it to get_query_template( $type, $templates ), which:

  1. strips everything but a-z, 0-9 and - from the type, which is why the front-page hook is frontpage and the privacy-policy hook privacypolicy;

  2. applies the {type}_template_hierarchy filter (4.7) to the candidate list;

  3. calls locate_template(), which returns the first candidate that exists as a PHP file in the child theme, then the parent theme, then wp-includes/theme-compat/ (since 7.1.2 a name containing .. is only accepted if it resolves inside one of those directories);

  4. calls locate_block_template() (5.8), which looks for a block template and, if it finds one, returns wp-includes/template-canvas.php and stores the block template in globals;

  5. applies the {type}_template filter to the resulting path.

The same candidate names serve both kinds of theme: they are always written with a .php suffix, and the block template layer strips it to get a template slug (single-lp_book.php becomes the slug single-lp_book, matched by templates/single-lp_book.html). The official Theme Handbook writes the hierarchy with .html names for block themes; the order is identical.

The hierarchy by request type

The figure shows every request type, in the order the loader tests it, with its candidates and the chain it falls back to. Placeholders are written as in the Theme Handbook: {post_type}, {slug}, {id} and so on.

Template hierarchy: for each request type, in the order the loader tests it, the candidate templates from most to least specific and the fallback chain ending at index
Request (Lumen Press example) Candidates, most specific first Then

Front page (/)

front-page

"Your latest posts": the home chain. "A static page": the page chain of that page.

Blog posts index (/news/, the page chosen as "Posts page")

home, index

none

Privacy policy page

privacy-policy

the page chain

Single post (/2026/10/autumn-catalogue/)

custom template, single-{post_type}-{slug}, single-{post_type}, single

singular, index

Single book (/books/the-glass-orchard/)

custom template, single-lp_book-the-glass-orchard, single-lp_book, single

singular, index

Page (/about/)

custom template, page-{slug}, page-{id}, page

singular, index

Attachment (/books/the-glass-orchard/glass-orchard-cover/)

{type}-{subtype} (image-jpeg), {subtype} (jpeg), {type} (image), attachment

the single chain (single-attachment-…, single-attachment, single), singular, index

Post type archive (/books/)

archive-{post_type} (archive-lp_book), archive

index

Custom taxonomy term (/genre/literary-fiction/)

taxonomy-{taxonomy}-{term}, taxonomy-{taxonomy}-{term_id} (6.9), taxonomy-{taxonomy}, taxonomy

archive, index

Category (/category/news/)

category-{slug}, category-{id}, category

archive, index

Tag

tag-{slug}, tag-{id}, tag

archive, index

Author (/author/tom-author/)

author-{nicename}, author-{id}, author

archive, index

Date (/2026/10/)

date

archive, index

Search (/?s=orchard)

search

index

Not found

404

index

Embed (/books/the-glass-orchard/embed/)

embed-{post_type}-{post_format}, embed-{post_type}, embed

the core embed template

Details that the table compresses:

  • Slugs with non-ASCII characters are tried decoded first and then URL-encoded (page-über before page-%c3%bcber), for categories, tags, terms, pages and single posts (4.7). Taxonomy terms then try the term ID (taxonomy-lp_genre-2), which 6.9 added; categories and tags have had the ID form for much longer.

  • Custom templates come first for singular content. For a classic theme it is the file named in the post’s _wp_page_template meta, declared with a Template Name: header (and Template Post Type: since 4.7 for posts and custom post types). For a block theme it is a slug declared in the customTemplates section of theme.json (with postTypes for anything but pages) or created in the Site Editor. The stored value goes into the list as is: a block template slug such as page-no-title appears without a .php suffix. A value that names a template the active theme does not have is skipped silently.

  • singular (4.3) is reached only when no single or page template exists; a theme that wants one template for every post, page and book ships singular and no single/page.

  • home is the posts index, not the front page. A site that shows "Your latest posts" on the front page tests front-page, then home, then index.

  • Post type archives go through get_archive_template(), so they are logged under the archive hook type, not a hook of their own. A post type registered without has_archive has no archive URL and the getter returns nothing.

  • Author and date archives fall back to archive because is_archive() is also true for them; the loader moves down the conditional list rather than having a fallback encoded in the getter.

  • Attachment pages are disabled on installations created since 6.4 (option wp_attachment_pages_enabled is 0), and their URLs redirect to the file. The attachment hierarchy applies only where the option is 1, which upgrades from older versions keep. Since 4.3 the MIME candidates run from most to least specific.

  • Embeds are not supported by the block template system. A custom embed template must be a PHP file in the theme root, in a block theme too; without one, WordPress uses wp-includes/theme-compat/embed.php instead of index.

  • paged.php and comments-popup.php are no longer part of the hierarchy (get_paged_template() is deprecated since 4.7). A paged_template_hierarchy hook is still documented, but the loader never fires it.

Block templates and PHP files

locate_block_template() runs only when the active theme supports block-templates: every block theme, and a classic theme that opts in with add_theme_support( 'block-templates' ) (5.8) so that users can create templates in the editor. It works like this:

  1. If locate_template() already found a PHP file, the candidate list is cut after that file: a block template is used only if it is at least as specific. A block theme that ships single-lp_book.php in its root and only templates/single.html renders books with the PHP file; if it also ships templates/single-lp_book.html, the block template wins the tie.

  2. resolve_block_template() queries every block template whose slug is in the remaining list and sorts them by the list’s order. For one slug, the sources rank user-edited template (a wp_template post saved from the Site Editor) over the theme file (child theme templates/, then parent) over a plugin-registered template (register_block_template(), 6.7).

  3. One child-theme rule: when the child theme has a PHP file and the parent theme a block template with the same slug, the child’s PHP file is used.

  4. If a block template is found, its markup is stored in the $_wp_current_template_content global, its ID in $_wp_current_template_id (6.3; the admin bar’s "Edit template" link uses it), and the function returns template-canvas.php, the file that prints the document around the rendered blocks. An empty template shows a warning to logged-in users (6.8).

  5. If none is found and no PHP file was either, the function returns an empty string and the loader moves on to the next conditional; for the index type it still returns the canvas, so a block theme’s templates/index.html is the final fallback. That file is what makes a theme a block theme.

The default template types the Site Editor offers in "Add template" are index, home, front-page, singular, single, page, archive, author, category, taxonomy, date, tag, attachment, search, privacy-policy and 404, plus per-object variants (a specific book, a specific genre) built from the same patterns. There is no embed entry, consistent with embeds being PHP only.

The following diagram traces the request for one Lumen Press book through the loader on the lumen block theme.

flowchart TD A["GET /books/the-glass-orchard/"] --> B["wp(): rewrite rule books/(.+) sets lp_book=the-glass-orchard; main query finds post 9"] B --> C["template_redirect action"] C --> D{"is_embed, is_404, is_search, is_front_page, is_home, is_privacy_policy, is_post_type_archive, is_tax, is_attachment?"} D -->|"all false"| E{"is_single()?"} E -->|"true"| F["get_single_template(): single-lp_book-the-glass-orchard.php, single-lp_book.php, single.php"] F --> G["single_template_hierarchy filter"] G --> H["locate_template(): first PHP file in child theme, parent theme, theme-compat"] H --> I{"Theme supports block-templates?"} I -->|"no (classic)"| P["PHP path, or empty: loader tries is_page, is_singular, then index"] I -->|"yes (lumen)"| J["locate_block_template(): keep candidates down to the PHP match"] J --> K["resolve_block_template(): wp_template posts, then theme templates/*.html, then plugin-registered"] K --> L{"Block template found?"} L -->|"yes: lumen//single-lp_book"| M["template-canvas.php"] L -->|"no"| P M --> N["single_template filter, then template_include filter"] P --> N N --> O["realpath() check, wp_before_include_template, include"]

The hierarchy filters

Three families of hooks change the result, from the most targeted to the bluntest:

Hook Receives and returns Since

{type}_template_hierarchy

The candidate array, before any lookup. Add, remove or reorder names. Works for block and classic themes alike, because both use the same list.

4.7

{type}_template

The located path (a PHP file or template-canvas.php), plus $type and $templates (4.8).

1.5

template_include

The final path, for every request type. Return another file to render with it.

3.0

pre_get_block_templates, get_block_templates, get_block_template

The block template objects (to supply or alter templates without files)

5.9

template_redirect

Nothing to return: redirect and exit. Not a place to load templates.

1.5

Adding candidates: a template per genre

Lumen Press wants literary fiction titles to look different from children’s books without assigning a template to each book. The lumen-catalog plugin inserts single-lp_book-{genre} names before single-lp_book, so the theme can ship templates/single-lp_book-literary-fiction.html (block) or single-lp_book-literary-fiction.php (classic), and both are picked up by the normal lookup.

<?php
/**
 * Adds "single-lp_book-{genre}" candidates, so a theme can style each genre's books.
 *
 * File: wp-content/plugins/lumen-catalog/includes/genre-templates.php
 */

defined( 'ABSPATH' ) || exit;

add_filter( 'single_template_hierarchy', 'lumen_catalog_genre_templates' );

function lumen_catalog_genre_templates( $templates ) {
	$book = get_queried_object();
	if ( ! $book instanceof WP_Post || 'lp_book' !== $book->post_type ) {
		return $templates;
	}

	$genres = get_the_terms( $book, 'lp_genre' );
	if ( ! is_array( $genres ) ) {
		return $templates;
	}

	$extra = array();
	foreach ( $genres as $genre ) {
		$extra[] = 'single-lp_book-' . sanitize_file_name( $genre->slug ) . '.php';
	}

	// Insert the genre templates just before "single-lp_book.php".
	$position = array_search( 'single-lp_book.php', $templates, true );
	array_splice( $templates, false === $position ? 0 : $position, 0, $extra );

	return $templates;
}

The plugin’s main file loads it with require __DIR__ . '/includes/genre-templates.php';. With a template for the genre in each theme, the logged requests show the new candidate and its winner:

lumen /books/the-glass-orchard/
  single        single-lp_book-the-glass-orchard.php single-lp_book-literary-fiction.php single-lp_book.php single.php
  => wp-includes/template-canvas.php
  => block template lumen//single-lp_book-literary-fiction (source: theme)
lumen-classic /books/the-glass-orchard/
  single        single-lp_book-the-glass-orchard.php single-lp_book-literary-fiction.php single-lp_book.php single.php
  => wp-content/themes/lumen-classic/single-lp_book-literary-fiction.php

Candidate names become file paths, so anything derived from content goes through sanitize_file_name() (or sanitize_key()) first; never build a name from raw request input.

Supplying templates from a plugin

A plugin that adds a post type should not depend on every theme shipping a template for it. lumen-catalog provides a default for both kinds of theme: for block themes it registers a block template with register_block_template() (6.7), which a theme file or a user-edited template of the same slug overrides; for classic themes it falls back to its own PHP file through template_include when the theme has no single-lp_book.php.

<?php
/**
 * Default book templates for themes that do not ship their own.
 *
 * File: wp-content/plugins/lumen-catalog/includes/templates.php
 */

defined( 'ABSPATH' ) || exit;

/*
 * Block themes: register "single-lp_book" in the template registry (WordPress 6.7+).
 * A theme file or a user-edited template with the same slug wins over it.
 */
add_action( 'init', 'lumen_catalog_register_block_templates' );

function lumen_catalog_register_block_templates() {
	if ( ! function_exists( 'register_block_template' ) ) {
		return;
	}

	register_block_template(
		'lumen-catalog//single-lp_book',
		array(
			'title'       => __( 'Single Book', 'lumen-catalog' ),
			'description' => __( 'Displays a single book of the catalogue.', 'lumen-catalog' ),
			'content'     => (string) file_get_contents( dirname( __DIR__ ) . '/templates/single-lp_book.html' ),
		)
	);
}

/*
 * Classic themes: use the plugin's PHP template when the theme has no single-lp_book.php.
 */
add_filter( 'template_include', 'lumen_catalog_template_include' );

function lumen_catalog_template_include( $template ) {
	if ( wp_is_block_theme() || ! is_singular( 'lp_book' ) ) {
		return $template;
	}

	if ( '' !== locate_template( array( 'single-lp_book.php' ) ) ) {
		return $template;
	}

	return dirname( __DIR__ ) . '/templates/single-lp_book.php';
}

The block template markup is ordinary serialized blocks; the header and footer parts are resolved from the active theme.

<!-- wp:template-part {"slug":"header","area":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:post-title {"level":1} /-->
	<!-- wp:post-terms {"term":"lp_genre"} /-->
	<!-- wp:post-featured-image /-->
	<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","area":"footer"} /-->

The classic fallback, templates/single-lp_book.php, is a regular Loop template (get_header(), the_post(), the_title(), the_terms() for lp_genre, the_content(), get_footer()), as described in Classic Theme Anatomy and The Loop.

Logged on Twenty Twenty-Five (no book template of its own), on lumen after a user saved a customized single-lp_book template in the Site Editor, and on lumen-classic without its single-lp_book.php:

twentytwentyfive /books/the-glass-orchard/
  single        single-lp_book-the-glass-orchard.php single-lp_book.php single.php
  => wp-includes/template-canvas.php
  => block template twentytwentyfive//single-lp_book (source: plugin)
lumen /books/the-glass-orchard/
  single        single-lp_book-the-glass-orchard.php single-lp_book.php single.php
  => wp-includes/template-canvas.php
  => block template lumen//single-lp_book (source: custom)
lumen-classic /books/the-glass-orchard/
  single        single-lp_book-the-glass-orchard.php single-lp_book.php single.php
  => wp-content/plugins/lumen-catalog/templates/single-lp_book.php

Two details show in that output. A registered template’s ID uses the active theme’s slug (twentytwentyfive//single-lp_book), not the plugin name; its source is plugin and its plugin property lumen-catalog. And the registry template outranks a PHP file of equal specificity in a block theme exactly as a theme’s .html file would: on lumen with single-lp_book.php in the theme root and no .html for books, the plugin’s block template was used until it was unregistered; only then did the PHP file render.

A template_include callback must return a path to an existing .php or .html file: in 7.1 anything else is not included, and an Administrator sees the theme’s errors (if any) instead of a page. Callbacks that load a template from template_redirect and exit break every later callback on that action.

Seeing the hierarchy on a real site

A must-use plugin that hooks every {type}_template_hierarchy filter at the last priority and template_include records which candidates each request produced and what was finally included. It does nothing unless WP_DEBUG is on, writes to a file under uploads/ (readable over HTTP on a misconfigured server, so use it only on a local site), and must not be deployed to production.

<?php
/**
 * Plugin Name: Lumen Template Debug
 * Description: Logs the template candidates and the chosen template of every front-end request. Development only.
 *
 * File: wp-content/mu-plugins/lumen-template-debug.php
 */

defined( 'ABSPATH' ) || exit;

if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
	return;
}

/*
 * Every query type that get_query_template() can pass to "{$type}_template_hierarchy".
 */
$lumen_template_types = array(
	'embed', '404', 'search', 'frontpage', 'home', 'privacypolicy', 'taxonomy',
	'attachment', 'single', 'page', 'singular', 'category', 'tag', 'author',
	'date', 'archive', 'paged', 'index',
);

foreach ( $lumen_template_types as $lumen_type ) {
	add_filter(
		"{$lumen_type}_template_hierarchy",
		static function ( $templates ) use ( $lumen_type ) {
			$GLOBALS['lumen_template_trace'][] = sprintf( '  %-13s %s', $lumen_type, implode( ' ', $templates ) );
			return $templates;
		},
		PHP_INT_MAX
	);
}

add_filter(
	'template_include',
	static function ( $template ) {
		global $_wp_current_template_id;

		$lines   = $GLOBALS['lumen_template_trace'] ?? array();
		$lines[] = '  => ' . str_replace( ABSPATH, '', $template );
		if ( ! empty( $_wp_current_template_id ) ) {
			$block_template = get_block_template( $_wp_current_template_id );
			$lines[]        = sprintf(
				'  => block template %s (source: %s)',
				$_wp_current_template_id,
				$block_template ? $block_template->source : 'unknown'
			);
		}

		error_log(
			sprintf( "%s %s\n%s\n", get_stylesheet(), $_SERVER['REQUEST_URI'], implode( "\n", $lines ) ),
			3,
			WP_CONTENT_DIR . '/uploads/lumen-template-debug.log'
		);

		return $template;
	},
	PHP_INT_MAX
);

The source printed for a block template is theme for a file, custom for a template saved in the database and plugin for a registered one.

The test site

The site runs the official Apache image (so real HTTP requests go through index.php) next to a WP-CLI container, with the plugin, the mu-plugin and the two themes mounted read-only. It extends the standard stack of Local Development Environments; the passwords are placeholders for a local machine. Inside the Compose network the web container answers as lumenpress.test over plain HTTP, so the site URL is http://lumenpress.test rather than the https://lumenpress.test/ of a full local setup.

# compose.yaml
name: lumen-th
x-wp-db: &wp-db
  WORDPRESS_DB_HOST: db
  WORDPRESS_DB_USER: wordpress
  WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD:?set WP_DB_PASSWORD}
  WORDPRESS_DB_NAME: wordpress
x-wp-code: &wp-code
  - wp:/var/www/html
  - ./site/mu-plugins:/var/www/html/wp-content/mu-plugins:ro
  - ./site/plugins/lumen-catalog:/var/www/html/wp-content/plugins/lumen-catalog:ro
  - ./site/themes/lumen:/var/www/html/wp-content/themes/lumen:ro
  - ./site/themes/lumen-classic:/var/www/html/wp-content/themes/lumen-classic:ro
services:
  db:
    image: mariadb:11.8
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: ${WP_DB_PASSWORD:?set WP_DB_PASSWORD}
      MARIADB_RANDOM_ROOT_PASSWORD: "1"
    tmpfs: [/var/lib/mysql]
  web:
    image: wordpress:7.1.3-php8.4-apache
    depends_on: [db]
    environment: *wp-db
    networks:
      default:
        aliases: [lumenpress.test]
    volumes: *wp-code
  cli:
    image: wordpress:cli-2.12.0-php8.4
    depends_on: [web]
    user: "33:33"
    environment:
      <<: *wp-db
      WP_ADMIN_PASSWORD: ${WP_ADMIN_PASSWORD:?set WP_ADMIN_PASSWORD}
    volumes: *wp-code
    entrypoint: ["sleep", "infinity"]
volumes:
  wp: {}

The seed script installs WordPress, creates the canonical users and the content each request type needs: a static front page and a posts page, a post by tom.author, an About page, a book in a genre and an attachment. It enables attachment pages, which are off on new installations. The plugin’s main file registers lp_book (has_archive, rewrite slug books) and lp_genre (rewrite slug genre) as in Custom Post Types and Taxonomies and requires includes/templates.php; the themes ship the templates named with each log below.

export WP_DB_PASSWORD='<a-local-db-password>' WP_ADMIN_PASSWORD='<a-local-admin-password>'
docker compose up -d
# The web container copies WordPress into the volume and writes wp-config.php on first start.
until docker compose exec -T cli wp db check >/dev/null 2>&1; do sleep 2; done
docker compose exec -T cli sh <<'EOF'
set -e
wp core install --url=http://lumenpress.test --title="Lumen Press" --admin_user=admin \
  --admin_email=admin@lumenpress.test --admin_password="$WP_ADMIN_PASSWORD" --skip-email
wp config set WP_DEBUG true --raw
wp plugin activate lumen-catalog
wp rewrite structure '/%year%/%monthnum%/%postname%/'
wp user create maria.editor maria@lumenpress.test --role=editor --user_pass="$WP_ADMIN_PASSWORD"
wp user create tom.author tom@lumenpress.test --role=author --user_pass="$WP_ADMIN_PASSWORD"
HOME_ID=$(wp post create --post_type=page --post_status=publish --post_title=Home --porcelain)
NEWS_ID=$(wp post create --post_type=page --post_status=publish --post_title=News --porcelain)
wp post create --post_type=page --post_status=publish --post_title=About
wp option update show_on_front page
wp option update page_on_front "$HOME_ID"
wp option update page_for_posts "$NEWS_ID"
wp post update 1 --post_author=3 --post_title='Autumn catalogue announced' --post_name=autumn-catalogue \
  --post_date='2026-10-01 10:00:00'
wp term create lp_genre 'Literary Fiction' --slug=literary-fiction
BOOK=$(wp post create --post_type=lp_book --post_status=publish --post_title='The Glass Orchard' \
  --post_name=the-glass-orchard --porcelain)
wp post term set "$BOOK" lp_genre literary-fiction
wp post create --post_type=attachment --post_mime_type=image/jpeg --post_status=inherit \
  --post_parent="$BOOK" --post_title='Glass Orchard cover' --post_name=glass-orchard-cover
wp option update wp_attachment_pages_enabled 1
wp rewrite flush
EOF

Then each theme is activated in turn and one request of every type is sent from the WP-CLI container:

for theme in lumen lumen-classic; do
  docker compose exec -T cli wp theme activate "$theme"
  docker compose exec -T cli sh -c 'for path in / /news/ /2026/10/autumn-catalogue/ /about/ /books/ \
      /books/the-glass-orchard/ /genre/literary-fiction/ /author/tom-author/ /2026/10/ "/?s=orchard" \
      /no-such-page/ /books/the-glass-orchard/glass-orchard-cover/ /books/the-glass-orchard/embed/; do
    curl -s -o /dev/null -w "%{http_code} $path\n" "http://lumenpress.test$path"
  done'
done
docker compose exec -T cli cat wp-content/uploads/lumen-template-debug.log

Every request returned 200 except /no-such-page/ (404). The log for the block theme lumen, which ships index, home, single, page, archive, search, 404, single-lp_book, archive-lp_book and taxonomy-lp_genre templates and no front-page:

lumen /
  frontpage     front-page.php
  page          page-home.php page-5.php page.php
  => wp-includes/template-canvas.php
  => block template lumen//page (source: theme)
lumen /news/
  home          home.php index.php
  => wp-includes/template-canvas.php
  => block template lumen//home (source: theme)
lumen /2026/10/autumn-catalogue/
  single        single-post-autumn-catalogue.php single-post.php single.php
  => wp-includes/template-canvas.php
  => block template lumen//single (source: theme)
lumen /about/
  page          page-about.php page-7.php page.php
  => wp-includes/template-canvas.php
  => block template lumen//page (source: theme)
lumen /books/
  archive       archive-lp_book.php archive.php
  => wp-includes/template-canvas.php
  => block template lumen//archive-lp_book (source: theme)
lumen /books/the-glass-orchard/
  single        single-lp_book-the-glass-orchard.php single-lp_book.php single.php
  => wp-includes/template-canvas.php
  => block template lumen//single-lp_book (source: theme)
lumen /genre/literary-fiction/
  taxonomy      taxonomy-lp_genre-literary-fiction.php taxonomy-lp_genre-2.php taxonomy-lp_genre.php taxonomy.php
  => wp-includes/template-canvas.php
  => block template lumen//taxonomy-lp_genre (source: theme)
lumen /author/tom-author/
  author        author-tom-author.php author-3.php author.php
  archive       archive.php
  => wp-includes/template-canvas.php
  => block template lumen//archive (source: theme)
lumen /2026/10/
  date          date.php
  archive       archive.php
  => wp-includes/template-canvas.php
  => block template lumen//archive (source: theme)
lumen /?s=orchard
  search        search.php
  => wp-includes/template-canvas.php
  => block template lumen//search (source: theme)
lumen /no-such-page/
  404           404.php
  => wp-includes/template-canvas.php
  => block template lumen//404 (source: theme)
lumen /books/the-glass-orchard/glass-orchard-cover/
  attachment    image-jpeg.php jpeg.php image.php attachment.php
  single        single-attachment-glass-orchard-cover.php single-attachment.php single.php
  => wp-includes/template-canvas.php
  => block template lumen//single (source: theme)
lumen /books/the-glass-orchard/embed/
  embed         embed-lp_book.php embed.php
  => wp-includes/theme-compat/embed.php

The classic theme lumen-classic ships the same names as PHP files. Its candidate lists were identical, line for line; only the result differs, a theme file instead of the canvas (embed again used the core template). The first two entries:

lumen-classic /
  frontpage     front-page.php
  page          page-home.php page-5.php page.php
  => wp-content/themes/lumen-classic/page.php
lumen-classic /books/the-glass-orchard/
  single        single-lp_book-the-glass-orchard.php single-lp_book.php single.php
  => wp-content/themes/lumen-classic/single-lp_book.php

With archive removed from both themes, a date archive walks all the way to index:

lumen-classic /2026/10/
  date          date.php
  archive       archive.php
  index         index.php
  => wp-content/themes/lumen-classic/index.php
lumen /2026/10/
  date          date.php
  archive       archive.php
  index         index.php
  => wp-includes/template-canvas.php
  => block template lumen//index (source: theme)

On Twenty Twenty-Five, assigning the theme’s "Page No Title" template to About (wp post meta set 7 _wp_page_template page-no-title) put the slug at the top of the page list: page page-no-title page-about.php page-7.php page.php, resolved to twentytwentyfive//page-no-title.

docker compose down -v removes the site. For editing a resolved template from the front end, the admin bar’s "Edit template" link (block themes) opens the template held in $_wp_current_template_id; the Query Monitor plugin shows a similar candidate list in its "Template" panel.

What older books get wrong or leave out

  • The classic-theme-only book on professional WordPress development presents the hierarchy as a list of PHP files and says nothing of block templates; the same order applies to templates/*.html, with user-edited and plugin-registered templates in front of or behind the theme’s files as described above. Its dating of full site editing to WordPress 5.4 to 5.6 is also wrong: block themes and the Site Editor shipped in 5.9.

  • The All-in-One For Dummies and Ultimate WordPress Handbook books do not cover the template hierarchy at all; site owners meet it as the Site Editor’s template list (see Site Editor and Global Styles).

  • Older tutorials still list paged.php (dropped in 4.7) and comments-popup.php, omit singular (4.3), single-{post_type}-{slug} (4.4), the decoded-slug variants and Template Post Type (4.7), the privacy policy template (5.2) and taxonomy-{taxonomy}-{term_id} (6.9), and show attachment pages as always available (new sites have them off since 6.4).

  • Some tutorials switch templates in template_redirect with include and exit; the supported way is template_include or the hierarchy filter. Plugins also no longer need to build WP_Block_Template objects by hand in get_block_templates filters: register_block_template() (6.7) does it.

Common errors and pitfalls

Symptom Cause and fix

A new single-lp_book.html is ignored

A customized single-lp_book exists in the database (the Site Editor’s version always wins), or the file is in the theme root instead of templates/. Reset the template in the Site Editor or delete the wp_template post.

A block theme’s PHP file is ignored

A block template of equal or higher specificity exists, including one registered by a plugin.

The front page uses page, not home

"A static page" is selected in Settings > Reading; ship front-page for a dedicated design.

/books/ uses index or 404s

The post type lacks has_archive, or rewrite rules were not flushed after registering it (Settings > Permalinks > Save, or wp rewrite flush).

taxonomy-lp_genre.html not used for a genre

The URL is a category or tag (those use category/tag), or the term slug differs from the file name; check the logged candidates.

Attachment URL redirects to the image

Attachment pages are disabled (wp_attachment_pages_enabled is 0 on sites installed with 6.4+).

embed.html has no effect

Embeds are PHP only; add embed-lp_book.php or embed.php to the theme root.

A hook named front_page_template_hierarchy never fires

The type is sanitized: the hooks are frontpage_… and privacypolicy_….

A template_include callback yields a blank page

It returned a path that does not exist or does not end in .php/.html; 7.1 includes nothing in that case.

A child theme’s template is not used

Child files win only by name; a more specific parent file (for example single-lp_book in the parent and only single in the child) still wins, because specificity is checked before theme.