Templates and Template Parts
|
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. |
A block theme has no PHP template files. Every screen of the site is described by an HTML file of block markup:
the theme’s templates/ folder holds one file per kind of page (the blog index, a single post, a page, a book
archive, the 404 page), and parts/ holds the pieces they share, such as the header and the footer. WordPress picks a
template for the request, renders its blocks on the server and, when the site owner edits the template in the Site
Editor, saves a copy in the database that takes precedence over the file. This page is for theme developers who write
those files and for plugin developers who ship templates for their own post types. It builds the templates and parts
of lumen, the block theme of the fictitious publisher Lumen Press, and the templates that its lumen-catalog plugin
registers for the lp_book post type.
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. The Gutenberg plugin (24.2.0) previews editor changes before they reach core; nothing here requires it. WordPress 7.2 is in beta and due in December 2026; its betas do not change the file format or the storage described here. Every listing and response below comes from a 7.1.3 site running the files on this page.
How WordPress chooses which template renders a request (the template hierarchy) is covered in Template Hierarchy; this page only names the files. The site owner’s side of editing templates is in Site Editor and Global Styles.
What templates and template parts are
Block themes arrived in WordPress 5.9 (January 2022) together with the Site Editor. A theme becomes a block theme when
it contains templates/index.html; that single file is the minimum template, and it is how WordPress recognises the
theme type and enables Appearance > Editor. Some older books place "full site editing" in 5.4 to 5.6; those releases
only had Gutenberg plugin experiments. Other older books present templates only as PHP files with template tags, or
describe the Site Editor only from the site owner’s point of view and never show the files behind it; both views still
exist, but a block theme’s templates are the files described here.
| Location | Contents | Notes |
|---|---|---|
|
Templates: complete page layouts, one per hierarchy slot ( |
|
|
Template parts: reusable areas ( |
Inserted with the Template Part block. Keep them directly in |
|
|
Titles, areas and allowed post types. Version 3 since WordPress 6.6. |
|
Patterns used inside templates |
The place for translatable text and PHP logic, since |
|
Legacy folder names from the Gutenberg plugin era |
Still read: if either folder exists, WordPress uses both legacy names instead of |
Templates and parts are plain HTML files, not PHP. Anything that must be computed (the current year, a translated
string, a list of posts) comes from a dynamic block that renders on the server, or from a pattern referenced with
<!-- wp:pattern {"slug":"lumen/..."} /-->, whose PHP runs when the template is rendered.
The complete theme used on this page has this layout:
wp-content/themes/lumen/
├── style.css
├── theme.json
├── functions.php (optional: only for the custom area example below)
├── parts/
│ ├── header.html
│ └── footer.html
└── templates/
├── index.html
├── single.html
├── page.html
├── page-wide.html (custom template)
├── archive-lp_book.html
├── single-lp_book.html
├── book-landing.html (custom template)
├── 404.html
└── search.html
Source: Theme Handbook: Theme Structure
style.css (name, Version: 0.1.0, Requires at least: 6.9, Requires PHP: 7.4, Text Domain: lumen) and the
colour palette and layout sizes in theme.json are the ones shown in
Themes Overview; this page adds only the template keys of theme.json.
Serialized block markup and its validity rules
A template is a sequence of blocks serialized as HTML with comment delimiters, the same format the block editor stores
in post_content:
-
A block starts with
<!-- wp:name {attributes} -->and ends with<!-- /wp:name -->. Core blocks omit thecore/namespace (wp:group), other blocks keep it (wp:lumen/book-card). -
Attributes are a JSON object in the opening comment. It must be valid JSON: double-quoted keys and strings, no trailing commas, no comments.
-
A block with no inner HTML is self-closing:
<!-- wp:post-title {"level":1} /-->. Dynamic blocks (Site Title, Post Title, Query Loop children, Navigation, Template Part) are written this way because their HTML is produced by PHP at render time. -
A static block (Group, Columns, Heading, Paragraph) stores its HTML between the delimiters, and that HTML must be exactly what the block’s
save()function produces for those attributes. A Group with"align":"full"must have thealignfullclass on its<div>; a Column with"width":"33.33%"must carrystyle="flex-basis:33.33%"; a Heading must haveclass="wp-block-heading". -
Whitespace and indentation between blocks are ignored, so template files can be indented for readability.
The front end does not validate anything: do_blocks() renders static blocks' stored HTML as written. The editor does.
When the Site Editor loads a template whose static HTML differs from what save() would produce, it marks that block
as invalid and offers to attempt recovery or convert it to HTML. A template that looks right on the site can therefore
be broken for the site owner. The reliable way to produce valid markup is to build the layout in the Site Editor and
copy it from the Code editor (More options > Code editor), or export it (see
Exporting edits back to files), then tidy the indentation.
Rules that keep templates portable:
-
No hard-coded IDs. A Navigation block with
{"ref":9}, an Image block with{"id":42}or a Query LooptaxQuerywith term IDs refers to rows of one database. A theme ships a Navigation block withoutref(it shows a fallback menu) and uses slugs or inherited queries instead of IDs. -
No
themeattribute on Template Part blocks. WordPress adds the active theme when it renders the block; a hard-coded"theme":"lumen"breaks the part in a child theme or a renamed copy. -
No user-facing text that needs translation in
.htmlfiles: it cannot pass through__(). Put it in a pattern and reference the pattern, or keep it as editable placeholder content as the examples below do. -
No PHP, no
<script>. The file is never executed, and scripts belong in blocks or enqueued assets. -
The Post Content block in every single and page template. Without
<!-- wp:post-content /-->the post’s own content is not shown.
Template parts
A template part is an HTML file in parts/ inserted with the Template Part block. The lumen header holds the site
title and a Navigation block; the footer holds a line of text and the tagline:
<!-- parts/header.html -->
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">
<!-- wp:group {"align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} -->
<div class="wp-block-group alignwide">
<!-- wp:site-title {"level":0} /-->
<!-- wp:navigation {"overlayMenu":"mobile","layout":{"type":"flex","justifyContent":"right"}} /-->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->
Source: Theme Handbook: Template Parts
<!-- parts/footer.html -->
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">
<!-- wp:paragraph -->
<p>Lumen Press, independent publishers.</p>
<!-- /wp:paragraph -->
<!-- wp:site-tagline /-->
</div>
<!-- /wp:group -->
Source: Theme Handbook: Template Parts
The leading <!-- parts/... --> comment is only a label on this page; the files start with the first block. A
template inserts a part by slug with <!-- wp:template-part {"slug":"header","tagName":"header"} /-->.
The Template Part block’s attributes are slug (the file name without .html), tagName (header, footer,
main, section, aside, article or div), area and theme. Without tagName, the element comes from the
part’s area (area_tag below).
Areas and the templateParts key
Each part belongs to an area, which tells the editor what the part is for: the Site Editor groups parts by area,
offers parts of the same area when the site owner replaces one, and uses block variations named after the area
("Header", "Footer"). The area is declared in theme.json:
{
"$schema": "https://schemas.wp.org/wp/7.1/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"color": {
"palette": [
{ "slug": "paper", "name": "Paper", "color": "#fbf8f1" },
{ "slug": "ink", "name": "Ink", "color": "#1f1d1a" },
{ "slug": "lumen-red", "name": "Lumen red", "color": "#a3271f" }
]
},
"layout": {
"contentSize": "680px",
"wideSize": "1140px"
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--paper)",
"text": "var(--wp--preset--color--ink)"
}
},
"templateParts": [
{ "name": "header", "title": "Header", "area": "header" },
{ "name": "footer", "title": "Footer", "area": "footer" }
],
"customTemplates": [
{ "name": "page-wide", "title": "Page: wide, no title", "postTypes": [ "page" ] },
{ "name": "book-landing", "title": "Book: landing page", "postTypes": [ "lp_book" ] }
]
}
Each templateParts entry has name (the file name without extension), title (shown in the editor and
translatable through the theme’s text domain) and area. WordPress 7.1.3 allows four areas:
area |
Label | Default wrapper (area_tag) |
Use |
|---|---|---|---|
|
Header |
|
Site title, logo, main navigation |
|
Footer |
|
Credits, secondary links |
|
General |
|
Anything else: a sidebar, a "book details" box |
|
Navigation Overlay |
|
The block-based mobile menu overlay of the Navigation block (added in 7.0) |
A part that is not listed in templateParts, or that names an area outside this list, is filed under
uncategorized. With WP_DEBUG on, an unknown area also raises a notice ("is not a supported wp_template_part area
value"). Core defines a WP_TEMPLATE_PART_AREA_SIDEBAR constant, but sidebar is not one of the allowed areas, so
"area": "sidebar" silently becomes General unless a filter adds it.
Adding an area
The default_wp_template_part_areas filter (since 5.9) adds areas. Each needs area, label, description, icon
and area_tag. Lumen adds a Sidebar area for book filters:
<?php
/**
* Lumen theme functions.
*
* @package Lumen
*/
/**
* Adds a "Sidebar" area for template parts.
*
* @param array[] $areas Allowed template part areas.
* @return array[] Areas including the sidebar.
*/
function lumen_template_part_areas( array $areas ) {
$areas[] = array(
'area' => 'sidebar',
'area_tag' => 'aside',
'label' => __( 'Sidebar', 'lumen' ),
'description' => __( 'Secondary content next to the main column, such as book filters.', 'lumen' ),
'icon' => 'sidebar',
);
return $areas;
}
add_filter( 'default_wp_template_part_areas', 'lumen_template_part_areas' );
With { "name": "book-filters", "title": "Book filters", "area": "sidebar" } added to templateParts and a
parts/book-filters.html file, the part is listed with area sidebar, and
get_allowed_block_template_part_areas() returns
{"uncategorized":"div","header":"header","footer":"footer","navigation-overlay":"div","sidebar":"aside"} as its
area tags. The Theme Handbook lists div, article, aside, footer, header, main and section as valid
area_tag values, and only the header, footer and sidebar icons have their own artwork. An area that the
active theme adds disappears with the theme, and parts already saved with it fall back to General; an area meant to
outlive the theme belongs in a plugin.
The Lumen templates
Each template below starts with the header part, wraps the page body in a Group rendered as <main>, and ends with the
footer part. Which file serves which request is the template hierarchy’s job; in short, index.html is the final
fallback, single.html and page.html serve posts and pages, archive-lp_book.html and single-lp_book.html are
the post-type-specific versions for books (the lp_book type registers has_archive, so /books/ is its archive),
search.html serves ?s= requests and 404.html serves requests that match nothing.
index.html: the fallback and the blog
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"queryId":1,"query":{"inherit":true,"perPage":10}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-date /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
<!-- wp:query-no-results -->
<!-- wp:paragraph -->
<p>Nothing has been published yet.</p>
<!-- /wp:paragraph -->
<!-- /wp:query-no-results -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
Source: Theme Handbook: Templates
single.html and page.html
<!-- templates/single.html -->
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-title {"level":1} /-->
<!-- wp:group {"layout":{"type":"flex"}} -->
<div class="wp-block-group">
<!-- wp:post-date /-->
<!-- wp:post-author-name /-->
<!-- wp:post-terms {"term":"category"} /-->
</div>
<!-- /wp:group -->
<!-- wp:post-featured-image /-->
<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
<!-- wp:post-navigation-link {"type":"previous"} /-->
<!-- wp:post-navigation-link /-->
<!-- wp:comments -->
<div class="wp-block-comments">
<!-- wp:comments-title /-->
<!-- wp:comment-template -->
<!-- wp:comment-author-name /-->
<!-- wp:comment-content /-->
<!-- /wp:comment-template -->
<!-- wp:post-comments-form /-->
</div>
<!-- /wp:comments -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
Source: Theme Handbook: Templates
page.html is the same layout without the date, author, terms, post navigation and comments: header part, Post
Title, Post Featured Image, Post Content and footer part.
archive-lp_book.html and single-lp_book.html
The catalogue archive shows books in a three-column grid with cover, title and genre; the single book view puts the cover beside the title, series, genre and description, and lists three more books:
<!-- templates/archive-lp_book.html -->
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query-title {"type":"archive","showPrefix":false} /-->
<!-- wp:query {"queryId":2,"query":{"inherit":true,"perPage":12},"align":"wide"} -->
<div class="wp-block-query alignwide">
<!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
<!-- wp:post-featured-image {"isLink":true,"aspectRatio":"2/3"} /-->
<!-- wp:post-title {"level":2,"isLink":true} /-->
<!-- wp:post-terms {"term":"lp_genre"} /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
<!-- wp:query-no-results -->
<!-- wp:paragraph -->
<p>The catalogue is empty.</p>
<!-- /wp:paragraph -->
<!-- /wp:query-no-results -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
Source: Theme Handbook: Templates
<!-- templates/single-lp_book.html -->
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:columns {"align":"wide"} -->
<div class="wp-block-columns alignwide">
<!-- wp:column {"width":"33.33%"} -->
<div class="wp-block-column" style="flex-basis:33.33%">
<!-- wp:post-featured-image {"aspectRatio":"2/3"} /-->
</div>
<!-- /wp:column -->
<!-- wp:column {"width":"66.66%"} -->
<div class="wp-block-column" style="flex-basis:66.66%">
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-terms {"term":"lp_series","prefix":"Series: "} /-->
<!-- wp:post-terms {"term":"lp_genre"} /-->
<!-- wp:post-content /-->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
<!-- wp:heading -->
<h2 class="wp-block-heading">More from the catalogue</h2>
<!-- /wp:heading -->
<!-- wp:query {"queryId":3,"query":{"postType":"lp_book","perPage":3,"inherit":false}} -->
<div class="wp-block-query">
<!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
<!-- wp:post-title {"level":3,"isLink":true} /-->
<!-- /wp:post-template -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
Source: Theme Handbook: Templates
The ISBN, page count and publication date are post meta; showing them in a template is the job of block bindings
(Block Bindings API) or of the lumen/book-card block, not of plain
template markup.
404.html and search.html
404.html replaces the Query Loop with a level-1 Heading ("Page not found"), a Paragraph suggesting a search, and a
Search block (<!-- wp:search {"label":"Search","buttonText":"Search"} /-->). WordPress still sends the 404 status
code; the template only controls the body.
search.html is index.html with two additions above the Query Loop: a Query Title block with
{"type":"search"} ("Search results for: …") and a Search block, so that visitors can refine the query; its
No Results block says that nothing matched.
The front-end search covers every public post type, so books appear in the results; the lp_book type would be
excluded only if it set exclude_from_search. All the template and part files on this page parse as valid blocks with
the core block library, which is the same check the Site Editor performs when it opens them.
The Query Loop and post blocks
The Query Loop block (core/query) is how a template lists posts. Its children repeat or wrap the list: Post Template
(core/post-template) repeats its inner blocks once per post, Pagination and its Previous, Numbers and Next blocks
page through the results, and No Results renders only when the query is empty. Query Title shows "Books", "Search
results for: …" or an author name, depending on its type.
| Attribute | Meaning |
|---|---|
|
Distinguishes several loops on one page; each non-inherited loop paginates with its own |
|
|
|
Post type, items per page, items to skip, maximum pages (0 = no limit) |
|
|
|
Filters. |
|
Wrapper element ( |
|
Client-side pagination without a full page reload (Interactivity API, 6.4) |
|
Marks a block variation that a plugin built on the Query Loop |
inherit: true is what makes one template serve many URLs: archive-lp_book.html lists books at /books/,
index.html lists posts on the blog, and search.html lists results, without any of them naming a post type. With an
inherited query the perPage attribute is not used; the page size comes from Settings > Reading ("Blog pages show at
most", the posts_per_page option), and changing it changes the archive. To change the main query (for example, 12
books per page on /books/ only), hook pre_get_posts in a plugin rather than switching the template to a custom
query, which would run a second query and break pagination. inherit: false, as in the "More from the catalogue"
loop, is for secondary lists.
The post blocks render one field of the current post, inside a Post Template or in a single template:
| Block | Renders |
|---|---|
|
The title, optionally as a link ( |
|
The post’s own blocks; required in single, page and custom templates |
|
The excerpt or a trimmed version of the content, with an optional "Read more" link |
|
The featured image ( |
|
Publication or modified date, author name or author box |
|
The post’s terms in the taxonomy named by |
|
Link to the previous or next post |
|
A link to the post, for use inside Post Template |
|
The comment list and form |
A Post Terms block for a taxonomy that the post type does not use renders nothing; it does not fail. A block that
depends on a post type supports value (featured image, excerpt, comments) also renders nothing when the type lacks
that support, which is why lp_book registers excerpt and thumbnail.
Custom templates
A custom template is a template that is not part of the hierarchy and that an editor chooses per post in the
Template panel of the post editor. Older books call them "page templates" and create them with a Template Name:
comment in a PHP file; block themes list them in customTemplates in theme.json (shown above), with the file in
templates/. Each entry has name (file name without .html), title and optionally postTypes, which defaults to
page only. Lumen ships two:
page-wide.html is page.html without the Post Title, with the main Group and the Post Content set to
"align":"full" and the default (unconstrained) layout. The book template:
<!-- templates/book-landing.html: a book's campaign page -->
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","align":"full","layout":{"type":"constrained"}} -->
<main class="wp-block-group alignfull">
<!-- wp:post-featured-image {"align":"wide","aspectRatio":"16/9"} /-->
<!-- wp:post-title {"level":1,"textAlign":"center"} /-->
<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
Source: Theme Handbook: Custom Templates
The choice is stored in the post’s _wp_page_template meta as the template slug (page-wide), the same meta that
classic page templates use, and wp post update <id> --page_template=page-wide sets it from WP-CLI. A custom template
whose name matches a hierarchy file (single, archive) would be confused with it, and a custom template missing from
customTemplates still works but shows its slug as title and is offered for pages only.
Templates from plugins: register_block_template()
A plugin that adds a post type usually wants it to look right in any block theme, not only in one built for it. Before
WordPress 6.7 that meant filtering get_block_templates and pre_get_block_file_template. Since 6.7,
register_block_template() registers a template with a name of the form plugin-slug//template-slug, and
unregister_block_template() removes it. The lumen-catalog plugin registers a single book template, used when the
active theme has none, and a custom "printable sheet" template offered for books:
<?php
/**
* Plugin Name: Lumen Catalog
* Description: Book catalogue of Lumen Press: the lp_book post type, its taxonomies, meta and templates.
* Version: 0.1.0
* Requires at least: 6.9
* Requires PHP: 7.4
* License: GPL-2.0-or-later
* Text Domain: lumen-catalog
*
* @package LumenCatalog
*/
defined( 'ABSPATH' ) || exit;
// lp_book, lp_genre and lp_series are registered as shown in Custom Post Types and Taxonomies;
// lp_book uses 'has_archive' => true, 'rewrite' => array( 'slug' => 'books' ) and supports
// title, editor, excerpt, thumbnail and custom-fields.
/**
* Registers the plugin's block templates (WordPress 6.7+).
*/
function lumen_catalog_register_templates() {
if ( ! function_exists( 'register_block_template' ) ) {
return;
}
register_block_template(
'lumen-catalog//single-lp_book',
array(
'title' => __( 'Single book', 'lumen-catalog' ),
'description' => __( 'Displays one book of the catalogue.', 'lumen-catalog' ),
'content' => lumen_catalog_template_content( 'single-lp_book' ),
)
);
register_block_template(
'lumen-catalog//book-print',
array(
'title' => __( 'Book: printable sheet', 'lumen-catalog' ),
'description' => __( 'A sheet without header and footer, for press kits.', 'lumen-catalog' ),
'content' => lumen_catalog_template_content( 'book-print' ),
'post_types' => array( 'lp_book' ),
)
);
}
add_action( 'init', 'lumen_catalog_register_templates' );
/**
* Reads a template's block markup from the plugin's templates directory.
*
* @param string $slug Template slug, without extension.
* @return string Block markup, or an empty string if the file is missing.
*/
function lumen_catalog_template_content( $slug ) {
$file = __DIR__ . '/templates/' . sanitize_file_name( $slug ) . '.html';
if ( ! is_readable( $file ) ) {
return '';
}
return (string) file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
}
The markup lives in wp-content/plugins/lumen-catalog/templates/, so it can be edited and validated like a theme
file. Its single-lp_book.html is a plain version of the theme’s (header part, Post Title, Post Featured Image, Post
Terms for lp_genre, Post Content, footer part); book-print.html drops the header and footer:
<!-- templates/book-print.html (plugin) -->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-terms {"term":"lp_series","prefix":"Series: "} /-->
<!-- wp:post-excerpt /-->
<!-- wp:post-content /-->
</main>
<!-- /wp:group -->
How the registry behaves, as checked on 7.1.3:
-
$argstakestitle,description,content,post_typesandplugin. A template whose slug is a hierarchy name (single-lp_book) joins the hierarchy; one withpost_typesbecomes a custom template offered for those types. -
The name must be lowercase and contain the
//separator; otherwise the function returns aWP_Error(template_name_no_uppercase,template_no_prefix,template_already_registered) and triggers_doing_it_wrong(). -
The theme wins. When the active theme has a file with the same slug, the theme file is used; the template only records which plugin also provides it. With
lumenactive,single-lp_bookis served from the theme; with Twenty Twenty-Five, which has no book template, the plugin’s version is served instead of the theme’s genericsingle.html. -
Only templates can be registered, not template parts. The plugin template above uses the theme’s
headerandfooterparts by slug, which every well-built block theme provides. -
Plugin templates appear in the Site Editor like theme templates and can be customized there; the customization is stored as described in the next section.
Activating Lumen and checking the output
The examples run on the Compose stack of Local Development
Environments (WordPress 7.1.3 with Apache on port 8080, MariaDB 11.8 and a wordpress:cli-2.12.0-php8.4 service
named cli), with the theme and the plugin bind-mounted into both the wordpress and the cli services:
volumes:
- wp:/var/www/html
- ./wp-content/themes/lumen:/var/www/html/wp-content/themes/lumen
- ./wp-content/plugins/lumen-catalog:/var/www/html/wp-content/plugins/lumen-catalog
export WP_DB_PASSWORD='<a-local-db-password>' WP_ADMIN_PASSWORD='<a-local-admin-password>'
docker compose up -d
docker compose exec cli wp core install --url=http://lumenpress.test:8080 --title="Lumen Press" \
--admin_user=admin --admin_email=admin@lumenpress.test --admin_password="$WP_ADMIN_PASSWORD" --skip-email
docker compose exec cli sh -c '
wp plugin activate lumen-catalog && wp theme activate lumen
wp rewrite structure "/%postname%/" && wp option update blogdescription "Independent publishers since 2009"
wp term create lp_genre "Science Fiction" --slug=science-fiction
wp term create lp_series "The Tidewater Cycle" --slug=tidewater
for t in "Salt and Signal" "The Lantern Keeper" "Northern Glass"; do
id=$(wp post create --post_type=lp_book --post_status=publish --post_title="$t" --porcelain)
wp post term set "$id" lp_genre science-fiction && wp post term set "$id" lp_series tidewater
done
wp post create --post_type=page --post_status=publish --post_title=About'
Source: WP-CLI: wp theme activate
The resolved templates, their source and the plugin that also provides them, come from get_block_templates():
<?php
// list-templates.php: run with "wp eval-file list-templates.php".
foreach ( array( 'wp_template', 'wp_template_part' ) as $type ) {
foreach ( get_block_templates( array(), $type ) as $t ) {
WP_CLI::log(
sprintf(
'%-17s %-28s %-7s %-14s %s',
$type,
$t->id,
$t->source,
$t->plugin ?? '',
'wp_template_part' === $type ? $t->area : implode( ',', (array) $t->post_types )
)
);
}
}
wp_template lumen//single theme
wp_template lumen//page theme
wp_template lumen//index theme
wp_template lumen//archive-lp_book theme
wp_template lumen//book-landing theme lp_book
wp_template lumen//404 theme
wp_template lumen//page-wide theme page
wp_template lumen//search theme
wp_template lumen//single-lp_book theme lumen-catalog
wp_template lumen//book-print plugin lumen-catalog lp_book
wp_template_part lumen//footer theme footer
wp_template_part lumen//header theme header
Template IDs have the form theme//slug, even for plugin templates: lumen//book-print has source plugin, while
lumen//single-lp_book comes from the theme file and only notes that lumen-catalog registers the same slug. Running
the same script with Twenty Twenty-Five active lists twentytwentyfive//single-lp_book with source plugin.
The rendered pages show which template answered each URL (--resolve maps the host name without editing
/etc/hosts):
for u in / /books/ /books/salt-and-signal/ /about/ /no-such-page/ "/?s=lantern"; do
printf '%s %s\n' "$u" "$(curl -s -o /dev/null -w '%{http_code}' --resolve lumenpress.test:8080:127.0.0.1 \
"http://lumenpress.test:8080$u")"
curl -s --resolve lumenpress.test:8080:127.0.0.1 "http://lumenpress.test:8080$u" \
| grep -oE '<h1[^>]*>[^<]*|More from the catalogue|catalogue is empty|wp-block-search__input' | sort -u
done
/ 200
/books/ 200
<h1 class="wp-block-query-title">Books
/books/salt-and-signal/ 200
<h1 class="wp-block-post-title">Salt and Signal
More from the catalogue
/about/ 200
<h1 class="wp-block-post-title">About
/no-such-page/ 404
<h1 class="wp-block-heading">Page not found
wp-block-search__input
/?s=lantern 200
<h1 class="wp-block-query-title">Search results for: “lantern”
wp-block-search__input
Every page also starts with <header class="wp-block-template-part"> and ends with
<footer class="wp-block-template-part">: the tagName of the Template Part blocks. The blog index has no <h1>
because index.html has no Query Title. Assigning the custom templates changes the output of single URLs:
docker compose exec cli wp post update "$(docker compose exec -T cli wp post list --post_type=page --name=about --field=ID)" --page_template=page-wide
docker compose exec cli wp post update "$(docker compose exec -T cli wp post list --post_type=lp_book --name=the-lantern-keeper --field=ID)" --page_template=book-print
Source: WP-CLI: wp post update
After that, /about/ renders without a title (page-wide), and /books/the-lantern-keeper/ renders the plugin’s
printable sheet: the title and "Series: The Tidewater Cycle" with no <header> element, because book-print has no
header part.
How edited templates are stored
The Site Editor never writes to the theme folder. When the site owner saves a template or a part, WordPress creates a post with the template’s markup, and from then on that post wins over the file:
| Item | Storage |
|---|---|
Edited or new template |
A |
Edited or new template part |
A |
Theme binding |
A |
Origin |
An |
History |
Both post types support revisions; the Site Editor shows them under Revisions. |
Permissions |
Creating, editing and deleting requires |
Source: REST API Handbook: Templates
The REST API describes the result in each template’s fields: source is theme, plugin or custom (edited),
origin says where a customized template came from, wp_id is the post ID, has_theme_file tells whether a file
of that slug exists (and therefore whether "Reset" is possible, or only "Delete"), and is_custom marks custom
templates. The endpoints are /wp/v2/templates/<theme>//<slug> and /wp/v2/template-parts/<theme>//<slug>.
Seeing an edit in the database
The script below saves the header part and the book template through the same REST endpoints the Site Editor calls,
in process with rest_do_request(), so no browser or application password is needed:
<?php
/**
* Saves the header template part and the book template the way the Site Editor does.
* Run with: wp eval-file edit-templates.php --user=admin
*/
$header = get_block_template( 'lumen//header', 'wp_template_part' );
$content = str_replace(
'<!-- wp:site-title {"level":0} /-->',
"<!-- wp:site-title {\"level\":0} /-->\n\t\t<!-- wp:site-tagline /-->",
$header->content
);
$request = new WP_REST_Request( 'POST', '/wp/v2/template-parts/lumen//header' );
$request->set_body_params( array( 'content' => $content ) );
$saved = rest_do_request( $request )->get_data();
WP_CLI::log( "header: source {$saved['source']}, wp_id {$saved['wp_id']}, area {$saved['area']}" );
$request = new WP_REST_Request( 'POST', '/wp/v2/templates/lumen//single-lp_book' );
$request->set_body_params( array( 'title' => 'Single book (edited)' ) );
$saved = rest_do_request( $request )->get_data();
WP_CLI::log( "single-lp_book: source {$saved['source']}, wp_id {$saved['wp_id']}" );
docker compose exec cli wp eval-file edit-templates.php --user=admin
docker compose exec cli wp post list --post_type=wp_template,wp_template_part --post_status=any \
--fields=ID,post_type,post_name,post_title
docker compose exec cli wp post term list 9 wp_theme wp_template_part_area --fields=taxonomy,slug
docker compose exec cli wp post meta list 10
Source: WP-CLI: wp post list
header: source custom, wp_id 9, area header
single-lp_book: source custom, wp_id 10
ID post_type post_name post_title
9 wp_template_part header Header
10 wp_template single-lp_book Single book (edited)
taxonomy slug
wp_template_part_area header
wp_theme lumen
post_id meta_key meta_value
10 origin theme
The IDs depend on what the site already contains. Saving only the title of single-lp_book copied the whole template
into post_content: from that moment the file is no longer read for that template, even though its markup was not
touched. The rendered pages now show the tagline twice (in the edited header and in the footer).
Theme updates do not reach edited templates
If a new version of the theme changes parts/header.html (for example, adding a paragraph), a site with a customized
header keeps rendering its database copy: the curl output above stays the same and the new paragraph does not appear.
This is intended, since the site owner’s work must survive theme updates, and it is the most common support question
about block themes. The fix is a reset, which deletes the post; the Site Editor’s "Reset" does the equivalent of:
<?php
/**
* Resets the customized header to the theme file, like "Reset" in the Site Editor.
* Run with: wp eval-file reset-header.php --user=admin
*/
$request = new WP_REST_Request( 'DELETE', '/wp/v2/template-parts/lumen//header' );
$request->set_param( 'force', true );
$response = rest_do_request( $request );
WP_CLI::log( 'status ' . $response->get_status() . ', deleted: ' . var_export( $response->get_data()['deleted'], true ) );
WP_CLI::log( 'header source now: ' . get_block_template( 'lumen//header', 'wp_template_part' )->source );
status 200, deleted: true
header source now: theme
After the reset the page renders the updated theme file, including the new paragraph, and the owner’s tagline edit is gone. A theme that changes a part’s structure in an update should say so in its changelog, so that owners with customized copies can reset or merge by hand.
Exporting edits back to files
To move a design from the database into the theme (for a release, or to put it under version control), the Site
Editor’s More options > Tools > Export builds a ZIP of the whole theme with every customized template and part written to
templates/ and parts/; wp_generate_block_templates_export_file() is the function behind it. The Create Block
Theme plugin can save changes directly into the theme folder and then clear the database copies. Both are covered in
Create Block Theme and Version Control. An export
keeps Navigation "ref" IDs, which must be removed before the markup is shipped in a theme.
What older books get wrong or leave out
| Older claim or gap | Current behaviour |
|---|---|
Full site editing arrived in WordPress 5.4 to 5.6 |
Block themes, block templates and the Site Editor arrived in 5.9; earlier releases only had Gutenberg plugin experiments. |
Full site editing is still maturing and best avoided in favour of a Customizer-based theme |
Block themes have been the default since Twenty Twenty-Two; templates, parts, custom templates and plugin templates are stable APIs. |
Templates are PHP files with template tags and The Loop |
That is still true for classic themes (Classic Theme Anatomy and The Loop); block themes use |
Page templates are declared with a |
Block themes declare custom templates in |
A site owner’s template edits change the theme |
Edits are |
Plugins customise a post type’s display with |
With a block theme, a plugin registers block templates with |
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
"Block contains unexpected or invalid content." in the Site Editor, while the front end looks fine |
Hand-written static HTML does not match the block’s |
Header or footer area shows "Template part has been deleted or is unavailable" |
The |
A part is listed under General although |
|
A custom template is not offered for books |
|
The single template shows the title but not the text |
No Post Content block in the template. |
|
The post type lacks |
Archive shows 10 items although the Query Loop says 12 |
The loop inherits the main query; the page size is the Reading setting. Use |
Theme update has no visible effect |
The site has a customized copy of the template or part. Compare in Appearance > Editor (customized items are marked) and reset or merge. |
Translated sites still show English text from a template |
Text in |
|
Uppercase letters, a missing |
Related pages
-
Themes Overview: block, classic and hybrid themes and the
style.cssheader -
Template Hierarchy: which template answers which request
-
Theme Patterns: translatable and computed content inside templates
-
Navigation and Menus: the Navigation block and overlays in the header
-
Child Themes: overriding a parent’s templates
-
Site Editor and Global Styles: editing templates as a site owner
-
Custom Post Types and Taxonomies: the
lp_booktype