A theme whose oldest file dates from 2008, sixty-one template files, and an editorial team that files a ticket to change a phone number in the footer. WordPress 6.2 took the beta label off the site editor in March, which changed nothing technically and changed what could be proposed.
The symptom
$ ls *.php | wc -l
61
$ head -3 header.php
<?php
/**
* @since 1.0 (2008-11-04)
$ grep -rc 'dynamic_sidebar' *.php
footer.php:4
sidebar.php:2
# four widget areas in the footer, configured through
# Appearance → Widgets, which an editor can technically
# reach and has been told not to touch.the tickets, one quarter:
"change the phone number in the footer" 3
"add a link to the footer" 2
"change the copyright year" 1 (annual)
"reorder the footer columns" 1
"change the header CTA text" 4
11 tickets, all of them content, all of them a deploy.Why it happens
Fifteen years of templates, each added for one page, none ever removed. The header and footer are PHP because in 2008 there was no alternative, and everything built since has assumed they are PHP.
The fix
The inventory, measured rather than guessed
// log the resolved template for every front-end request,
// for a month
add_filter( 'template_include', function ( $template ) {
if ( ! is_admin() ) {
turkerdev_log_template( basename( $template ), $_SERVER['REQUEST_URI'] );
}
return $template;
}, PHP_INT_MAX );
one month, 61 templates:
template requests distinct URLs
single.php 182,004 1,412
page.php 88,120 188
index.php 41,208 204
archive.php 12,884 44
single-product.php 11,204 302
page-contact.php 4,102 1
... 14 more with > 100 requests
... 41 with fewer than 10 URLs a month
41 of 61 templates render fewer than 10 URLs a month.
six of them rendered zero.Measuring rather than reading the theme is what made this tractable. Six templates had rendered nothing in a month — they were for post types removed in 2016 — and forty-one more were serving a handful of URLs each, which meant the migration only had to be good on six.
The hybrid step
a theme with templates/index.html is a BLOCK theme, and
the site editor appears. templates that exist as PHP and
not as HTML still resolve through the old hierarchy.
so the migration is per-template:
templates/index.html ← block
templates/single.html ← block
templates/page.html ← block
templates/archive.html ← block
templates/single-product.html ← block
templates/404.html ← block
page-contact.php ← still PHP
archive-event.php ← still PHP
... 39 more, still PHP
and both work, on the same site, at the same time.This is the fact that makes a fifteen-year-old site migratable at all. A block theme is not an all-or-nothing conversion; it is a directory that takes precedence where it has an answer, and the PHP hierarchy handles everything else.
theme.json as the styling boundary
{
"version": 2,
"settings": {
"appearanceTools": true,
"color": {
"custom": false,
"palette": [
{ "slug": "brand", "color": "#1f6feb", "name": "Brand" },
{ "slug": "ink", "color": "#101418", "name": "Ink" }
]
},
"spacing": { "units": ["px", "rem", "%"] },
"layout": { "contentSize": "46rem", "wideSize": "72rem" }
}
}
"custom": false is the setting that makes this a design system rather than a colour picker — an editor may use the brand palette and may not invent a colour. That single line prevented more drift than any documentation would have.
and the stylesheet has to agree, which is the work:
the 2008 stylesheet 2,104 lines
values that are now tokens 188 occurrences
values that stayed hardcoded 17 (documented)
rules made redundant by
theme.json layout 412 lines deletedHeader and footer as template parts
<!-- parts/footer.html -->
<!-- wp:group {"tagName":"footer","layout":{"type":"constrained"}} -->
<footer class="wp-block-group">
<!-- wp:columns -->
<!-- wp:column -->
<!-- wp:navigation {"ref":41} /-->
<!-- /wp:column -->
<!-- wp:column -->
<!-- wp:paragraph --><p>+44 20 7946 0000</p><!-- /wp:paragraph -->
<!-- /wp:column -->
<!-- /wp:columns -->
</footer>
<!-- /wp:group -->
the four widget areas, resolved:
footer-1 a navigation menu → wp:navigation
footer-2 a text widget with
the address → wp:paragraph
footer-3 a custom widget
listing recent posts → wp:query, and the
widget deleted
footer-4 empty since 2019 → removed
and the widget area registrations, and the custom widget
class, and its stylesheet: 340 lines deleted.The custom recent-posts widget being replaced by a query loop is the pattern for most legacy widgets — they exist because there was no block that did it, and now there is. The one that had been empty since 2019 is the more common finding.
A custom post type the site editor can template
register_post_type( 'case_study', array(
'public' => true,
'show_in_rest' => true, // required for the block editor
'has_archive' => true, // and for an archive template
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
) );
<!-- templates/archive-case_study.html -->
<!-- wp:query {"query":{"postType":"case_study","perPage":12}} -->
<!-- wp:post-template -->
<!-- wp:post-featured-image {"isLink":true} /-->
<!-- wp:post-title {"isLink":true,"level":2} /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination --><!-- /wp:query-pagination -->
<!-- /wp:query -->
The archive template that had been a hundred and forty lines of PHP with a custom WP_Query, a manual pagination function and its own markup is nine lines of block comments. That is the case where the site editor is straightforwardly better, and it is most archive templates.
What has no equivalent
two templates stayed PHP, and will:
page-pricing.php
renders a table whose rows come from a third-party
API, with a fallback, a cache and error handling.
the query loop cannot express "call this service".
→ could become a dynamic block. costed at 3 days
against a template nobody edits. deferred.
archive-event.php
a query with a meta comparison, a date fallback and
an ordering that depends on whether the event is
past. the query block cannot express the condition.
→ a pre_get_posts filter plus a block template was
tried and the conditional ordering did not survive.
the rule: if the template contains an `if` that changes
what is queried, it stays PHP.That rule — a conditional affecting the query keeps it in PHP — is the clearest line we found, and it is worth stating because the alternative is discovering it three days into a conversion. Conditionals affecting presentation are fine; conditionals affecting the query are not.
The content migration that was not needed
1,412 posts written in the classic editor, none of them
block content.
what happens when a block theme renders them: the
post-content block outputs the raw content, exactly as
before. classic content renders identically.
what we did NOT do:
bulk-convert posts to blocks
run the block converter over 1,412 posts
touch the content at all
the styling had to accommodate it: a .entry-content
fallback stylesheet for markup that has no block classes.
74 lines, kept.Verifying it worked
# rendered markup, diffed template by template
$ ./bin/render-diff --urls=urls.txt --before=old --after=new
188 URLs compared
identical: 171
differing: 17
of which intentional (spacing tokens): 15
genuine regressions: 2
# the two: a missing rel="noopener" on footer links,
# and a heading level change on the archive template.
$ npx backstop test
Passed: 44 Failed: 3 (all three approved)
# and the actual test
# an editor changed the footer phone number, alone,
# in 4 minutes, with no deploy.Diffing rendered markup for a hundred and eighty-eight URLs is the only verification that scales here, and the two regressions it found were both invisible to a visual comparison. The editor changing a phone number unaided is the outcome the eleven tickets a quarter were paying for.
What this costs
A site whose appearance is now partly in the database and partly in git. A template part edited in the site editor is stored as a post, which means the git version is a default rather than the truth, and a deploy does not restore it. That is a genuine change to how the site is operated and it has to be explained.
It also means a staging-to-production content migration now moves appearance as well as content, which the previous arrangement kept cleanly separated. There is no good tooling for this: exporting the modified template parts is a manual step, and getting it wrong means a design change made on staging is silently absent in production.
Thirty-nine PHP templates remain, so the theme is now two systems rather than one. That is a stable state rather than a half-finished migration — six templates serve ninety-four per cent of traffic and are in the site editor, and the rest are the long tail — but somebody joining the project has to learn both.