WordPress to Next.js: A Real-World Migration Playbook
I'm about to migrate 20+ WordPress sites to Next.js for Lift Legal Marketing. Here's the battle-tested playbook I'm using — practical steps, real code, and hard-won lessons.
WordPress powers 43% of the web. It also powers approximately 100% of my client headaches — plugin conflicts, slow load times, security vulnerabilities that keep you up at night, and a development experience that makes you question your career choices.
I'm about to migrate 20+ WordPress sites for Lift Legal Marketing's law firm clients to modern Next.js applications. This isn't a theoretical guide — it's the actual playbook I'm using. Battle-tested, opinionated, and designed to get you from WordPress to Next.js without destroying your SEO or losing your mind.
Why Migrate? (The Real Reasons)
Let me be blunt about why WordPress-to-Next.js migrations are worth the effort:
Performance. A typical WordPress site I've audited loads in 3-4 seconds. The same content on Next.js? Under 1 second. That's not a marginal improvement — it's the difference between a visitor staying and bouncing.
Security. WordPress sites get attacked constantly. Plugin vulnerabilities, brute force login attempts, SQL injection via outdated themes. A static Next.js site on Vercel has a dramatically smaller attack surface.
Developer experience. Working in WordPress means PHP, a 20-year-old template system, and an admin panel that fights you at every turn. Next.js means TypeScript, React components, and hot reload that actually works.
Cost. WordPress hosting that doesn't suck costs $20-50/month. Vercel's free tier handles most sites comfortably. Do the math across 20+ client sites.
WordPress: $30/mo × 20 sites = $600/month in hosting alone
Next.js: $0/mo × 20 sites = $0/month (Vercel free tier)
Annual savings: $7,200+ 💰
Phase 1: The Audit (Don't Skip This)
Before you touch any code, audit the existing WordPress site. I learned this the hard way on an early project where I started migrating before understanding the full scope. Never again.
Here's my audit checklist:
Content inventory:
- Total pages and posts (check custom post types too)
- Media library size and file types
- Which content is actually getting traffic vs. dead weight
Plugin audit:
- List every active plugin and what it does
- Categorize: essential functionality vs. replaceable with code vs. unnecessary
- Check for plugins that modify database schema (these are the tricky ones)
SEO baseline:
- Current Google Search Console rankings
- Existing sitemap structure and URL patterns
- Backlink profile (which URLs are other sites linking to?)
- Yoast/RankMath SEO data (titles, descriptions, focus keywords)
Third-party integrations:
- Contact forms and where submissions go
- Analytics setup (GA4, GTM containers)
- Payment processors, membership systems, booking tools
Audit output example:
├── 47 blog posts (38 with traffic)
├── 12 practice area pages
├── 156 media files (89MB total)
├── 23 active plugins (14 replaceable)
├── Yoast SEO data for all pages
├── Contact Form 7 → 3 forms
└── GA4 + GTM tracking
This audit takes half a day per site. It saves you from nasty surprises mid-migration.
Phase 2: Content Extraction
WordPress gives you two main options for getting your content out:
Option A: WXR Export — WordPress's built-in XML export under Tools > Export. Quick, includes everything, but the format is gnarly XML with embedded HTML.
Option B: REST API — Cleaner for programmatic processing, especially for large sites.
For the Lift Legal migrations, I'm using the REST API approach with a custom Node.js extraction script:
# Fetch all posts with pagination
curl "https://clientsite.com/wp-json/wp/v2/posts?per_page=100&page=1" \
-H "Authorization: Bearer YOUR_APP_PASSWORD"
# Fetch all pages
curl "https://clientsite.com/wp-json/wp/v2/pages?per_page=100"
# Fetch media library metadata
curl "https://clientsite.com/wp-json/wp/v2/media?per_page=100"
The extraction script pulls posts, pages, media metadata, categories, tags, and SEO data (via the Yoast REST API extension) into a clean JSON structure.
Pro tip: Don't forget to grab images separately. The REST API gives you media URLs, but you need to actually download the files. I use a simple download script that fetches each media URL and saves it to public/uploads/.
Phase 3: Content Transformation (WordPress HTML → MDX)
This is where it gets interesting. WordPress content is a mess of Gutenberg block comments, shortcodes, and HTML artifacts. You need to transform it into clean MDX.
Here's my transformation pipeline:
WordPress HTML
↓ Strip Gutenberg comments (<!-- wp:paragraph --> etc.)
↓ Convert shortcodes to React components
↓ Transform HTML to Markdown (using turndown)
↓ Clean up artifacts and fix formatting
↓ Generate frontmatter from post metadata
↓
Clean MDX file
The key libraries:
fast-xml-parser— for parsing WXR exportsturndown— for HTML-to-Markdown conversiongray-matter— for generating frontmatter
The trickiest part is shortcode conversion. WordPress shortcodes like [contact-form-7 id="123"] need to become React component references like <ContactForm />. I maintain a mapping file that handles the common ones:
const shortcodeMap: Record<string, string> = {
'contact-form-7': '<ContactForm />',
'gallery': '<ImageGallery />',
'embed': '<VideoEmbed />',
'accordion': '<Accordion />',
}
For the 20+ Lift Legal sites, most use the same set of plugins, so this mapping works across all of them. Efficiency through standardization.
Phase 4: URL Redirects (This Is Where Migrations Die)
Listen carefully: URL redirects are the single most important part of a WordPress-to-Next.js migration. Get this wrong and you lose your Google rankings. I've seen agencies destroy months of SEO work because they didn't handle redirects properly.
WordPress uses URL patterns like:
/category/practice-area/post-name//?p=123(legacy numeric URLs)/author/username//tag/keyword/
Your Next.js site probably uses:
/blog/post-name/services/service-name
Every old URL needs a permanent (301) redirect to the new URL. In next.config.ts:
async redirects() {
return [
// Category-based blog URLs → flat blog URLs
{
source: '/category/:category/:slug/',
destination: '/blog/:slug',
permanent: true,
},
// Legacy numeric URLs (generate from audit)
{
source: '/',
has: [{ type: 'query', key: 'p', value: '123' }],
destination: '/blog/specific-post-slug',
permanent: true,
},
// Author pages → about page
{
source: '/author/:username',
destination: '/about',
permanent: true,
},
]
}
For large sites: Don't write these by hand. I built a script that reads the WordPress URL structure and generates the redirect array automatically. For 20+ sites, this alone saves days of tedious work.
Phase 5: Replace Plugins with Code
This is the satisfying part. All those WordPress plugins that cause conflicts and security issues? Most of them are replaceable with a few lines of Next.js code.
WordPress Plugin → Next.js Replacement
─────────────────────────────────────────────────
Yoast SEO → generateMetadata() + app/sitemap.ts
Contact Form 7 → React form + Server Action
WP Super Cache → Next.js ISR + Vercel Edge Cache
UpdraftPlus Backups → Git + Vercel deployment history
Wordfence Security → Not needed (static site)
WP Rocket → Not needed (Next.js optimizes by default)
Akismet Anti-Spam → Turnstile/reCAPTCHA on form
Google Analytics plugin → Next.js Script component
Schema/Structured Data → JSON-LD in generateMetadata()
The result? Zero plugin update anxiety. No more "your site has 7 plugin updates available" emails. No more breaking changes from a plugin author who decided to pivot their business model.
Phase 6: SEO Preservation
Every WordPress page with Yoast/RankMath data needs that SEO metadata preserved in the Next.js version.
I extract the SEO data during the content extraction phase and map it to MDX frontmatter:
---
title: "Personal Injury Lawyer in Manila"
description: "Experienced personal injury attorneys serving Manila and surrounding areas. Free consultation. No fee unless we win."
keywords: ["personal injury lawyer", "Manila attorney"]
ogImage: "/images/practice-areas/personal-injury.jpg"
---
Then in the page component:
export async function generateMetadata({ params }: Props) {
const post = await getPostBySlug(params.slug)
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
images: [post.ogImage],
},
}
}
Don't forget the sitemap. Next.js makes this easy with app/sitemap.ts — no plugin needed.
The Results
Here's what a typical WordPress-to-Next.js migration delivers for a Lift Legal client site:
Metric WordPress Next.js Improvement
────────────────────────────────────────────────────────
PageSpeed Score 52 97 +87%
Time to First Byte 1.8s 180ms -90%
Largest Paint 4.2s 1.1s -74%
Monthly Hosting $30 $0 -100%
Plugin Updates Weekly Never ∞
Security Patches Monthly N/A ∞
Multiply that across 20+ sites and the impact is massive — both for client satisfaction and for the bottom line.
My Migration Timeline (Per Site)
With AI-assisted development using Claude Code, here's my realistic timeline per site:
Day 1 (Morning): Audit + content extraction
Day 1 (Afternoon): Content transformation + site scaffolding
Day 2 (Morning): Custom components + redirect setup
Day 2 (Afternoon): SEO verification + QA testing
Day 2 (Evening): Client review + deployment
2 days per site. For 20+ sites, that's about 6-8 weeks total — compared to 20-40 weeks doing it the traditional way.
The AI acceleration comes from Claude Code handling the repetitive parts: generating page structures, writing redirect rules, creating SEO metadata functions, and scaffolding component boilerplate. I focus on the custom stuff — unique design elements, client-specific features, and quality assurance.
Should You Migrate?
Not every WordPress site needs to migrate. If your site is a simple blog with minimal traffic and you're comfortable with WordPress, keep it.
But if you're dealing with:
- Performance issues that plugins can't fix
- Security concerns that keep you up at night
- A growing content library that needs better organization
- Multiple sites that are expensive to host and maintain
- Clients who expect modern, fast web experiences
Then yeah, it's time to migrate. And with the right toolchain — Next.js + MDX + Vercel + Claude Code — it's faster than you think.
Hit me up if you want to talk about your migration. I've got the playbook, and I'm always happy to help a fellow developer avoid the pitfalls I've already navigated.