How to Convert a Classic WordPress Theme to a Block Theme Step by Step

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 what functions.php used 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.phptheme.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 tagBlock equivalent
mytheme_posted_on(), date + authorPost Date + Post Author blocks
mytheme_posted_in(), categories + tagsPost Terms block (run it twice: categories, then tags)
mytheme_post_thumbnail()Post Featured Image block
mytheme_entry_footer(), edit linkPost 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 wideSize from theme.json
  • All post types render, test single post, page, archive, search, and 404 templates
  • No PHP errors, enable WP_DEBUG and check the debug log for any missing function calls that survived the migration

Common Errors and Fixes

ErrorCauseFix
Blank page after activationMissing templates/index.htmlCreate the file with at minimum a single paragraph block
Header not showingWrong slug in wp:template-partVerify "slug":"header" matches the filename parts/header.html
Custom colors not in editorTypo in theme.json slug or missing settings.color.paletteValidate JSON with a linter; check version is set to 3
Navigation block emptyNo menu assigned in the Site EditorGo to Editor, click the Navigation block, assign a menu
Layout not constrainedMissing "layout":{"type":"constrained"} on the main groupAdd 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.

Scroll to Top