Migrating from WordPress to Headless CMS
WordPress to headless CMS migration isn't just a platform change. It's an architecture review: separating content storage from frontend, changing editor workflow, different deployment. Requires planning, phased implementation and parallel operation period.
Pre-Migration Assessment
Before choosing target CMS, answer:
- How many content types (Custom Post Types) and fields (ACF)?
- Are plugins with custom logic used (WooCommerce, Events Calendar)?
- What technical competency level has content team?
- What's budget for licenses (Contentful, Sanity) vs. self-hosted?
- Do you need draft and preview support in new CMS?
Target Platform Comparison
| Criterion | Contentful | Sanity | Strapi | KeystoneJS |
|---|---|---|---|---|
| Hosting | SaaS | SaaS/Self | Self | Self |
| License | From $300/mo | From $15/mo | Open Source | Open Source |
| Editor | Good | Excellent | Basic | Basic |
| API | REST + GraphQL | GROQ + GraphQL | REST + GraphQL | GraphQL |
| Media | CDN included | CDN included | Own storage | Own |
Stage 1: Content Audit and Mapping (1–2 weeks)
Inventory existing content:
# Export from WordPress via WP-CLI
wp export --post_type=post,page,product --status=publish --path=/var/www/html
# Analyze ACF fields
wp acf field-group export --group_id=all --output=json > acf-fields.json
# Statistics by type
wp post list --post_type=post --format=count
wp post list --post_type=page --format=count
WordPress → target CMS mapping:
# mapping.yaml
wordpress_types:
post:
target: blogPost
fields:
post_title: title
post_content: body (RichText)
post_excerpt: excerpt
post_date: publishedAt
_thumbnail_id: featuredImage (Asset)
categories: categories (Reference[])
tags: tags (Reference[])
acf.seo_title: seoTitle
acf.seo_description: seoDescription
product:
target: product
fields:
post_title: name
_regular_price: price (Number)
_stock_qty: stock (Number)
product_cat: categories
acf.gallery: gallery (Asset[])
Stage 2: New CMS Setup (1 week)
Create Content Types in target CMS exactly per mapping. Configure validations, localization, roles.
Stage 3: Migration Script (1–2 weeks)
// scripts/migrate-from-wp.ts
import axios from 'axios';
import * as contentful from 'contentful-management';
import TurndownService from 'turndown';
const turndown = new TurndownService({ headingStyle: 'atx' });
const cmaClient = contentful.createClient({ accessToken: process.env.CMA_TOKEN! });
async function migratePosts() {
const space = await cmaClient.getSpace(process.env.SPACE_ID!);
const env = await space.getEnvironment('master');
// Get posts from WP REST API
let page = 1;
while (true) {
const { data: posts } = await axios.get(
`${WP_URL}/wp-json/wp/v2/posts?per_page=100&page=${page}&_embed`
);
if (!posts.length) break;
for (const wpPost of posts) {
await migratePost(env, wpPost);
await delay(200); // Rate limiting
}
page++;
}
}
async function migratePost(env: any, wpPost: any) {
const featuredImageUrl = wpPost._embedded?.['wp:featuredmedia']?.[0]?.source_url;
let imageAsset;
if (featuredImageUrl) {
imageAsset = await uploadAsset(env, featuredImageUrl, wpPost.title.rendered);
}
const entry = await env.createEntry('blogPost', {
fields: {
title: { 'en-US': wpPost.title.rendered },
slug: { 'en-US': wpPost.slug },
body: { 'en-US': turndown.turndown(wpPost.content.rendered) },
excerpt: { 'en-US': wpPost.excerpt.rendered.replace(/<[^>]*>/g, '') },
publishedAt: { 'en-US': wpPost.date },
...(imageAsset && {
featuredImage: { 'en-US': { sys: { type: 'Link', linkType: 'Asset', id: imageAsset.sys.id } } },
}),
},
});
await entry.publish();
console.log(`Migrated: ${wpPost.title.rendered}`);
}
Stage 4: Media Files Migration
// Download and upload all WP media files
async function migrateMedia() {
const { data: media } = await axios.get(`${WP_URL}/wp-json/wp/v2/media?per_page=100`);
for (const item of media) {
const asset = await env.createAsset({
fields: {
title: { 'en-US': item.title.rendered },
description: { 'en-US': item.alt_text },
file: { 'en-US': {
contentType: item.mime_type,
fileName: path.basename(item.source_url),
upload: item.source_url,
}},
},
});
await asset.processForAllLocales();
await asset.publish();
mediaIdMap[item.id] = asset.sys.id; // for references
}
}
Stage 5: Parallel Launch and Switch
- Launch new frontend on staging with real data
- Conduct design review with content team
- Set up automatic WordPress → new CMS sync for transition period
- DNS switch during low-traffic period
- Disable WordPress 2–4 weeks after stabilization
Typical Migration Timeline
| Stage | Small site (<500 items) | Medium (500–5000) | Large (5000+) |
|---|---|---|---|
| Audit and mapping | 1 week | 1–2 weeks | 2–4 weeks |
| CMS setup | 3–5 days | 1 week | 1–2 weeks |
| Migration script | 1 week | 1–2 weeks | 2–4 weeks |
| Testing | 3–5 days | 1 week | 2 weeks |
| Launch | 1 day | 1–2 days | 1 week |
| Total | 4–6 weeks | 6–10 weeks | 3–5 months |







