Skip to content
System nominalUTC --:--:--Articles 041
MODULE 02 Lesson 05 / 06 30 MIN

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.

SurfaceEditsPortable 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.

Lab 30 minutes

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.