Hybrid Themes and Migration to Block Themes
|
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 classic theme does not have to be rewritten in one go to benefit from the block editor. WordPress lets it adopt block
features one at a time: a theme.json file, block-based template parts printed from PHP, block patterns, and single
block templates for some routes. A theme in that middle state is usually called a hybrid theme. Adding
templates/index.html finally turns it into a block theme. This page, for theme developers maintaining a classic theme
and site owners planning a redesign, takes the Lumen Press theme lumen-classic through every stage, checking after
each step what WordPress detects, which admin screens appear and what the front end renders.
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, in beta and due in December 2026,
does not change the hybrid-theme behaviour described here so far.
Theme kinds are introduced in Themes Overview; the classic pieces replaced here are covered in Customizer API, Widgets and Sidebars and Navigation and Menus.
What a hybrid theme is
"Hybrid theme" is a community term, not a third theme type. Only one file decides: a theme with a readable
templates/index.html is a block theme (wp_is_block_theme() returns true); every other theme is classic, however
many block features it uses. A hybrid theme renders pages through the PHP template hierarchy, has no Site Editor, and
keeps the Customizer, Menus and Widgets screens.
Each block feature that a classic theme can adopt has its own switch and arrived in its own version:
| Feature | Since | What it gives a classic theme |
|---|---|---|
|
5.8 |
Global settings and styles: palettes, font sizes, layout widths, CSS custom properties, editor controls. |
|
5.9 |
Prints |
|
6.1 |
Lets administrators edit those parts in the Site Editor; without it they are files only. |
|
6.0 |
Auto-registered block patterns, as in block themes. |
Appearance > Patterns |
6.5 |
The Site Editor’s Patterns screen for any classic theme (moved into the Site Editor in 6.6). |
Appearance > Design |
6.8 |
Replaces Patterns for a classic theme that declares |
Appearance > Fonts |
7.0 |
The Font Library page for every theme, classic ones included. |
Learn WordPress treats each stage as a valid stopping point. Staying hybrid suits a site that depends on PHP logic in its templates or on Customizer options it cannot drop yet; a full conversion suits owners who should edit layouts without a developer, or a theme being redesigned anyway.
The migration path
The site keeps working after each step, so each can ship as a separate release and be checked the same way.
A site to follow along
The examples use the Compose stack from
Themes Overview (MariaDB 11.8
and the wordpress:cli-2.12.0-php8.4 image, WordPress 7.1.3 at https://lumenpress.test). Add one more volume to the
cli service, ./scripts:/scripts:ro, for the two helper scripts below. Activate the lumen-catalog plugin for the
lp_book post type. WP_DEVELOPMENT_MODE set to theme turns off the caches of theme patterns and theme.json (not
for production), and wp server serves the front end inside the container.
docker compose exec cli wp config set WP_DEVELOPMENT_MODE theme
docker compose exec cli wp theme activate lumen-classic
docker compose exec cli sh -c '
wp rewrite structure "/%postname%/"
wp post create --post_type=page --post_title=About --post_status=publish --porcelain # 4
wp post create --post_type=page --post_title=Contact --post_status=publish --porcelain # 5
wp post create --post_type=lp_book --post_title="The Glass Orchard" --post_status=publish
wp menu create Primary
wp menu item add-custom primary Books https://lumenpress.test/books/
wp menu item add-post primary 4
wp menu item add-post primary 5
wp menu location assign primary primary
wp widget reset --all
wp widget add block lumen-footer --content="<!-- wp:paragraph --><p>Lumen Press, independent publishers since 1998.</p><!-- /wp:paragraph -->"
wp theme mod set lumen_classic_accent_color "#8a1f18"'
docker compose exec -d cli wp server --host=127.0.0.1 --port=8080
Source: WP-CLI Commands
The seed data is what a migration must not lose: a classic menu, a footer widget and an owner’s Customizer colour.
The first helper prints how WordPress classifies the active theme and builds the Appearance menu as wp-admin does:
<?php
// Prints how WordPress sees the active theme. Usage: wp eval-file /scripts/check-theme.php --user=admin
printf( "theme: %s\n", get_stylesheet() );
printf( "wp_is_block_theme: %s\n", var_export( wp_is_block_theme(), true ) );
printf( "has theme.json: %s\n", var_export( wp_theme_has_theme_json(), true ) );
printf( "block-templates: %s\n", var_export( current_theme_supports( 'block-templates' ), true ) );
printf( "block-template-parts: %s\n", var_export( current_theme_supports( 'block-template-parts' ), true ) );
// Build the admin menu the way wp-admin does, then list Appearance.
require_once ABSPATH . 'wp-admin/includes/admin.php';
global $menu, $submenu, $_wp_submenu_nopriv;
$_SERVER['REQUEST_URI'] = '/wp-admin/';
require ABSPATH . 'wp-admin/menu.php';
$items = array();
foreach ( $submenu['themes.php'] as $item ) {
$label = trim( wp_strip_all_tags( preg_replace( '/<span.*<\/span>/', '', $item[0] ) ) );
$items[] = $label . ' (' . remove_query_arg( 'return', $item[2] ) . ')';
}
echo 'Appearance: ' . implode( ', ', $items ) . "\n";
The second requests a page from wp server and prints the parts of the HTML that matter:
<?php
// Prints the matches of a regex in a page served by wp server. Usage: php /scripts/fetch.php <path> <regex>
$context = stream_context_create( array( 'http' => array( 'header' => "Host: lumenpress.test\r\n", 'ignore_errors' => true ) ) );
$html = file_get_contents( 'http://127.0.0.1:8080' . $argv[1], false, $context );
preg_match_all( '#' . $argv[2] . '#', $html, $matches );
echo implode( "\n", array_unique( $matches[0] ) ), "\n";
Source: WP-CLI Commands: wp server
Step 0: the classic starting point
lumen-classic is the minimal classic theme from Themes Overview, split into
header.php, index.php and footer.php as described in
Classic Theme Anatomy and The Loop. header.php prints the
menu with wp_nav_menu( array( 'theme_location' => 'primary' ) ) and footer.php prints the widget area with
dynamic_sidebar( 'lumen-footer' ). Its functions.php holds the three things the migration has to map: a menu location,
a widget area and a Customizer colour.
<?php
/**
* Lumen Classic theme setup (the starting point of the migration).
*
* @package Lumen_Classic
*/
function lumen_classic_setup() {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'html5', array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script' ) );
register_nav_menus( array( 'primary' => __( 'Primary menu', 'lumen-classic' ) ) );
}
add_action( 'after_setup_theme', 'lumen_classic_setup' );
function lumen_classic_widgets_init() {
register_sidebar(
array(
'id' => 'lumen-footer',
'name' => __( 'Footer', 'lumen-classic' ),
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
)
);
}
add_action( 'widgets_init', 'lumen_classic_widgets_init' );
function lumen_classic_customize_register( $wp_customize ) {
$wp_customize->add_setting(
'lumen_classic_accent_color',
array(
'default' => '#a3271f',
'sanitize_callback' => 'sanitize_hex_color',
)
);
$wp_customize->add_control(
new WP_Customize_Color_Control(
$wp_customize,
'lumen_classic_accent_color',
array(
'label' => __( 'Accent colour', 'lumen-classic' ),
'section' => 'colors',
)
)
);
}
add_action( 'customize_register', 'lumen_classic_customize_register' );
function lumen_classic_enqueue_assets() {
wp_enqueue_style( 'lumen-classic-style', get_stylesheet_uri(), array(), wp_get_theme()->get( 'Version' ) );
$accent = sanitize_hex_color( get_theme_mod( 'lumen_classic_accent_color', '#a3271f' ) );
wp_add_inline_style( 'lumen-classic-style', ':root{--lumen-classic-accent:' . $accent . ';}' );
}
add_action( 'wp_enqueue_scripts', 'lumen_classic_enqueue_assets' );
Source: Theme Handbook: Theme Functions
docker compose exec cli wp eval-file /scripts/check-theme.php --user=admin
docker compose exec cli php /scripts/fetch.php / '<header[^>]*>|<ul id="menu-primary"[^>]*>|--lumen-classic-accent:[^;]+;|<div id="block-[0-9]+" class="widget[^"]*">'
Source: WP-CLI Commands: wp eval-file
theme: lumen-classic
wp_is_block_theme: false
has theme.json: false
block-templates: false
block-template-parts: false
Appearance: Themes (themes.php), Patterns (site-editor.php?p=%2Fpattern), Customize (customize.php), Widgets (widgets.php), Fonts (font-library.php), Menus (nav-menus.php), Theme File Editor (theme-editor.php)
--lumen-classic-accent:#8a1f18;
<header class="site-header">
<ul id="menu-primary" class="menu">
<div id="block-7" class="widget widget_block widget_text">
Even this plain classic theme gets two block-era screens: Patterns (6.5) and Fonts (7.0).
Step 1: add theme.json
A theme.json in the theme root moves design tokens out of PHP and CSS and switches on the editor’s design tools. It
uses the palette and widths of the block theme lumen; defaultPalette: false hides core’s colours:
{
"$schema": "https://schemas.wp.org/wp/7.1/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"color": {
"defaultPalette": false,
"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)" },
"elements": {
"link": {
"color": { "text": "var(--wp--preset--color--lumen-red)" }
}
}
}
}
The colour now has a home in theme.json, so the Customizer setting and the inline CSS go. functions.php loses
lumen_classic_customize_register() and its hook, lumen_classic_enqueue_assets() keeps only the
wp_enqueue_style() call, and the a { color: ... } rule leaves style.css, because
styles.elements.link now produces it. Editor-related theme supports that a classic theme may declare are
superseded by theme.json, which wins when both exist:
add_theme_support() feature |
theme.json equivalent |
|---|---|
|
|
|
|
|
|
|
|
|
Not recommended with |
$ wp eval-file /scripts/check-theme.php --user=admin
theme: lumen-classic
wp_is_block_theme: false
has theme.json: true
block-templates: true
block-template-parts: false
Appearance: Themes (themes.php), Design (site-editor.php), Customize (customize.php), Widgets (widgets.php), Fonts (font-library.php), Menus (nav-menus.php), Theme File Editor (theme-editor.php)
$ php /scripts/fetch.php / '--wp--preset--color--lumen-red: [^;]+;|--wp--style--global--content-size: [^;]+;|a:where\(:not\(.wp-element-button\)\)\{[^}]+\}'
--wp--preset--color--lumen-red: #a3271f;
--wp--style--global--content-size: 680px;
a:where(:not(.wp-element-button)){color: var(--wp--preset--color--lumen-red);}
Still classic, the theme now has block-templates support; Patterns became Design (6.8: Style Book and
Patterns), and the global stylesheet carries the presets and the link rule.
Carrying the owner’s Customizer value over
The owner’s Customizer value #8a1f18 lives in the option theme_mods_lumen-classic, which nothing reads any more.
Owner choices belong in the user layer of global styles, a wp_global_styles post per theme where the Site Editor
also stores edited presets. A one-off script copies the value across:
<?php
/**
* Copies the Lumen Classic accent colour from the Customizer into the user's global styles.
* Usage: wp eval-file /scripts/migrate-customizer.php --user=admin
*/
// Theme mods are stored per theme slug, so read the classic theme's option directly.
$mods = get_option( 'theme_mods_lumen-classic', array() );
$accent = isset( $mods['lumen_classic_accent_color'] ) ? sanitize_hex_color( $mods['lumen_classic_accent_color'] ) : '';
if ( ! $accent ) {
WP_CLI::success( 'The accent colour was never customized; the theme.json default applies.' );
return;
}
// The wp_global_styles post of the active theme (created if missing).
$post = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme(), true );
$data = json_decode( $post['post_content'], true );
// Override the theme palette, as the Site Editor does when a preset colour is edited.
$palette = wp_get_global_settings( array( 'color', 'palette', 'theme' ) );
foreach ( $palette as $i => $color ) {
if ( 'lumen-red' === $color['slug'] ) {
$palette[ $i ]['color'] = $accent;
}
}
$data['settings']['color']['palette']['theme'] = $palette;
wp_update_post(
array(
'ID' => $post['ID'],
'post_content' => wp_json_encode( $data ),
)
);
WP_CLI::success( "lumen-red is now {$accent} in global styles post {$post['ID']}." );
$ wp eval-file /scripts/migrate-customizer.php --user=admin
Success: lumen-red is now #8a1f18 in global styles post 10.
$ php /scripts/fetch.php / '--wp--preset--color--lumen-red: [^;]+;'
--wp--preset--color--lumen-red: #8a1f18;
The user layer applies to a classic theme with theme.json too, so the colour is right from this step on. Other
Customizer settings map in the same spirit:
| Classic (Customizer) | Block theme equivalent |
|---|---|
Colour, font and width settings of the theme ( |
|
Additional CSS (a |
Styles > Additional CSS. A block theme still prints the old Customizer CSS, now inside the global stylesheet (checked in Step 5) |
Site Identity: logo, title, tagline, site icon |
Site Logo, Site Title and Site Tagline blocks; the site icon stays under Settings > General |
Header image, background image |
An Image or Cover block in the header part; |
Text settings such as footer text |
Plain content in a template part or pattern, edited in place |
Step 2: block template parts in PHP templates
The header becomes a block template part: the part file, its theme.json registration and a block_template_part()
call in header.php. Adding add_theme_support( 'block-template-parts' ) to lumen_classic_setup() lets
administrators edit the part in the Site Editor; without it the part renders from the file only.
parts/header.html contains the Site Title and a Navigation block. The Navigation block has no ref yet, which is
deliberate (see below):
<!-- wp:group {"tagName":"header","className":"site-header","layout":{"type":"flex","justifyContent":"space-between"}} -->
<header class="wp-block-group site-header">
<!-- wp:site-title {"level":0} /-->
<!-- wp:navigation {"overlayMenu":"mobile"} /-->
</header>
<!-- /wp:group -->
Source: Theme Handbook: Template Parts
theme.json gains a top-level list that assigns the part to the Header area,
"templateParts": [ { "name": "header", "title": "Header", "area": "header" } ].
header.php keeps the document head and replaces the hand-written header with the part. The wp-site-blocks wrapper
is the class block templates use for their outer element, so block layout styles apply the same way in both:
<?php // header.php: document head, then the block template part parts/header.html. ?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<div class="wp-site-blocks">
<?php block_template_part( 'header' ); ?>
<main id="content">
footer.php closes the extra </div> before wp_footer(). block_template_part( $part ) looks up
<stylesheet>//<part> (an edited copy in the database first, then the theme file), prints it through do_blocks(),
and prints nothing if the part does not exist; block_header_area() and block_footer_area() are shortcuts.
$ wp eval --user=admin 'foreach ( rest_do_request( "/wp/v2/template-parts" )->get_data() as $part ) { printf( "%s %s %s\n", $part["id"], $part["area"], $part["source"] ); }'
lumen-classic//header header theme
$ php /scripts/fetch.php / '<header class="wp-block-group site-header|<p class="wp-block-site-title"|<nav class="[^"]*wp-block-navigation[^"]*"|<span class="wp-block-navigation-item__label">[^<]+'
<header class="wp-block-group site-header
<p class="wp-block-site-title"
<nav class="is-responsive wp-block-navigation is-layout-flex wp-block-navigation-is-layout-flex"
<span class="wp-block-navigation-item__label">Books
<span class="wp-block-navigation-item__label">About
<span class="wp-block-navigation-item__label">Contact
$ wp post list --post_type=wp_navigation --fields=ID,post_title,post_name
ID post_title post_name
12 Primary primary
The Appearance menu does not change in 7.1: the header part is edited from Appearance > Design > Patterns, under the
Header area. The REST API lists the part with source theme until someone edits it.
Menus become Navigation
The menu items appear although nothing was migrated by hand: a Navigation block without ref uses the fallback, which
converted the classic menu in the primary location into a wp_navigation post (ID 12) on the first page view,
leaving the classic menu untouched. The converted items
keep the classic CSS classes (menu-item menu-item-type-post_type ...) and absolute URLs. Once the result looks
right, select the menu in the editor so that the part stores {"ref":12} instead of relying on the fallback.
Importing from the block’s menu, scripting the import with WP_Classic_To_Block_Menu_Converter, and turning the
automatic creation off are described in
Navigation and Menus. register_nav_menus()
stays in functions.php until Step 5, so the classic menu can still be edited and switched back to.
Step 3: patterns replace widgets
Widget areas have no block-theme equivalent; their content becomes patterns placed in template parts. As a pattern, the Lumen Press footer text becomes translatable and versioned with the theme:
<?php
/**
* Title: Footer imprint
* Slug: lumen-classic/footer-imprint
* Categories: footer
* Block Types: core/template-part/footer
* Description: Publisher imprint and catalogue link, replacing the Footer widget area.
*
* @package Lumen_Classic
*/
?>
<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:paragraph -->
<p><?php esc_html_e( 'Lumen Press, independent publishers since 1998.', 'lumen-classic' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p><a href="<?php echo esc_url( home_url( '/books/' ) ); ?>"><?php esc_html_e( 'Browse the catalogue', 'lumen-classic' ); ?></a></p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->
parts/footer.html places the pattern, and theme.json adds
{ "name": "footer", "title": "Footer", "area": "footer" } to templateParts:
<!-- wp:group {"tagName":"footer","className":"site-footer","layout":{"type":"constrained"}} -->
<footer class="wp-block-group site-footer">
<!-- wp:pattern {"slug":"lumen-classic/footer-imprint"} /-->
</footer>
<!-- /wp:group -->
In footer.php, <?php block_template_part( 'footer' ); ?> (or the shortcut block_footer_area()) replaces the
<footer> element and its dynamic_sidebar() call; the closing </main>, </div> and wp_footer() stay.
$ wp eval '$p = WP_Block_Patterns_Registry::get_instance()->get_registered( "lumen-classic/footer-imprint" ); printf( "%s | %s | %s\n", $p["title"], implode( ",", $p["categories"] ), implode( ",", $p["blockTypes"] ) );'
Footer imprint | footer | core/template-part/footer
$ php /scripts/fetch.php / '<footer class="wp-block-group site-footer[^"]*"|<p class="wp-block-paragraph">Lumen Press[^<]*</p>|Browse the catalogue</a>|<div id="block-7"'
<footer class="wp-block-group site-footer is-layout-constrained wp-block-group-is-layout-constrained"
<p class="wp-block-paragraph">Lumen Press, independent publishers since 1998.</p>
Browse the catalogue</a>
The widget block-7 is no longer printed but still exists in the sidebars_widgets option. Sites with many widgets
need not rewrite them by hand: in a block theme, the Template Part block’s Advanced panel has Import widget area,
which copies a widget area’s blocks into a new template part (legacy PHP widgets arrive as Legacy Widget blocks). It
lists only registered, non-empty areas, so keep register_sidebar() until the import is done (see Step 5).
Writing widgets and blocks is covered in Widgets and Sidebars.
Without WP_DEVELOPMENT_MODE set to theme, the list of theme pattern files is cached in a site transient until the
theme’s Version header changes. A new file in patterns/ then silently does not register. Bump Version with every
release that adds patterns.
Step 4: block templates for selected routes
With block-templates support (from theme.json), core’s block template loader runs even in a classic theme:
locate_block_template() takes the PHP template the hierarchy found and renders an equally or more specific block
template from templates/ instead, through template-canvas.php. Routes can move one at a time; Lumen Press starts
with the book page:
<!-- wp:template-part {"slug":"header","tagName":"div"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-featured-image /-->
<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
<!-- wp:post-terms {"term":"lp_genre"} /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"div"} /-->
Source: Theme Handbook: Templates
With ?_wp-find-template=true the front end returns the chosen block template as JSON (PHP-served pages return HTML):
$ php /scripts/fetch.php '/books/the-glass-orchard/?_wp-find-template=true' '"id":"[^"]+"|<main id="content">'
"id":"lumen-classic\/\/single-lp_book"
$ php /scripts/fetch.php '/about/?_wp-find-template=true' '"id":"[^"]+"|<main id="content">'
<main id="content">
$ php /scripts/fetch.php /books/the-glass-orchard/ '<h1 class="wp-block-post-title"[^>]*>[^<]+|<main[^>]*>'
<main id="wp--skip-link--target" class="wp-block-group is-layout-constrained wp-block-group-is-layout-constrained">
<h1 class="wp-block-post-title">The Glass Orchard
The book uses single-lp_book.html, and its <main> gets the skip-link target that core adds to block templates. The
About page still uses index.php. The same support lets administrators create templates for single posts or pages
from the editor’s Template panel; they are stored as wp_template posts.
Step 5: templates/index.html makes it a block theme
The last fallback of the hierarchy, templates/index.html, is the switch. From now on every request is served by a
block template and PHP templates are ignored:
<!-- wp:template-part {"slug":"header","tagName":"div"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"queryId":1,"query":{"inherit":true}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"div"} /-->
With the file added and functions.php unchanged:
$ wp eval-file /scripts/check-theme.php --user=admin
theme: lumen-classic
wp_is_block_theme: true
has theme.json: true
block-templates: true
block-template-parts: true
Appearance: Themes (themes.php), Editor (site-editor.php), Fonts (font-library.php), Menus (nav-menus.php), Widgets (widgets.php)
$ wp eval --user=admin 'foreach ( rest_do_request( "/wp/v2/sidebars" )->get_data() as $s ) { printf( "%s %s [%s]\n", $s["id"], $s["name"], implode( ",", $s["widgets"] ) ); }'
wp_inactive_widgets Inactive widgets [block-2,block-3,block-4,block-5,block-6]
lumen-footer Footer [block-7]
Editor replaced Design, and the Theme File Editor moved to Tools. Customize is gone because nothing hooks
customize_register any more (a plugin that does would bring it back). Menus and Widgets remain only because
functions.php still registers a menu location and a sidebar, which lets Import widget area offer block-7. After
the import, the classic leftovers go. functions.php, header.php, footer.php and index.php are
deleted, and style.css keeps only its header. Core gives block themes post-thumbnails, html5, responsive-embeds,
editor-styles and feed links, the block template canvas prints <title>, and lumen needs no functions.php. The theme
holds style.css, theme.json, parts/header.html, parts/footer.html, patterns/footer-imprint.php,
templates/index.html and templates/single-lp_book.html.
The owner had also saved Customizer Additional CSS (wp_update_custom_css_post() stores it as a custom_css post).
The final check looks at that CSS too:
$ wp eval-file /scripts/check-theme.php --user=admin
theme: lumen-classic
wp_is_block_theme: true
has theme.json: true
block-templates: true
block-template-parts: false
Appearance: Themes (themes.php), Editor (site-editor.php), Fonts (font-library.php)
$ php /scripts/fetch.php / '<title>[^<]*</title>|--wp--preset--color--lumen-red: [^;]+;|\.site-header \{ border-bottom[^}]*\}|<header class="wp-block-group site-header|<footer class="wp-block-group site-footer'
<title>Lumen Press</title>
--wp--preset--color--lumen-red: #8a1f18;
.site-header { border-bottom: 1px solid currentColor; }
<header class="wp-block-group site-header
<footer class="wp-block-group site-footer
The title tag, the migrated colour, the old Additional CSS (now printed inside the global stylesheet, editable under
Styles > Additional CSS) and both parts are all there. With its sidebar gone, block-7 moves to Inactive widgets.
Renaming lumen-classic to lumen
Lumen Press ships the result as lumen (text domain lumen). Several records are keyed by the theme slug. On the test site, wp post list shows the custom_css post
named lumen-classic and the wp_global_styles post wp-global-styles-lumen-classic, and wp option list shows
theme_mods_lumen-classic:
| Keyed by slug | Effect of switching to lumen |
|---|---|
|
Menu locations, Customizer values and the stored sidebars of the old theme stay behind. The migration script above reads the old option by name, so it still works. |
|
User styles start empty for |
|
Additional CSS does not follow; copy it into Styles > Additional CSS. |
|
Edited templates and parts no longer apply. Export them into the theme files first (see Create Block Theme and Version Control). |
Pattern slugs |
Rename them in the files, and update |
|
Not tied to a theme; the imported menu survives the switch. |
What older books get wrong or leave out
| Older advice | Current state (7.1) |
|---|---|
A theme is either PHP templates with the Customizer or nothing, and block themes are left out (Pro WordPress, Ultimate WordPress Handbook) |
Classic themes can adopt |
Full site editing arrived in 5.4 to 5.6 (Pro WordPress) |
|
Customizer settings are output with |
Every setting needs a |
Menus are managed only under Appearance > Menus, and widgets are the way to add content to sidebars and footers (Pro WordPress) |
In a block theme, menus are Navigation blocks over |
"Reusable blocks" (Ultimate WordPress Handbook) |
Renamed synced patterns in 6.3. Patterns are available to classic themes through Appearance > Patterns (6.5) and Design (6.8). |
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
The Site Editor does not appear although the theme has |
Only |
|
The file is not |
The header part cannot be edited in the Site Editor |
|
Colours changed in |
The user layer (an earlier Customizer migration or Site Editor edit) overrides the theme. Check the theme’s |
A new pattern file is not registered |
The pattern list is cached until |
Customize is still in the menu of the finished block theme |
Something still hooks |
Import widget area offers nothing |
The widget area is no longer registered or is empty. Keep |
A block template in a hybrid theme is not used |
A more specific PHP template exists. For example, |
Security notes: patterns/*.php files run PHP whenever patterns are registered, so escape output (esc_html_e(),
esc_url()) and never echo request data in them. Run the migration scripts with WP-CLI on a staging copy first (see
Backups and Disaster Recovery), never as web-reachable files.