Classic WordPress themes have powered the web for over two decades, but the block editor and Full Site Editing have changed what a theme can do. If you’re maintaining a classic theme and want to convert it to a block theme, this guide walks through every step, from auditing your existing files to writing real theme.json settings and HTML block templates that actually work.
What You’re Converting and Why It Matters
A classic WordPress theme is built around PHP template files (header.php, footer.php, single.php, and so on) plus a functions.php file that registers menus, widget areas, post thumbnails, and custom styles. A block theme replaces all of that with:
- HTML block templates in a
templates/folder, these use block markup instead of PHP calls - HTML template parts in a
parts/folder, reusable pieces like headers and footers theme.json, a single configuration file that replaces most of whatfunctions.phpused to do (color palettes, font sizes, spacing presets, layout settings)- No PHP required for templates, the block editor renders everything through core blocks and patterns
The result is a theme that works fully inside the Site Editor, lets users edit headers and footers visually, and gets automatic updates from core blocks as WordPress evolves. Before you write a single line of new code, it helps to understand how theme.json controls global settings and styles, the complete reference will save you significant trial-and-error during this conversion.
Step 1: Audit Your Classic Theme Structure
Before writing a single line of new code, map out what your classic theme currently does. Open the theme folder and list every PHP template file:
your-classic-theme/
├── style.css
├── functions.php
├── index.php
├── header.php
├── footer.php
├── single.php
├── page.php
├── archive.php
├── search.php
├── 404.php
├── sidebar.php
├── template-parts/
│ ├── content.php
│ └── content-single.php
└── assets/
├── css/
└── js/
For each file, note:
- What PHP functions it calls (
get_header(),the_loop(),get_sidebar(), custom template tags) - What widgets or dynamic sidebars it outputs
- Any custom query logic or conditional template logic
- Enqueued scripts or styles registered in
functions.php
This audit produces a conversion checklist. Everything in that list needs a block-theme equivalent, either a core block, a block pattern, a plugin, or (rarely) a custom block.
What Cannot Convert Directly
Some classic-theme features don’t have a one-to-one block equivalent:
- Widget areas, replaced by block template parts; widgets themselves are legacy and should be rebuilt as blocks
- Dynamic sidebars, no equivalent; use a Columns block or Query Loop block instead
- Custom template tags (functions like
mytheme_posted_on()), migrate their output into a Post Meta block or a custom block - Complex custom queries, the Query Loop block covers most cases; edge cases need a custom block. See the guide on building custom blocks for block themes using the WordPress Block API for those scenarios
Step 2: Set Up the Block Theme File Structure
Create a new folder for your block theme. The minimum required structure looks like this:
your-block-theme/
├── style.css ← required: theme header lives here
├── theme.json ← required: design tokens + editor settings
├── templates/
│ ├── index.html ← required: fallback template
│ ├── single.html
│ ├── page.html
│ ├── archive.html
│ ├── search.html
│ └── 404.html
└── parts/
├── header.html
└── footer.html
The style.css file still needs the theme header comment block, but it no longer needs to contain any CSS, you can load styles through theme.json or enqueue them from functions.php (which block themes can still include for PHP-dependent functionality).
/*
Theme Name: Your Block Theme
Theme URI: https://example.com
Description: A block theme converted from a classic theme.
Version: 1.0.0
Requires at least: 6.0
Tested up to: 6.5
Requires PHP: 7.4
Author: Your Name
License: GNU General Public License v2 or later
*/
Step 3: Migrate functions.php to theme.json
This is the biggest conceptual shift. In a classic theme, functions.php calls things like add_theme_support(), register_nav_menus(), and add_editor_style(). Most of those moves into theme.json. For a production theme, pair this with a design token system in theme.json so your colors, font sizes, and spacing remain maintainable as the theme grows.
Classic functions.php Patterns and Their theme.json Equivalents
| Classic functions.php | theme.json equivalent |
|---|---|
add_theme_support('editor-color-palette', [...]) | settings.color.palette |
add_theme_support('editor-font-sizes', [...]) | settings.typography.fontSizes |
add_theme_support('custom-spacing') | settings.spacing.padding: true |
add_theme_support('align-wide') | settings.layout.wideSize |
add_theme_support('post-thumbnails') | Automatic in block themes |
add_theme_support('title-tag') | Automatic in block themes |
Here is a complete theme.json that maps a typical classic theme’s functions.php settings:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"layout": {
"contentSize": "760px",
"wideSize": "1100px"
},
"color": {
"palette": [
{
"slug": "primary",
"color": "#1a1a2e",
"name": "Primary"
},
{
"slug": "secondary",
"color": "#16213e",
"name": "Secondary"
},
{
"slug": "accent",
"color": "#0f3460",
"name": "Accent"
},
{
"slug": "surface",
"color": "#f5f5f5",
"name": "Surface"
},
{
"slug": "white",
"color": "#ffffff",
"name": "White"
}
]
},
"typography": {
"fontSizes": [
{
"slug": "small",
"size": "0.875rem",
"name": "Small"
},
{
"slug": "medium",
"size": "1rem",
"name": "Medium"
},
{
"slug": "large",
"size": "1.25rem",
"name": "Large"
},
{
"slug": "x-large",
"size": "1.5rem",
"name": "Extra Large"
},
{
"slug": "xx-large",
"size": "2rem",
"name": "2X Large"
}
],
"fontFamilies": [
{
"slug": "inter",
"name": "Inter",
"fontFamily": "Inter, sans-serif",
"fontFace": [
{
"fontFamily": "Inter",
"fontWeight": "400",
"fontStyle": "normal",
"src": ["file:./assets/fonts/inter-regular.woff2"]
},
{
"fontFamily": "Inter",
"fontWeight": "600",
"fontStyle": "normal",
"src": ["file:./assets/fonts/inter-semibold.woff2"]
}
]
}
]
},
"spacing": {
"spacingScale": {
"operator": "*",
"increment": 1.5,
"steps": 7,
"mediumStep": 1.5,
"unit": "rem"
},
"spacingSizes": [
{ "slug": "20", "size": "0.5rem", "name": "Extra Small" },
{ "slug": "30", "size": "1rem", "name": "Small" },
{ "slug": "40", "size": "1.5rem", "name": "Medium" },
{ "slug": "50", "size": "2.5rem", "name": "Large" },
{ "slug": "60", "size": "4rem", "name": "Extra Large" }
],
"padding": true,
"margin": true,
"blockGap": true
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--white)",
"text": "var(--wp--preset--color--primary)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--inter)",
"fontSize": "var(--wp--preset--font-size--medium)",
"lineHeight": "1.6"
},
"spacing": {
"blockGap": "var(--wp--preset--spacing--40)"
},
"elements": {
"h1": { "typography": { "fontSize": "var(--wp--preset--font-size--xx-large)", "fontWeight": "700" } },
"h2": { "typography": { "fontSize": "var(--wp--preset--font-size--x-large)", "fontWeight": "600" } },
"h3": { "typography": { "fontSize": "var(--wp--preset--font-size--large)", "fontWeight": "600" } },
"link": {
"color": { "text": "var(--wp--preset--color--accent)" },
":hover": { "color": { "text": "var(--wp--preset--color--secondary)" } }
}
}
}
}
Notice how color palette slugs like primary automatically become CSS custom properties: var(--wp--preset--color--primary). You reference them the same way in both theme.json styles and any custom CSS you add.
Step 4: Convert PHP Templates to HTML Block Templates
Each PHP template file gets a corresponding HTML file in the templates/ folder. The HTML files contain block markup, the same serialized format the block editor saves to the database.
The Fallback: templates/index.html
This is the only required template. WordPress uses it as a fallback when no more specific template matches. It should handle both singular and archive views using the Query Loop block:
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":true}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|40","bottom":"var:preset|spacing|40"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:post-featured-image {"isLink":true,"aspectRatio":"16/9"} /-->
<!-- wp:post-title {"isLink":true,"level":2} /-->
<!-- wp:post-excerpt {"moreText":"Read more"} /-->
<!-- wp:post-date /-->
</div>
<!-- /wp:group -->
<!-- /wp:post-template -->
<!-- wp:query-pagination {"layout":{"type":"flex","justifyContent":"center"}} -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
The Header Part: parts/header.html
Template parts live in the parts/ folder and are referenced by slug. Here is a full parts/header.html with site logo, navigation, and a skip-to-content link for accessibility:
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|30","bottom":"var:preset|spacing|30"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap","justifyContent":"space-between","verticalAlignment":"center"}} -->
<div class="wp-block-group">
<!-- wp:site-logo {"width":120} /-->
<!-- wp:site-title {"level":0} /-->
<!-- wp:navigation {"overlayMenu":"mobile","layout":{"type":"flex","flexWrap":"nowrap"}} /-->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->
Single Post Template: templates/single.html
This replaces single.php and template-parts/content-single.php. The Post Content block renders the actual post blocks, replacing the_content():
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-featured-image {"aspectRatio":"16/9"} /-->
<!-- wp:post-title {"level":1} /-->
<!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap"}} -->
<div class="wp-block-group">
<!-- wp:post-author {"showAvatar":true,"avatarSize":40} /-->
<!-- wp:post-date /-->
<!-- wp:post-terms {"term":"category"} /-->
</div>
<!-- /wp:group -->
<!-- wp:separator /-->
<!-- wp:post-content /-->
<!-- wp:separator /-->
<!-- wp:post-terms {"term":"post_tag","prefix":"Tags: "} /-->
<!-- wp:post-navigation-link {"type":"previous","label":"Previous Post"} /-->
<!-- wp:post-navigation-link {"type":"next","label":"Next Post"} /-->
<!-- wp:comments /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
Step 5: Handle Custom Template Tags
Classic themes often include custom template tag functions, PHP helpers that output formatted meta data, author boxes, reading time, and similar. When converting to a block theme, each of these needs a strategy:
Option A: Replace with Core Blocks
Most custom template tags output something a core block already handles. Map them first:
| Classic template tag | Block equivalent |
|---|---|
mytheme_posted_on(), date + author | Post Date + Post Author blocks |
mytheme_posted_in(), categories + tags | Post Terms block (run it twice: categories, then tags) |
mytheme_post_thumbnail() | Post Featured Image block |
mytheme_entry_footer(), edit link | Post Date + Post Terms + custom block for edit link |
mytheme_reading_time() | No core block, use a plugin or custom block |
Option B: Keep PHP in functions.php
Block themes can still have a functions.php file. Anything that needs PHP, custom REST endpoints, block patterns registered via PHP, or server-side rendering, stays in functions.php. You don’t have to move everything into theme.json at once. Start by removing only what theme.json handles, and keep the rest in PHP until you have a block alternative.
Here is a minimal functions.php for a converted block theme, stripped of everything theme.json now handles:
<?php
/**
* Block theme functions, stripped down from classic theme.
* theme.json now handles: color palette, font sizes, spacing, layout widths.
* functions.php now handles: block patterns, REST API, and custom blocks only.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register block patterns.
*/
function mytheme_register_block_patterns() {
register_block_pattern(
'mytheme/hero-section',
[
'title' => __( 'Hero Section', 'mytheme' ),
'categories' => [ 'featured' ],
'content' => '<!-- wp:cover {"dimRatio":50} --><div class="wp-block-cover"><div class="wp-block-cover__inner-container"><!-- wp:heading {"level":1} --><h1>Hero Heading</h1><!-- /wp:heading --></div></div><!-- /wp:cover -->',
]
);
}
add_action( 'init', 'mytheme_register_block_patterns' );
/**
* Enqueue custom block styles (only what theme.json can't handle).
*/
function mytheme_enqueue_styles() {
wp_enqueue_style(
'mytheme-custom',
get_template_directory_uri() . '/assets/css/custom.css',
[],
wp_get_theme()->get( 'Version' )
);
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_styles' );
Step 6: Test the Converted Theme
With the file structure in place, activate the theme on a development environment and run through this checklist:
- Site Editor opens, go to Appearance > Editor and verify templates and template parts show up
- Navigation menus, assign existing menus to the Navigation block inside the header part
- Color palette visible in the editor, open a block’s color picker and confirm your custom colors appear
- Font sizes in the editor, check that your custom slugs (small, medium, large) appear in the size picker
- Wide alignment works, add an image block, set it to Wide, verify it respects
wideSizefromtheme.json - All post types render, test single post, page, archive, search, and 404 templates
- No PHP errors, enable
WP_DEBUGand check the debug log for any missing function calls that survived the migration
Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
| Blank page after activation | Missing templates/index.html | Create the file with at minimum a single paragraph block |
| Header not showing | Wrong slug in wp:template-part | Verify "slug":"header" matches the filename parts/header.html |
| Custom colors not in editor | Typo in theme.json slug or missing settings.color.palette | Validate JSON with a linter; check version is set to 3 |
| Navigation block empty | No menu assigned in the Site Editor | Go to Editor, click the Navigation block, assign a menu |
| Layout not constrained | Missing "layout":{"type":"constrained"} on the main group | Add the layout attribute to your outermost group in each template |
What Comes Next in This Series
This article covered the core conversion, file structure, theme.json migration, PHP template to HTML block template conversion, and handling custom template tags. The remaining articles in the Block Theme Migration series go deeper:
- Article 2: Building a Custom Block Pattern Library for Your Block Theme
- Article 3: Managing Global Styles and Per-Block Style Variations in theme.json
- Article 4: Converting Classic Theme Child Themes to Block Theme Child Themes
- Article 5: Performance and Accessibility Testing Your Block Theme Before Launch
Start Your Conversion Today
Converting a classic WordPress theme to a block theme is a structured process, not a rebuild from scratch. You keep your design decisions, colors, fonts, spacing, but move them into theme.json where the editor can work with them. Your PHP templates become HTML block templates that the Site Editor can render and modify. Your functions.php shrinks to only what still needs PHP.
Start with the audit, set up the file structure, migrate your color palette and font sizes to theme.json first, then tackle templates one at a time. By the end of this series, you’ll have a fully converted block theme that works with the Site Editor, gets maintained by core, and gives your users full control over their site’s design.
