WordPress to Next.js Migration: The Complete 2026 Guide
Asad Ali
Founder & Lead Developer · Former WordPress Core Contributor
WordPress to Next.js Migration: The Complete 2026 Guide
Migrating from WordPress to Next.js is one of the most impactful upgrades you can make for your website. This comprehensive guide walks you through every step of the process.
Why Migrate to Next.js?
Before diving into the how, let's quickly review the why:
| Aspect | WordPress | Next.js |
| Performance | 2-5s load times (typical) | 0.3-1.5s load times |
| Security | Constant vigilance required | Secure by architecture |
| Hosting Cost | $15-100/month | Free-$20/month |
| Maintenance | Weekly updates needed | Minimal |
| Scalability | Expensive scaling | Automatic via CDN |
Migration Overview
The migration process has six main phases:
1. Planning - Audit your site, set goals
2. Content Export - Get your data out of WordPress
3. Design Rebuild - Recreate your look in React
4. Feature Replacement - Handle plugins and functionality
5. SEO Preservation - Maintain your rankings
6. Deployment - Go live on modern hosting
Let's cover each phase in detail.
Phase 1: Planning
Audit Your Current Site
Before migrating, document what you have:
Content Inventory:
- [ ] Number of posts
- [ ] Number of pages
- [ ] Custom post types
- [ ] Categories and tags
- [ ] Authors
- [ ] Media files (count and size)
Feature Inventory:
- [ ] Active plugins and their purpose
- [ ] Contact forms
- [ ] E-commerce functionality
- [ ] User accounts/memberships
- [ ] Comments system
- [ ] Search functionality
- [ ] Multi-language support
Technical Details:
- [ ] Current URL structure
- [ ] Custom fields (ACF, etc.)
- [ ] Custom theme modifications
- [ ] Database size
Set Migration Goals
Define what success looks like:
- Performance target: PageSpeed score of 90+
- Timeline: 2-4 weeks typical for small sites
- Budget: $0 (DIY) to $5,000+ (agency)
- URL preservation: Keep same URLs for SEO
Phase 2: Content Export
Method 1: WordPress REST API (Recommended)
The REST API gives you structured access to all content:
// Fetch all posts
const response = await fetch('https://your-site.com/wp-json/wp/v2/posts?per_page=100');
const posts = await response.json();
// Each post contains:
// - title, content, excerpt
// - slug, date, modified
// - categories, tags
// - featured_media ID
// - author ID
Method 2: Built-in WordPress Export
1. Go to Tools → Export in WordPress admin
2. Select All content
3. Download the WXR (XML) file
This file can be parsed to extract content.
Method 3: Use Our Free Tool
LeaveWP Migration Tool automates the entire process:
1. Connect your WordPress site
2. Select what to migrate
3. Download Next.js-ready MDX files
No coding required for the export step.
Converting to MDX Format
Your content needs to become MDX files:
---
title: "Your Post Title"
date: "2026-01-15"
author: "Author Name"
excerpt: "Post excerpt here"
categories: ["Category 1", "Category 2"]
featuredImage: "/images/post-image.jpg"
Your Post Title
Your post content in Markdown...
More content...
Handling Media
Options for your media files:
1. Download and include: Host images with your site
2. Keep on WordPress: Use WordPress as image CDN (temporary)
3. Move to Cloudinary: Professional image hosting
4. Use Vercel Image: Built-in optimization
Recommended: Download images and use Next.js Image component for automatic optimization.
Phase 3: Design Rebuild
Start with a Template
Don't start from scratch. Use these as starting points:
- Next.js Blog Starter: Official Next.js template
- Taxonomy: Blog template with Tailwind
- Chronark: Minimal blog template
- Our LeaveWP templates: Pre-built themes
Core Layout Structure
A typical Next.js blog structure:
app/
├── page.tsx # Homepage
├── blog/
│ ├── page.tsx # Blog listing
│ └── [slug]/
│ └── page.tsx # Individual posts
├── about/
│ └── page.tsx # About page
├── contact/
│ └── page.tsx # Contact page
└── layout.tsx # Root layout with header/footer
Creating the Blog Post Page
// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPosts } from '@/lib/posts';
import { MDXContent } from '@/components/mdx-content';
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return (
{post.title}
);
}
Styling Approaches
Option 1: Tailwind CSS (Recommended)
Option 2: CSS Modules
import styles from './blog-post.module.css';
Option 3: styled-components
const Article = styled.article
max-width: 65ch;
margin: 0 auto;
;
Phase 4: Feature Replacement
Contact Forms
WordPress: Contact Form 7, Gravity Forms, WPForms
Next.js Alternatives:
1. Formspree (Recommended for simplicity)
2. React Hook Form + API Route
// Handle form submission via your own API route
const onSubmit = async (data) => {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(data)
});
};
Learn more about contact forms on static sites →
Comments
WordPress: Native comments, Disqus
Next.js Options:
1. Giscus (GitHub-based, free)
repo="your-username/your-repo"
repoId="YOUR_REPO_ID"
category="Comments"
categoryId="YOUR_CATEGORY_ID"
mapping="pathname"
/>
2. Disqus (Still works on static sites)
3. Custom with database (More complex)
Search
WordPress: Built-in or plugins like SearchWP
Next.js Options:
1. Pagefind (Static search, free)
2. Algolia (Powerful, has free tier)
3. Fuse.js (Client-side fuzzy search)
Newsletter Signup
Keep your existing email provider (Mailchimp, ConvertKit, etc.) and embed their form:
Analytics
Replace Google Analytics plugin with direct implementation:
// app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google';
export default function RootLayout({ children }) {
return (
{children}
);
}
Or use privacy-friendly alternatives like Plausible or Fathom.
Phase 5: SEO Preservation
👉 For a detailed SEO checklist, see our complete SEO migration guide.
This is critical for maintaining your Google rankings.
Preserve URL Structure
Your WordPress URLs need to match in Next.js:
| WordPress | Next.js |
/blog/post-title/ | /blog/[slug]/page.tsx |
/category/tech/ | /category/[slug]/page.tsx |
/author/john/ | /author/[slug]/page.tsx |
Metadata Implementation
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({ params }): Promise {
const post = await getPostBySlug(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.featuredImage],
type: 'article',
publishedTime: post.date,
},
};
}
Sitemap Generation
Create an automated sitemap:
// app/sitemap.ts
import { getAllPosts } from '@/lib/posts';
export default async function sitemap() {
const posts = getAllPosts();
const postUrls = posts.map((post) => ({
url: https://your-domain.com/blog/${post.slug},
lastModified: post.modified,
}));
return [
{ url: 'https://your-domain.com', lastModified: new Date() },
{ url: 'https://your-domain.com/about', lastModified: new Date() },
...postUrls,
];
}
Robots.txt
// app/robots.ts
export default function robots() {
return {
rules: [
{ userAgent: '*', allow: '/' },
],
sitemap: 'https://your-domain.com/sitemap.xml',
};
}
Set Up Redirects
If any URLs changed, set up redirects in next.config.js:
module.exports = {
async redirects() {
return [
{
source: '/old-url',
destination: '/new-url',
permanent: true, // 301 redirect
},
];
},
};
Structured Data (Schema)
Add JSON-LD schema for blog posts:
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.date,
dateModified: post.modified,
author: {
'@type': 'Person',
name: post.author,
},
image: post.featuredImage,
};
return (
<>