Templates and patterns
Template Parts and the Site Editor in Block Themes
How a template references a part, how theme.json declares its area, where the Site Editor stores your edits, and how to get back to the theme's file.
You edit parts/header.html, deploy, reload, and the header on the site is
unchanged. You open the Site Editor and the header you just wrote is not in the list
either. Or you add parts/sidebar.html, and the part works on the front end
while the editor files it under General with no title.
Each of those is the same misunderstanding seen from a different angle. A template part is two things that only look like one. There is the content, which is a file of block markup, and there is the record of that part in the editor, which is a row in the database and a block inside a template. Get the two confused and the symptoms above stop being mysterious.
A template references a slug, not a file
What makes a part appear on a page is one block in a template:
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
The block stores the slug. WordPress resolves that slug to
parts/header.html, or to a database row of the wp_template_part
post type that claims the same slug, and renders the winner through the block renderer.
The template therefore has no idea whether the header comes from a file or from the
editor, which is exactly why the next section matters.
Because the reference is a slug, renaming a file without updating the templates that
point at it leaves those templates referring to a part that no longer resolves. Nothing
warns. The block renders as an empty wp-block-template-part wrapper and the
header is simply absent from the page.
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:query {"query":{"perPage":10,"postType":"post"}} /-->
</main>
<!-- /wp:group -->
The part's title and area are declared in theme.json
A file-based part gets its metadata from the theme.json declaration, not from the
heart of the file. The relevant key is templateParts, and each entry takes
exactly three fields: name, which must equal the filename without its
extension, title, which is the label the editor shows, and
area.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"templateParts": [
{ "name": "header", "title": "Site header", "area": "header" },
{ "name": "footer", "title": "Site footer", "area": "footer" }
]
}
Leave the entry out and the part still resolves and still renders, because resolution
only needs the file. What you lose is the label and the area. The file
parts/sidebar.html with no declaration appears in the editor as
sidebar with the area uncategorized, which is why the list grows
a pile of untitled parts that all land under General.
The area is not decoration. It is a claim about what the part is for, and the editor
uses it to sort parts into contexts. Dressing a footer as an header area does
not change how it renders; it changes which slot the editor offers it for, and the next
person to open the theme will believe the label.
Four area values exist, and a fifth is silently downgraded
WordPress 7.0.7 accepts four area values: uncategorized,
header, footer and navigation-overlay. The list is
returned by get_allowed_block_template_part_areas(), and you can extend it
with the default_template_part_areas filter.
An area value outside that list is not an error you will notice. Every area passes
through _filter_block_template_part_area(), which falls back to
uncategorized and raises a warning through
wp_trigger_error(). On a production site those warnings are suppressed, so
the visible result is a part that quietly loses the area you gave it. The most common
version of this: writing "area": "sidebar", which has a familiar-looking
constant and is not on the list.
Constants are not the same as the allowed list
WP_TEMPLATE_PART_AREA_SIDEBAR is defined in core, which makes
sidebar look supported. It is not one of the four areas
get_allowed_block_template_part_areas() returns. Check the list before you
use a value, not the constant.
The Site Editor writes to the database, not to your files
Open the Site Editor, click into the header, change a colour, save, and nothing has
happened to parts/header.html. The save created or updated a
wp_template_part post whose post_name is header,
carrying the full block markup you just edited. If the theme had a part with that slug,
the record is tagged with the wp_theme taxonomy for the active theme and
becomes the winner.
The resolver reports this clearly once you know what to look for:
header source=theme has_theme_file=true wp_id=NULL
header source=custom has_theme_file=true wp_id=8
The first line is a theme with a file and no override. The second is the same theme
after one save in the editor: the file still exists, has_theme_file is still
true, and the row that renders comes from the database. That is why your edit to
parts/header.html produced nothing. You were editing the fallback.
The same layering applies to whole templates, which is the point from the previous lesson, and it applies to the styles layer as well. The Site Editor is a client of the REST API, and every save there is a post, not a file write. There is no file system path from the editor to your theme directory at all.
Which is why a theme update cannot overwrite a user's edits
This is the part that reads as a bug and is actually a feature. You ship a new header, the site owner sees the old one, and updating the theme changes nothing. Their edit was never in the file you replaced, so replacing the file is not something their edit can notice.
The consequences run in both directions. Good: deploying a theme does not destroy customizations, and a user who has spent an afternoon on a header keeps it. Bad: you cannot ship a fix to a customized part, and support conversations that start with "I updated the theme and the header is still wrong" end at the same place every time. The file is fine. The row is in charge.
There is a second, quieter version of the problem. Because the override is keyed by slug, renaming a part is a breaking change. A new slug has no override, so it renders from the file, while the old override sits in the database attached to a name no template uses any more. The user sees a header that ignores the settings they applied, and the fix is to delete the orphan rather than to keep chasing the file.
Reset means delete the override, not restore the file
The Site Editor's answer is a menu item: selecting a template part and choosing Reset
template part deletes the database record. Nothing is copied back into it. The file was
there the whole time, untouched, and deleting the row is what makes it the source again.
Over REST, that is a DELETE against
/wp/v2/template-parts/{theme}//{slug}, which refuses to act on a part whose
source is already theme and returns
Templates based on theme files can't be removed.
<?php
// Reset one template part. This deletes the override; the theme file is untouched.
$response = wp_remote_request(
rest_url( 'wp/v2/template-parts/wpsifero-base//header?force=true' ),
array(
'method' => 'DELETE',
'headers' => array(
'X-WP-Nonce' => wp_create_nonce( 'wp_rest' ),
),
)
);
$code = wp_remote_retrieve_response_code( $response );
printf( "reset returned %d\n", (int) $code );
Run against a theme part with no override, that request answers 400 with
rest_invalid_template. Run against a customized one, it answers 200
and the next page load reads the file.
Two details about the reset. It is per record, so a site with a customized header, a customized footer and a customized single template needs three resets, and there is no bulk version in core. And it is destructive in the sense that the user's markup is gone once the row is deleted, so take an export of the template before you reset anything on a site you did not build.
The code editor is a different surface with a different contract
The Site Editor has a code editor view, reachable from the options menu, which shows the block markup of the template you are on. It is worth being precise about what it does and does not change, because it is easy to assume it edits files.
| Surface | Edits | Portable to another site |
|---|---|---|
| Site Editor, visual | The canvas. Saves a database record for the template or part. | No. Export the template and import it. |
| Site Editor, code editor | The same block markup, by hand. Same database record. | No. Same record, same export path. |
templates/ and parts/ files |
The theme's own markup, in your editor of choice. | Yes. These are the files you commit. |
| Site Editor export | Produces a theme zip with the database templates as files. | Yes, and it is the bridge between the two columns. |
The code editor is a convenience for the person on the site, not a deploy path. If you want a change to reach every installation of the theme, it has to end up in the theme directory, which means editing the file and, on sites that already have the override, resetting the record.
Create parts/header.html with a group containing your site title and a
navigation block, and parts/footer.html with a paragraph of credits.
Reference both from templates/index.html.
Load the front end and confirm both render. Then declare the header in
theme.json with "area": "header", leave the footer
undeclared, and open the Site Editor's template parts list. Write down the title and
area that each part shows.
Now edit the header in the visual editor: add a paragraph inside it and save. Load
the front end, then change the paragraph in parts/header.html to
something different and load again. Record which text appears.
Query the resolver for the header's source, has_theme_file
and wp_id. Then reset the part from the editor and query again. Finally,
answer: why did the footer never have a
wp_id in the first place?
Show the solution and the reasoning
Both parts render, and they are labelled differently in the editor. The header shows
the title from theme.json and the area header. The footer,
with no declaration, shows its slug as the title and the area
uncategorized. Nothing is wrong with the footer; it is missing metadata,
and metadata lives in JSON.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"templateParts": [
{ "name": "header", "title": "Site header", "area": "header" },
{ "name": "footer", "title": "Site footer", "area": "footer" }
]
}
After the visual edit, the editor's paragraph wins and your file edit is invisible. The first resolver query explains it:
header source=custom has_theme_file=true wp_id=8
source=custom means a database record exists, so the file is the
fallback. has_theme_file=true is the more useful half of the message: it
confirms WordPress can still see parts/header.html, and it is simply not
the winner. A theme that had deleted the file would report
has_theme_file=false, and the two states need different fixes.
After the reset, the same query returns:
header source=theme has_theme_file=true wp_id=NULL
No row, no id, and the file's paragraph is on the page. The reset deleted the record rather than writing anything into the theme, which is why the result was immediate and why no cache needed clearing.
The footer never had a wp_id because nobody had edited it in the editor.
A wp_id is evidence of a customization, not of a part existing. That
distinction is the fastest way to triage a report about a part that "will not update":
if wp_id is NULL, the file is already in charge and the problem
is elsewhere, most likely a template still referencing an old slug.
One trade-off is worth naming, because the obvious fix is worse than the symptom. You could rename the part's slug in every template to escape an unwanted override, and the part would render from the file again. You would also orphan the user's record under a name nothing references, break any child theme that references the old slug, and leave two headers in the editor list. Resetting the record is the smaller change.
Progress
Mark this lesson done and it stays marked in this browser.