# Migrating Legacy WordPress Content to the Block Editor: A Real-World Case Study

- Link: https://elliottrichmond.co.uk/migrating-legacy-wordpress-content-to-the-block-editor-a-real-world-case-study/
- Published: 2025-07-23T10:35:13+00:00

When WordPress introduced the block editor (Gutenberg), it revolutionised content
creation but left many existing sites with a challenge: how do you migrate years
of legacy content, custom widgets, and meta fields to the new block-based system?

There’s is a tool called [try-](https://github.com/WordPress/try-wordpress/)[wordpress](https://github.com/WordPress/try-wordpress/)
that converts HTML into blocks, but it only works with content stored in the `wp_posts`
table, it doesn’t account for any associated postmeta data.

This post shares lessons learned from building a comprehensive migration plugin 
for kate & tom’s a holiday rental website that needed to transform a complex legacy
widget system from the `wp_postmeta` table into modern block editor content.

**The Challenge**

The site featured amongst other things, all its content in custom meta field data
in widgets for:

 * Custom page header banners with meta field data
 * Legacy house photo galleries stored as serialized arrays
 * 15+ different widget types (standard, video, gallery, CTA, etc.)
 * Complex booking calendar integration
 * Hundreds of property listings with sub-pages
 * Production URL transformations needed

**The Solution: A Systematic Approach**

A manual migration would’ve been nearly impossible due to the way block content 
is structured, it’s based on `wp_post` markup and doesn’t inherently account for
metadata unless you explicitly register it using `register_meta()` on the `init`
hook, which I’ve done in some cases. But since the majority of the content wasn’t
tied to lookup data, its just content, it made more sense to build a pattern with
some placeholders to then replace them with the relavant postmeta as block content,
so… I built an automated system using my own custom WordPress action hooks for the
following:

**Content Analysis & Mapping**

 * Identified all legacy meta fields and widget structures
 * Mapped each widget type to corresponding block patterns
 * Created reusable transformation functions

**Pattern-Based Architecture**

 * Built theme patterns for each content type
 * Used placeholder replacement for dynamic content
 * Leveraged WordPress’s parse_blocks() and serialize_blocks() functions

**Batch Processing**

 * Created action hooks for different migration phases
 * Added support for processing specific post IDs or entire collections
 * Implemented proper error logging and progress tracking

**Key Technical Implementation Details** **Widget-to-Block Transformation:**

    ```wp-block-code
    function resolve_meta_with_pattern($k, $meta, $postID) {
        $widget_type = $meta["widget_type"];

        switch ($widget_type) {
            case "standard-widget":
                return pattern_standard_widget($meta);
            case "video-widget":
                return pattern_video_widget($meta);
            // ... 15+ widget types handled
        }
    }
    ```

**Dynamic Block Generation:**
It does get technical at times, especially due to 
the conditional logic involved, since not all patterns with inner blocks follow 
the same structure. Essentially, each `switch` case calls a specific pattern function,
which then returns the corresponding block markup from a `Pattern_Handler` class.
This allows for the programmatic generation of complex block structures, handling
everything from nested blocks to attribute injection and dynamic content replacement.

Here’s a snapshot of the method from the `Pattern_Handler` class:

    ```wp-block-code
    private function standard_widget_text($blocks, $meta) {

    	foreach ($blocks as &$block) {
    		$block = $this->transform_block_attrs_html_content( $block, $meta );

    		// Handle nested blocks
    		if (!empty($block['innerBlocks'])) {
    			$block['innerBlocks'] = $this->standard_widget_text($block['innerBlocks'], $meta);
    		}

    		// Handle content replacements
    		$this->replace_standard_widget_text_content($block, $meta);
    	}

    	return $blocks;
    }
    ```

To give you a clearer idea, here’s a more visual representation. Here you’ll see
the original widget as it appears in the classic backend.

![A snapshot of a standard content widget in the backend of WordPress](https://elliottrichmond.
co.uk/wp-content/uploads/2025/07/backend-widget-1024x915.png)

… classically renders like this:

![Example of a standard widget rendered in the frontend of a classic WordPress theme](
https://elliottrichmond.co.uk/wp-content/uploads/2025/07/classic-widget-rendered-
1024x235.png)

I then built out the desired layout using the Block Editor and copied the generated
markup into a theme pattern file with a corresponding name, for example, `standard-
widget-text.php`. This allowed me to reuse and inject the pattern programmatically
based on the original widget type.

![Snapshot of markup from a WordPress theme pattern](https://elliottrichmond.co.
uk/wp-content/uploads/2025/07/standard-widget-text-1024x671.png)

When I place the pattern, it’s not an exact visual match out of the box. That’s 
intentional, I added a generic background color and placeholder content so the `
Pattern_Handler` class can use them as logical targets for replacement. So if you
drop the pattern directly into the editor without any dynamic processing, it looks
more like this:

![Example of a placed pattern in the WordPress Block Editor](https://elliottrichmond.
co.uk/wp-content/uploads/2025/07/theme-pattern-1024x202.png)

Sure, I could’ve used a more structured approach like Handlebars-style placeholders,
but using simple stand-ins like `TitleTitleTitle` and `subtitlesubtitlesubtitle`
was perfectly adequate for my needs. If this were being developed as a commercial
plugin, I’d definitely opt for a more robust solution, but given how specific this
is to the site, my current method works just fine.

Now, this example only covers a single widget, there are several others with far
more complexity and conditional logic, but hopefully you get the gist of what I’ve
achieved here. Once the script runs on this widget, the original content is transformed
into block markup that appears like this in the List View:

![Example of a pattern listed in the List View of the WordPress block editor](https://
elliottrichmond.co.uk/wp-content/uploads/2025/07/pattern-in-list-view.png)

And it renders perfectly, a direct conversion of the original widget, complete with
all its metadata, into a block pattern that inherits the correct attributes from
the theme pattern. It’s a clean, reliable translation from legacy to modern block-
based structure.

![WordPress block as rendered in the frontend and backend](https://elliottrichmond.
co.uk/wp-content/uploads/2025/07/perfect-widget-to-pattern-conversion-1024x254.png)

Now don’t get me wrong, this kind of programming isn’t for the fainthearted. It 
was seriously tempting to throw in the towel more than once, especially with all
the debugging and the endless back-and-forth caused by broken blocks or unexpected/
invalid content.

But giving up? That’s just not in my nature. In fact, the more complex it gets, 
the more I lean in. That said, my boss (a.k.a. the wife) does get _slightly_ frustrated
when she asks, “Is that job finished yet?” and I reply with a sheepish, “Not quite…”
_cue frown_.

![Example of a broken WordPress block in the backend](https://elliottrichmond.co.
uk/wp-content/uploads/2025/07/unexpected-or-invalid-1024x302.png)

Block markup is rigid and temperamental, especially when there’s a mismatch between`
innerHTML` and `innerContent`. That’s when you enter the dreaded “Attempt Block 
Recovery” loop, and trust me, it’s not fun. Programming at this level required a
systematic understanding of what the markup _should_ be and what it _shouldn’t_.

Tools like Kaleidoscope have been fundamental to this process, letting me quickly
compare the original markup against the generated output, spot discrepancies, and
debug where things were going wrong. Without that kind of visual diffing, this project
would’ve been a nightmare.

![Snapshot of Kaleidoscope to help me easily see original markup verse new markup](
https://elliottrichmond.co.uk/wp-content/uploads/2025/07/kaleidoscope-1024x605.jpg)

Screenshot

**External API Integration:**
As for the booking functionality, the plugin integrates
with external APIs to keep calendar data in sync throughout the migration process,
but honestly, that’s a whole other blog post in itself, so I won’t dive into those
details here.

**Lessons Learned (Sometimes the Hard Way)**

Take regular breaks and get plenty of sleep, seriously! Trying to power through 
this kind of work when you’re tired is pointless. Your concentration will crumble
under the weight of programmatically matching metadata to block markup. It’s a battle
you just won’t win in that state. Trust me, fatigue and complex logic do not mix,
it’s utterly futile.

But that’s the easy bit to overlook. For a more pragmatic approach, here’s how I
plan and tackle this kind of complexity:

**Plan the Pattern Architecture First**

 * Design your block patterns before writing migration code
 * Use consistent placeholder naming conventions
 * Test patterns manually before automation

**Handle Edge Cases Gracefully**

 * Not all legacy content fits neat patterns
 * Some widgets need conditional block removal
 * Always validate data before transformation

**Maintain Flexibility**

 * Built support for processing specific post IDs
 * Separate cleanup and migration actions
 * Reversible operations where possible

**Performance Considerations**

 * Process in batches to avoid timeouts
 * Use static variables to cache heavy operations (Pattern_Handler)
 * Log progress for large migrations

**The Results**

The migration successfully transformed:

 * 200+ house listings with image galleries
 * 500+ legacy widgets across various post types
 * Complex booking calendar integration
 * Automatic sub-page generation with proper content structure

**Migration Actions Available** (note – these can be fired individually for more
efficient debugging)

 * `create_page_header_banner_title_widgets` – Page header & widget conversion
 * `convert_house_photos` – Photo gallery migration
 * `add_thumbnail_featured_image` – Single post thumb migration
 * `cleanup_house_sub_pages` – Rollback migration
 * `run_widgets_to_patterns` – Bulk widget transformation
 * `setup_house_sub_pages` – Sub page migration
 * `setup_house_availability_calendar` – Booking integration
 * `append_houses_gallery_to_gallery_pages` – Gallery page migration
 * `run_cleanup_patterns` – Content cleanup utilities

**Takeaways for Your Migration**

 1. **Audit First**: Document all your legacy content structures
 2. **Build Incrementally**: Start with simple widgets, add complexity gradually
 3. **Test Extensively**: Run migrations on staging with real data
 4. **Plan Rollbacks**: Build cleanup functions for each migration step
 5. **Monitor Performance**: Large migrations can impact site performance

**Tools & Techniques Used**

 * WordPress Block Editor APIs (parse_blocks, serialize_blocks)
 * Custom action hooks for migration phases
 * Pattern-based content generation
 * External API integration for booking systems
 * Batch processing with progress logging
 * URL transformation for staging-to-production deployment

This migration approach transformed a legacy system into a modern, maintainable 
block-based architecture while preserving all existing functionality and user experience.

Whether you’re dealing with custom widgets, legacy meta fields, or complex content
structures, the principles demonstrated here can be adapted for your specific migration
needs.

**Next Steps**
Consider this approach when facing similar challenges, and remember:
successful migrations require planning, testing, and patience. The block editor’s
flexibility makes it worth the effort.

**Final thought**
I still have further to go on [migrating legacy WordPress content to the Block Editor](https://elliottrichmond.co.uk/migrating-legacy-wordpress-content-to-the-block-editor-a-real-world-case-study)
so if you want to follow the full journey of this migration from a Classic theme
to a modern block-based setup, subscribe to my [YouTube channel](https://www.youtube.com/@elliottrichmondwp)
and check out the [dedicated ](https://youtube.com/playlist?list=PL837DkQZeKd69oicYKtiW7rFDu_Svdqx2&si=mBd756EQhB1FcGth)
[playlist](https://youtube.com/playlist?list=PL837DkQZeKd69oicYKtiW7rFDu_Svdqx2&si=mBd756EQhB1FcGth).
I’m documenting the whole process step by step, warts and all!

### Share this:

 * [  Facebook ](https://elliottrichmond.co.uk/migrating-legacy-wordpress-content-to-the-block-editor-a-real-world-case-study/?share=facebook)
 * [  X ](https://elliottrichmond.co.uk/migrating-legacy-wordpress-content-to-the-block-editor-a-real-world-case-study/?share=x)

### Like this:

Like Loading…
