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-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:
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:
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.

… classically renders like this:

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.

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:

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:

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.

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.

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.

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 conversionconvert_house_photos
– Photo gallery migrationadd_thumbnail_featured_image
– Single post thumb migrationcleanup_house_sub_pages
– Rollback migrationrun_widgets_to_patterns
– Bulk widget transformationsetup_house_sub_pages
– Sub page migrationsetup_house_availability_calendar
– Booking integrationappend_houses_gallery_to_gallery_pages
– Gallery page migrationrun_cleanup_patterns
– Content cleanup utilities
Takeaways for Your Migration
- Audit First: Document all your legacy content structures
- Build Incrementally: Start with simple widgets, add complexity gradually
- Test Extensively: Run migrations on staging with real data
- Plan Rollbacks: Build cleanup functions for each migration step
- 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 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 and check out the dedicated playlist. I’m documenting the whole process step by step, warts and all!
Leave a Reply