WordPress to Contentful Migration: A Composite Case Study

Quick disclaimer before we start: nobody in this story is real. It's a composite, stitched together from the kind of mid-market WordPress-to-Contentful projects that all start to blur together after you've sat through enough of them. The scenario, the ranges, the details, all generalized from patterns I keep running into. So the lessons hold up, and no actual client gets to see their own mess described in print (you're welcome).

Here's a small horror you don't see coming: you open a freshly migrated blog post, one that looked perfect in the page count, and sitting there in the middle of a paragraph, in plain readable text, is [cta id="7"]. Not a button. Not a form. Just the raw shortcode, printed like a typo the whole internet can see. Scroll down and there's a keeping it company. Somewhere a decade of WordPress is quietly laughing at you.

I keep coming back to this move because everyone describes it as a clean win on paper. Decouple the front end, model the content once, syndicate everywhere, hit your Core Web Vitals targets. All true, all worth wanting. But the first cut of these migrations breaks in ways that are entirely predictable once you've watched a few of them, and the reason is almost never the part anyone's watching. So let's walk through a composite mid-market company, roughly 3,000 to 6,000 URLs and a decade of accretion, and look at what actually broke, what saved it, and what I'd do differently before touching a single export.

Context

Picture a mid-market B2B software company: roughly 3,000–6,000 published URLs, a marketing site plus a busy blog and resource library, and a decade of WordPress accretion. Over the years the team had bolted on page builders (think a mix of shortcodes and a visual builder), a dozen plugins for SEO, forms, and redirects, and a theme that had been forked twice. Editors were productive but the front end was slow, security patching was constant, and every new campaign template required a developer.

The move to Contentful was driven by three things: a desire for a decoupled front end (a React/Next.js rebuild) to hit Core Web Vitals targets, structured content that could be reused across web, email, and an emerging mobile app, and governance: the CMO wanted content modeled once and syndicated, not copy-pasted. On paper it was a clean win. In practice, the first cut of the migration broke in ways that are entirely predictable once you have seen a few of these.

What broke

Redirects and URL structure

WordPress had been generating and storing redirects in a plugin's database table, not in a portable file. Nobody had a canonical export. The initial content sync moved posts and pages but silently dropped the several thousand accumulated 301s, plus the automatic redirects WordPress created every time a slug changed. On top of that, the new Next.js routing changed URL patterns /blog/2019/03/post-title/ became /resources/post-title. Without a mapping, dated permalinks, ?p=123 query-string URLs, attachment pages, and paginated archive URLs all 404'd. Early crawls after the staging launch showed a meaningful share of previously indexed URLs returning errors.

Content model mismatches

The biggest surprise was that WordPress content is not structured. It is a blob of HTML in post_content. Page-builder layouts, inline styles, embedded shortcodes, and figure markup were all baked into that blob. Contentful expects clean, typed fields and references. The team's first model was too rigid (a single Rich Text field per page), which just re-imported the mess, and their second attempt was too granular (dozens of tiny content types) which editors hated. Shortcodes like [cta id="7"] and came across as literal text. Embedded YouTube iframes, custom fields from ACF, and author bios stored in user meta needed bespoke transforms that the generic importer never touched.

Lost SEO and AEO signals

The SEO plugin had stored per-page titles, meta descriptions, canonical overrides, Open Graph data, noindex flags, and schema settings in post meta. None of that is visible in a standard WXR/XML export, so the first import shipped pages with auto-generated titles and no descriptions. Structured data (Article, FAQ, Breadcrumb, Organization) that the plugin had injected disappeared entirely, taking rich results and answer-engine eligibility with it. Canonical tags reset to self-referential defaults, which threatened to re-expose duplicate tag and category pages. Internal links written as absolute WordPress URLs now pointed at the dead domain path structure.

Taxonomy gaps

WordPress categories and tags are flexible to a fault. This site had 40+ categories, hundreds of tags, and heavy many-to-many tagging. Contentful models taxonomy explicitly, usually as reference fields to a topic or category content type. The import flattened tags into a comma-separated text field, breaking topic hub pages and faceted filtering. Hierarchical categories (parent/child) lost their nesting, and several tags were near-duplicates ("AI", "artificial-intelligence", "a.i.") that nobody had reconciled.

What worked

The recovery came from treating migration as a content transformation project, not a data copy. The team paused, exported everything to an intermediate format, and rebuilt in phases.

A URL inventory first. They crawled the live site, pulled Search Console's top pages by clicks and impressions, and dumped the redirect table via direct database query. That produced a single mapping spreadsheet of old URL → new URL, prioritized by traffic and links.

An editor-tested content model. They ran a card-sorting session with editors and settled on a mid-grained model: a handful of reusable components (Hero, RichText, CTA, MediaBlock, FAQ) composed on flexible page types. Shortcodes were parsed into these components programmatically.

Meta migrated as first-class fields. SEO title, meta description, canonical, robots directive, and a structured-data type became explicit Contentful fields, populated from the extracted post meta and rendered by the front end.

Taxonomy as references. Tags were deduplicated and mapped to a governed topic content type with defined parent relationships, restoring hub pages and filters.

A staging crawl gate. No production cutover until a full crawl showed redirect coverage, correct canonicals, and no orphaned high-value pages.

Lessons

WordPress content is unstructured HTML. Budget most of your effort for parsing post_content, shortcodes, and page-builder markup into typed fields, not for the export itself.

SEO lives in post meta, not the export. Titles, descriptions, canonicals, robots flags, and schema settings must be extracted from plugin tables deliberately or they vanish.

Own your redirects before you touch anything. Export the redirect table, add slug-change history, and account for dated permalinks, attachment pages, feeds, and pagination.

Model with editors, not just developers. Too rigid re-imports the mess; too granular kills productivity. Aim for reusable mid-grained components.

Taxonomy is a data model, not a tag cloud. Deduplicate and map to references before import, or you lose hubs and faceted navigation.

Rebaseline expectations. A temporary ranking dip during recrawl is normal; the risk is permanent loss from unmapped URLs and missing signals.

The projects that go smoothly are the ones that inventory what exists before deciding where it goes. Pre-migration visibility is exactly the gap tools like Replatform Radar exist to close.

Before you make this move

Export the full URL list plus Search Console top pages and the redirect table, reconcile into one mapping sheet.

Extract SEO meta and structured data from plugin/database tables, not just the WXR file.

Audit and deduplicate categories and tags; define the target taxonomy as Contentful references.

Prototype the content model with two or three real, messy pages before bulk import.

Write a shortcode/page-builder parsing map for every builder and shortcode in use.

Rewrite internal links to relative paths during transform.

Gate production cutover on a staging crawl that verifies redirects, canonicals, and no orphaned high-value URLs.

Plan a post-launch monitoring window for 404s, crawl errors, and index coverage.

So what do I actually think? Stop calling this a data copy. The teams that come out clean are the ones that treat it as a content transformation project, and they earn that by doing the boring inventory first: crawl the live site, pull the redirect table out of the plugin's database by hand, extract the SEO meta and schema that never show up in the standard export, and reconcile the whole tangled tag pile into a real taxonomy before anything moves. If I could only insist on one thing, it would be the staging crawl gate. No production cutover until a full crawl proves your redirects cover, your canonicals are right, and no high-value page has been orphaned. Everything else you can fix in flight; unmapped URLs and vanished signals are the ones that don't come back on their own.

Because the real lesson isn't that shortcode printing itself across your page. That one's embarrassing but obvious, and someone spots it in an afternoon. It's the redirect nobody exported and the meta description that quietly went blank, sitting there polite and invisible while your rankings drain. Inventory what exists before you decide where it goes, and you'll never have to read [cta id="7"] on your own homepage to find out what you missed.