Table of Contents
Migrating from WordPress to a headless CMS means moving your content into Strapi or Directus, rebuilding the front end in Next.js, and keeping every URL that earns traffic working. Do it when WordPress limits you: several channels need the same content, performance or security is a constant fight, or your content has outgrown posts and pages. Don't do it if WordPress already works and the main complaint is "it feels old."
Below is the playbook we use for a WordPress-to-headless migration: the go/no-go decision, content modeling, the export and transform pipeline, redirects and SEO, preview for editors, rollout, and the pitfalls that catch teams on their first try. If you'd rather hand the whole thing to a team that has done it before, that's what our headless CMS development services cover.
When a headless migration is worth it (and when it isn't)
A headless rebuild replaces a theme, a plugin ecosystem and an admin your editors already know. The real question is whether your specific problems go away once content and presentation are separated.
| Signal | Headless is likely worth it | Stay on WordPress (or fix it in place) |
|---|---|---|
| Channels | Content feeds a website, a mobile app, in-product help, partner feeds or kiosks | One marketing website, and nothing else is planned |
| Content shape | Structured entities (courses, locations, products, specs) crammed into custom fields and shortcodes | Mostly articles and landing pages |
| Performance | You've tuned caching and still can't hit Core Web Vitals because of theme and plugin weight | You haven't tried a lean theme, a CDN and image optimization yet |
| Security and ops | Plugin patching is a recurring incident source, or compliance wants a smaller public attack surface | Managed WordPress hosting already handles updates well |
| Team | You have (or will hire) front-end engineers who own the site as a product | Marketers build pages alone with a page builder, and no developer is on hand |
| Budget and timeline | You can fund a proper rebuild plus a stabilization period | You need a redesign next month on a small budget |
Watch the last two rows. Plenty of headless projects fail because a marketing team that ran on Elementor or Gutenberg blocks suddenly needs a developer for every new layout. If page-building autonomy matters, budget for a block-based "page builder" model in the CMS (covered below) or rethink the move. For a longer head-to-head, see our Strapi vs WordPress comparison.
Strapi or Directus?
Both are open-source, Node.js-based and self-hostable, and both work well behind Next.js. The difference is how they relate to your data:
- Strapi is schema-first. You define content types and components in the admin or in code, and Strapi owns the database schema. Its dynamic zones and reusable components suit the "editors assemble pages from blocks" model that WordPress users expect. It has built-in draft and publish, internationalization, and REST and GraphQL APIs. We're a Strapi Community Partner and built the education discovery platforms Colleges18 and Schools18 on Strapi and Next.js.
- Directus is database-first. It wraps an existing SQL database with an admin app and APIs, and it doesn't hide your tables behind its own abstraction. That makes it a strong fit when content lives next to operational data, or when other systems will query the same database. Its granular roles, permissions and flows (automation) are excellent.
A rough rule: if the migration is mostly marketing and editorial content, Strapi's component model is usually faster to adopt. If you're consolidating WordPress content with product or operational data in one database, lean toward Directus. The Strapi vs Directus comparison goes deeper, and both our Strapi development and Directus development teams can help you choose based on your actual content.
Step 1: Audit and model your content
Don't start by recreating WordPress's schema. WordPress stores almost everything as a "post" with a post_type, a blob of HTML in post_content, and loosely typed key/value pairs in wp_postmeta. A straight copy of that brings the mess along with it.
Run a content inventory first
- List every post type, taxonomy, custom field group (ACF, Meta Box, Pods), menu and options page.
- Pull traffic and backlink data per URL from your analytics and Search Console. That decides what gets migrated with care, what gets merged and what gets retired.
- Catalog shortcodes and Gutenberg blocks by how often they're used. A shortcode used on three pages can be flattened to HTML. One used on 800 pages needs a real component.
- List the plugins that produce front-end behavior: forms, search, SEO, related posts, sitemaps, multilingual, memberships. Each one needs a replacement plan.
Model entities, not pages
Turn implicit structure into explicit types. A "Locations" page with an address in an ACF text field becomes a Location collection with typed fields and a relation to Service. Typical mapping:
| WordPress | Headless model |
|---|---|
| Posts | Article collection: title, slug, excerpt, rich text body, cover image, author relation, category relations, SEO component |
| Pages built with blocks | Page collection with a dynamic zone (Strapi) or a many-to-any builder field (Directus) holding typed blocks: hero, feature grid, CTA, FAQ, rich text |
| Categories and tags | Separate collections with slugs, related many-to-many |
| ACF field groups | Real typed fields or reusable components |
| Users (authors) | An Author collection, kept separate from CMS admin users |
| Yoast/Rank Math meta | A reusable SEO component: meta title, meta description, canonical, noindex flag, OG image |
| Menus and site options | Single types or singleton collections (navigation, footer, global settings) |
Keep the block library small. Every block is a React component to build and maintain.
Step 2: Export and transform the content
You have three ways to get content out: the WordPress REST API, the WXR export file from Tools → Export, or direct SQL against the database. We usually use the REST API for posts, pages and taxonomies (it resolves a lot of relationships for you) and fall back to SQL for plugin data the API doesn't expose.
A minimal paginated fetch from the REST API in Node.js:
async function fetchAll(base, type = 'posts') {
const items = [];
let page = 1;
let totalPages = 1;
do {
const res = await fetch(
`${base}/wp-json/wp/v2/${type}?per_page=100&page=${page}&_embed`
);
if (!res.ok) throw new Error(`WP API ${res.status} on page ${page}`);
totalPages = Number(res.headers.get('X-WP-TotalPages')) || 1;
items.push(...(await res.json()));
page++;
} while (page <= totalPages);
return items;
}
Unauthenticated requests return only published content. If you need drafts, scheduled posts or private pages, authenticate (application passwords work well) and request status=any.
Build the pipeline as repeatable code
Treat the migration as an ETL job you can run again and again, not a one-time import:
- Extract raw WordPress JSON to disk so you can re-run the transform without hitting production.
- Transform each item: map fields, resolve relations by WordPress ID, clean the HTML, and convert shortcodes and Gutenberg blocks into your block structure.
- Load through the target CMS's API (Strapi's REST API or the Directus SDK), media first, then taxonomies and authors, then content that references them.
- Record a mapping table of WordPress ID → new ID → old URL → new URL. You'll need it for redirects, internal links and QA.
The transform is where the time goes
- HTML cleanup: strip inline styles, empty paragraphs, page-builder wrapper divs and
wp-image-*classes. Parse HTML with a real parser, not regex. - Gutenberg blocks: the raw post content stores block delimiters as HTML comments (
<!-- wp:heading -->). Parse them to map core blocks to your components, and decide what to do with third-party blocks. - Shortcodes: convert high-frequency ones to structured blocks, render low-frequency ones to static HTML, and log anything you can't handle.
- Media: download originals (not resized variants), upload them to the new media library or object storage, and rewrite every
srcandsrcsetin body content. Keep alt text. - Internal links: rewrite absolute links to your own domain, including old permalink formats and
?p=123links, using the mapping table. - Rich text format: decide early whether bodies are stored as HTML, Markdown or the CMS's block JSON. Converting everything to a structured format gives you cleaner rendering but costs more transform effort.
Run the pipeline against staging several times. Each run should report unmapped shortcodes, broken images and failed relations, and that list should reach zero (or agreed exceptions) before cutover.
Step 3: Preserve URLs, redirects and SEO
This is where migrations lose traffic. The goal: every URL that had traffic or backlinks either still resolves with the same content or permanently redirects to its closest equivalent in one hop.
Keep URLs where you can
The best redirect is the one you don't need. If your posts live at /blog/my-post/, build the Next.js routes to match, trailing slash included. Next.js has a trailingSlash option in next.config.js. Set it to match your WordPress permalinks so you don't create site-wide redirects by accident.
Redirect the rest
For pattern-based changes, such as dropping date-based permalinks, use Next.js redirects:
// next.config.js
module.exports = {
trailingSlash: true,
async redirects() {
return [
{
source: '/:year(\\d{4})/:month(\\d{2})/:slug',
destination: '/blog/:slug',
permanent: true,
},
{
source: '/category/:slug',
destination: '/blog/category/:slug',
permanent: true,
},
];
},
};
permanent: true returns a 308, which search engines treat as a permanent redirect just like a 301. For hundreds or thousands of one-off redirects, don't bloat the config. Store them in a Redirect collection in the CMS (so editors can manage them after launch) and resolve them in middleware or at the edge, or export them to your host's or CDN's redirect rules at build time.
SEO parity checklist
- Meta titles, descriptions and canonicals migrated per URL from Yoast or Rank Math fields, not regenerated.
- Open Graph and Twitter card images carried over.
- Structured data (Article, Breadcrumb, FAQ, Organization) re-implemented in the Next.js templates.
- An XML sitemap generated from the CMS, and your robots rules reproduced. The App Router has
sitemapandrobotsfile conventions for this. noindexflags preserved for thin or private pages.- Pagination, category and author archives: decide whether each is kept, redirected or retired on purpose.
- RSS feed and hreflang reproduced where they exist.
- Staging blocked from indexing, and that block removed at launch. Forgetting this second step is common.
Before launch, crawl the old site and the staging build with the same crawler and diff the results: status codes, titles, canonicals, H1s and word counts. Then run every old URL from your mapping table against staging and assert a 200 or a single-hop 301/308.
Step 4: Preview and the editorial workflow
Editors lose the most in a headless move, and poor preview is why they resent the new system. Treat it as a first-class feature.
- Draft preview: both Strapi and Directus support draft content and configurable preview URLs. On the Next.js side, use Draft Mode. A secret-protected route handler turns it on, and pages then fetch draft content instead of published content.
- Publish-triggered revalidation: use a CMS webhook on publish to call a Next.js route that runs
revalidateTagorrevalidatePath. Pages then update in seconds without a full rebuild. - Roles: map WordPress roles (author, editor, admin) to CMS roles with matching permissions, and add a review step if you had one.
- Scheduling: confirm how scheduled publishing works in your CMS before promising it. It may need a cron job or automation flow.
A minimal Draft Mode route handler in the App Router:
// app/api/preview/route.js
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const secret = searchParams.get('secret');
const slug = searchParams.get('slug');
if (secret !== process.env.PREVIEW_SECRET || !slug) {
return new Response('Invalid preview request', { status: 401 });
}
const draft = await draftMode();
draft.enable();
redirect(`/blog/${slug}`);
}
In production, verify the slug exists in the CMS before redirecting, and train editors on staging with real content well before launch. For the rendering and caching side of the architecture, see our guide to Next.js headless CMS architecture.
Step 5: A rollout plan that doesn't bet the domain
- Discovery (1–2 weeks): content inventory, traffic and backlink audit, plugin replacement plan, CMS choice.
- Modeling and design system: content types, the block library and the matching React components.
- Pipeline and front end in parallel: the ETL scripts fill staging while Next.js templates are built against real content, not lorem ipsum.
- QA: the crawl diff, the redirect test suite, Core Web Vitals checks on key templates, form and integration tests, and editor acceptance.
- Content freeze and final sync: freeze WordPress edits (or keep a short delta window), run the pipeline one last time, and spot-check.
- Cutover: switch DNS or the routing layer, remove the staging noindex, submit the new sitemap, and watch 404s and server logs hourly on day one.
- Stabilization (4–8 weeks): watch Search Console coverage and rankings for top pages, fix stragglers, and keep WordPress read-only and backed up until you're confident.
On large sites, consider a section-by-section rollout: a reverse proxy sends /blog/* to Next.js while everything else stays on WordPress, then other sections follow. Lower risk, at the cost of running two stacks for a while.
Typical pitfalls
- Replicating WordPress instead of redesigning the model. You end up with a
Pagetype holding one giant HTML field, which is WordPress without the ecosystem. - Underestimating plugin replacement. Forms, site search, multilingual, memberships and ecommerce each become a separate integration or build.
- Forgetting non-post URLs. Attachment pages, author archives,
/feed/,/wp-content/uploads/image URLs with backlinks, and old PDFs all need a plan. - Redirect chains. Old WordPress redirects plus new ones stack into multi-hop chains. Flatten them.
- Treating preview as a phase-two feature. Editors will push back on launch day.
- No front-end owner after launch. Layout changes now live in code, so someone must own them.
For the front-end build itself, our Next.js development team handles rendering strategy, caching and Core Web Vitals, which is where a headless site either beats the old WordPress install or falls short of it.
FAQ
How long does a WordPress to headless CMS migration take?
It depends mostly on content complexity, not page count. A marketing site with a few post types and clean content can move in weeks. A site with years of shortcodes, page-builder layouts, multilingual content and many plugin-driven features can take several months, including stabilization.
Will I lose SEO rankings when migrating from WordPress to Next.js?
Not if URLs, metadata, structured data and internal links are preserved and every changed URL gets a single permanent redirect. Some ranking movement is normal for a few weeks after any migration. Lasting drops usually trace back to missed redirects, changed content or lost metadata.
Should I migrate WordPress to Strapi or Directus?
Choose Strapi when editors need to compose pages from reusable blocks and you want the CMS to own the schema. Choose Directus when content should live in a SQL database you control, alongside other application data, or when you need fine-grained permissions and built-in automation.
What happens to my WordPress plugins?
They don't carry over. Map each one to a replacement: native CMS features (SEO fields, i18n, roles), Next.js features (sitemaps, metadata, image optimization), third-party services (forms, search), or custom code.
If you're weighing a WordPress-to-headless migration and want a second opinion on scope, CMS choice or SEO risk, our headless CMS migration team can review your current site and outline a realistic plan before you commit to a rebuild.












