WordPress to Next.js Migration on Vercel: A 2026 Guide
I've migrated over a dozen WordPress sites to Next.js in the past three years. Some went smoothly. Some made me question my career choices at 2 AM on a Tuesday. The difference between those two outcomes almost always came down to planning — specifically, understanding what WordPress was actually doing for the site before ripping it out.
This guide is everything I wish someone had handed me before my first migration. We'll cover the full journey: evaluating whether you should even migrate, choosing your headless CMS, moving content, rebuilding templates, handling SEO without losing rankings, and deploying on Vercel with a setup that won't fall over under traffic spikes.
Let's get into it.

Table of Contents
- Why Migrate from WordPress to Next.js in 2026?
- Pre-Migration Audit: What WordPress Is Actually Doing
- Choosing Your Headless CMS Backend
- Content Migration Strategy
- Rebuilding Your Frontend in Next.js 15
- URL Structure and SEO Preservation
- Deploying on Vercel: Configuration That Actually Works
- Performance Benchmarks: Before and After
- Common Migration Pitfalls
- FAQ
Why Migrate from WordPress to Next.js in 2026?
Let's be honest — WordPress still powers roughly 40% of the web in 2026. It's not going anywhere. But the reasons to leave have gotten more compelling:
Performance ceiling. Even with aggressive caching plugins (WP Rocket, W3 Total Cache), most WordPress sites hit a wall around 70-80 on Lighthouse performance scores. Plugin bloat, render-blocking PHP, and database queries on every page load create overhead that no amount of optimization can fully eliminate.
Security surface area. WordPress had 149 documented vulnerabilities in 2025 across core and popular plugins. Every plugin is an attack vector. Every theme update is a potential break. If you're running WooCommerce, the surface area doubles.
Developer experience. If your team knows React, building in PHP templates feels like writing with your non-dominant hand. Next.js 15's App Router, Server Components, and built-in caching give you a modern development workflow that WordPress can't match.
Cost at scale. Managed WordPress hosting (WP Engine, Kinsta) runs $30-$300/month for decent performance. Vercel's Pro plan at $20/user/month with edge functions and automatic scaling often costs less while performing better.
That said — don't migrate just because it's trendy. If your site is a simple blog with 50 posts and your client updates it weekly via the WordPress admin, a migration might create more problems than it solves. The best candidates for migration are:
- Sites with 500+ pages that need better performance
- Teams that want component-based development
- Sites where plugin conflicts are causing maintenance nightmares
- E-commerce sites hitting WooCommerce performance limits
- Marketing sites that need A/B testing and personalization at the edge
Pre-Migration Audit: What WordPress Is Actually Doing
This is where most migrations go wrong. People think they're migrating a blog, but WordPress is actually handling contact forms, redirects, image optimization, search, comments, authentication, cron jobs, and fifteen other things buried in plugin configurations.
Before writing a single line of Next.js code, document everything:
Plugin Inventory
Export your plugin list and categorize each one:
wp plugin list --status=active --format=csv > active-plugins.csv
For each plugin, answer: What does this do, and what replaces it in the Next.js ecosystem?
| WordPress Plugin | Function | Next.js Replacement |
|---|---|---|
| Yoast SEO | Meta tags, sitemaps, schema | next-seo + custom sitemap.xml route |
| WP Rocket | Caching, minification | Vercel Edge Cache + Next.js built-in |
| Contact Form 7 | Form handling | React Hook Form + API route or Formspree |
| Wordfence | Security | Not needed (no admin surface) |
| WPML | Multilingual | next-intl or built-in i18n routing |
| WooCommerce | E-commerce | Shopify Storefront API or Saleor |
| Advanced Custom Fields | Custom content models | Your headless CMS's content modeling |
| Redirection | URL redirects | next.config.js redirects or Vercel config |
| WP Cron | Scheduled tasks | Vercel Cron Jobs or separate service |
| Imagify | Image optimization | next/image with Vercel Image Optimization |
Content Inventory
Count and categorize your content:
SELECT post_type, post_status, COUNT(*)
FROM wp_posts
GROUP BY post_type, post_status;
Don't forget: custom post types, taxonomy terms, user profiles, menu structures, widget configurations, and option values. That "simple" WordPress site probably has more content types than you think.
URL Mapping
Export every URL. Every single one. Use Screaming Frog or a simple sitemap crawl:
curl -s https://yoursite.com/sitemap_index.xml | \
grep -oP '<loc>\K[^<]+' | \
xargs -I {} curl -s {} | \
grep -oP '<loc>\K[^<]+' > all-urls.txt
This file is gold. You'll use it for redirect mapping, SEO preservation, and QA testing after migration.

Choosing Your Headless CMS Backend
You need somewhere to store and manage content. The three most common paths in 2026:
Option 1: WordPress as a Headless CMS
Yes, you can keep WordPress as the backend and use Next.js as the frontend. WPGraphQL (now at v2.1) makes this surprisingly viable. Your editors keep the familiar admin interface. You get a modern frontend.
// lib/wordpress.js
const API_URL = process.env.WORDPRESS_GRAPHQL_URL;
export async function getPostBySlug(slug) {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query PostBySlug($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
date
featuredImage {
node {
sourceUrl
altText
}
}
seo {
title
metaDesc
opengraphImage {
sourceUrl
}
}
}
}
`,
variables: { slug },
}),
next: { revalidate: 60 },
});
const json = await res.json();
return json.data.post;
}
The downside? You still maintain a WordPress installation. Security updates, PHP version management, database backups — it all stays on your plate. And you're still paying for WordPress hosting.
Option 2: Purpose-Built Headless CMS
This is what I recommend for most migrations. Move your content to a CMS that was built from the ground up for API-first delivery.
| CMS | Pricing (2026) | Best For | Content Modeling | API Type |
|---|---|---|---|---|
| Sanity | Free tier, $15/user/mo Pro | Complex content, real-time collaboration | Excellent, code-defined | GROQ + GraphQL |
| Contentful | Free tier, $300/mo Team | Enterprise, large teams | Good, UI-defined | REST + GraphQL |
| Storyblok | Free tier, €106/mo Business | Visual editing, components | Great, visual | REST + GraphQL |
| Strapi v5 | Free (self-hosted), Cloud from $29/mo | Full control, open source | Flexible, UI-defined | REST + GraphQL |
| Payload CMS 3.0 | Free (self-hosted) | Developers who want code-first | Excellent, code-defined | REST + GraphQL |
If your team at Social Animal is handling the migration, we typically recommend Sanity or Payload for headless CMS development — they give developers the most control over content modeling while keeping editors happy.
Option 3: Markdown/MDX in the Repository
For developer blogs and documentation sites, storing content as MDX files in your Git repo is the simplest approach. No CMS to manage, no API calls, content versioned alongside code. But this only works if your content editors are comfortable with Git workflows.
Content Migration Strategy
Exporting from WordPress
The built-in WordPress export (Tools → Export) gives you an XML file. It's a start, but it's messy. For structured migration, I use a custom WP-CLI script:
// export-content.php
<?php
$posts = get_posts([
'post_type' => ['post', 'page', 'your_custom_type'],
'posts_per_page' => -1,
'post_status' => 'publish',
]);
$export = [];
foreach ($posts as $post) {
$export[] = [
'id' => $post->ID,
'title' => $post->post_title,
'slug' => $post->post_name,
'content' => apply_filters('the_content', $post->post_content),
'excerpt' => $post->post_excerpt,
'date' => $post->post_date,
'modified' => $post->post_modified,
'author' => get_the_author_meta('display_name', $post->post_author),
'categories' => wp_get_post_categories($post->ID, ['fields' => 'names']),
'tags' => wp_get_post_tags($post->ID, ['fields' => 'names']),
'featured_image' => get_the_post_thumbnail_url($post->ID, 'full'),
'acf' => function_exists('get_fields') ? get_fields($post->ID) : [],
'yoast' => [
'title' => get_post_meta($post->ID, '_yoast_wpseo_title', true),
'description' => get_post_meta($post->ID, '_yoast_wpseo_metadesc', true),
],
];
}
file_put_contents('export.json', json_encode($export, JSON_PRETTY_PRINT));
Transforming Content
WordPress content is stored as HTML with Gutenberg block markup. You'll need to decide: keep the HTML and render it, or convert to your CMS's structured format?
For Sanity, I use @sanity/block-tools to convert HTML to Portable Text. For Contentful, their migration CLI handles rich text conversion. Either way, budget time for content cleanup — WordPress content is full of [shortcodes], inline styles, and broken HTML that needs to be cleaned up.
// migrate-to-sanity.js
import { htmlToBlocks } from '@sanity/block-tools';
import { JSDOM } from 'jsdom';
import { Schema } from '@sanity/schema';
const schema = Schema.compile({ /* your schema */ });
const blockContentType = schema.get('post')
.fields.find(f => f.name === 'body').type;
function convertPost(wpPost) {
return {
_type: 'post',
title: wpPost.title,
slug: { current: wpPost.slug },
publishedAt: wpPost.date,
body: htmlToBlocks(
wpPost.content,
blockContentType,
{ parseHtml: (html) => new JSDOM(html).window.document }
),
};
}
Image Migration
Don't skip this. Download every image from wp-content/uploads, re-upload to your CMS or a CDN (Cloudinary, Vercel Blob Storage, S3), and update all content references. I typically write a script that:
- Crawls the export JSON for image URLs
- Downloads each image
- Uploads to the new storage
- Creates a URL mapping file
- Runs find-and-replace across all content
Rebuilding Your Frontend in Next.js 15
Next.js 15 (stable since late 2024, with 15.2 current in 2026) uses the App Router by default. Here's the structure I use for content-heavy sites:
app/
├── layout.tsx # Root layout with fonts, analytics
├── page.tsx # Homepage
├── blog/
│ ├── page.tsx # Blog listing with pagination
│ └── [slug]/
│ └── page.tsx # Individual blog posts
├── [slug]/
│ └── page.tsx # Generic pages
├── sitemap.ts # Dynamic sitemap generation
├── robots.ts # robots.txt
└── not-found.tsx # Custom 404
Static Generation with ISR
For most content pages, Incremental Static Regeneration is the sweet spot — static performance with dynamic freshness:
// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPostSlugs } from '@/lib/cms';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const slugs = await getAllPostSlugs();
return slugs.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }) {
const post = await getPostBySlug(params.slug);
if (!post) return {};
return {
title: post.seo?.title || post.title,
description: post.seo?.description || post.excerpt,
openGraph: {
images: [post.featuredImage?.url],
},
};
}
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPost({ params }) {
const post = await getPostBySlug(params.slug);
if (!post) notFound();
return (
<article className="prose lg:prose-xl">
<h1>{post.title}</h1>
<time dateTime={post.date}>{formatDate(post.date)}</time>
<PostBody content={post.body} />
</article>
);
}
For sites needing real-time updates (news, live content), use on-demand revalidation via webhooks from your CMS. Most headless CMS platforms support webhook triggers on content publish.
If you're looking at Next.js development and want to understand the rendering trade-offs more deeply, we've written about that separately.
URL Structure and SEO Preservation
This is non-negotiable. If you lose your URL structure, you lose your search rankings. Period.
Redirect Map
WordPress uses URL patterns like /2024/03/post-slug/ or /category/term/. Your Next.js site probably uses /blog/post-slug. Create a redirect map:
// next.config.js
module.exports = {
async redirects() {
return [
// Date-based URLs to clean slugs
{
source: '/:year(\\d{4})/:month(\\d{2})/:slug',
destination: '/blog/:slug',
permanent: true,
},
// Category archives
{
source: '/category/:slug',
destination: '/blog?category=:slug',
permanent: true,
},
// Pagination
{
source: '/page/:num',
destination: '/blog?page=:num',
permanent: true,
},
// Feed URLs
{
source: '/feed',
destination: '/rss.xml',
permanent: true,
},
];
},
};
For large sites (1000+ redirects), use Vercel's vercel.json configuration or middleware instead — next.config.js redirects have a soft limit of around 1024 entries before build times start suffering.
Structured Data
WordPress plugins like Yoast auto-generate JSON-LD. You need to replicate this:
// components/structured-data.tsx
export function ArticleSchema({ post }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
datePublished: post.date,
dateModified: post.modified,
author: {
'@type': 'Person',
name: post.author,
},
image: post.featuredImage?.url,
publisher: {
'@type': 'Organization',
name: 'Your Site Name',
logo: { '@type': 'ImageObject', url: '/logo.png' },
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
XML Sitemap
Next.js 15 makes dynamic sitemaps straightforward:
// app/sitemap.ts
import { getAllPosts, getAllPages } from '@/lib/cms';
export default async function sitemap() {
const posts = await getAllPosts();
const pages = await getAllPages();
return [
{ url: 'https://yoursite.com', lastModified: new Date() },
...pages.map((page) => ({
url: `https://yoursite.com/${page.slug}`,
lastModified: page.modified,
})),
...posts.map((post) => ({
url: `https://yoursite.com/blog/${post.slug}`,
lastModified: post.modified,
})),
];
}
Deploying on Vercel: Configuration That Actually Works
Project Setup
npx create-next-app@latest my-migrated-site --typescript --tailwind --app
cd my-migrated-site
vercel link
Environment Variables
Set these in Vercel's dashboard, not in .env files committed to Git:
CMS_API_URL=https://your-cms-api-endpoint
CMS_API_TOKEN=your-read-only-token
REVALIDATION_SECRET=a-random-string-for-webhook-auth
SITE_URL=https://yoursite.com
Vercel Configuration
// vercel.json
{
"crons": [
{
"path": "/api/revalidate-sitemap",
"schedule": "0 */6 * * *"
}
],
"headers": [
{
"source": "/fonts/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}
On-Demand Revalidation Webhook
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidation-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
const body = await request.json();
const { slug, type } = body;
if (type === 'post') {
revalidatePath(`/blog/${slug}`);
revalidatePath('/blog'); // Revalidate listing too
} else {
revalidatePath(`/${slug}`);
}
return NextResponse.json({ revalidated: true });
}
Point your CMS webhook at https://yoursite.com/api/revalidate with the secret header. Now content updates appear within seconds without full rebuilds.
Performance Benchmarks: Before and After
These are real numbers from migrations we've completed at Social Animal in 2025-2026:
| Metric | WordPress (WP Engine) | Next.js (Vercel) | Improvement |
|---|---|---|---|
| Lighthouse Performance | 62-78 | 95-100 | +30-40% |
| Largest Contentful Paint | 2.8-4.2s | 0.8-1.4s | 60-70% faster |
| Time to First Byte | 800ms-1.5s | 50-120ms | 90%+ faster |
| Cumulative Layout Shift | 0.12-0.25 | 0.01-0.05 | ~80% reduction |
| Monthly hosting cost | $115/mo avg | $20-40/mo | 60-80% savings |
| Build time (500 pages) | N/A (dynamic) | 45-90 seconds | N/A |
| Pages/second (ISR) | 15-30 req/s | 10,000+ from edge | Orders of magnitude |
The TTFB improvement alone is worth the migration. WordPress generates every page through PHP and MySQL. Vercel serves pre-rendered pages from edge nodes in 300+ locations worldwide.
Common Migration Pitfalls
Pitfall 1: Forgetting WordPress RSS feeds. If people subscribe to your feed, redirect /feed/ and /rss/ to a new RSS endpoint. Next.js doesn't generate feeds by default — you need a custom route.
Pitfall 2: Missing WordPress shortcodes. Content exported from WordPress is riddled with [gallery], [embed], and plugin-specific shortcodes. If you don't parse and convert these, they render as plain text. Write transformers for each shortcode type your content uses.
Pitfall 3: Ignoring WordPress comment data. If you have valuable comment threads, migrate them to a service like Disqus or build a custom comment system. Most people just drop comments, but check with stakeholders first.
Pitfall 4: Not testing internal links. WordPress content is full of internal links using absolute URLs (https://yoursite.com/old-path/). These need to be updated to relative paths or your new URL structure. A simple regex find-and-replace during migration handles most cases.
Pitfall 5: Forgetting wp-content/uploads references. Even after migrating images, old content may reference /wp-content/uploads/2024/03/image.jpg paths. Set up a catch-all redirect or proxy these to your new image CDN.
If this feels overwhelming, that's honestly normal. A proper migration takes 4-12 weeks depending on site complexity. Check out our pricing or reach out directly if you want experienced hands on the project.
FAQ
How long does a WordPress to Next.js migration take? For a site with 100-500 pages, expect 4-8 weeks with a dedicated developer. Larger sites with custom post types, e-commerce, or multilingual content can take 8-16 weeks. The content migration itself is usually 20% of the work — the other 80% is rebuilding templates, handling edge cases, and QA testing every URL.
Will I lose my Google rankings during migration? Not if you handle redirects properly. Implement 301 redirects for every URL that changes, preserve your meta titles and descriptions, submit your new sitemap to Google Search Console, and use the Change of Address tool if your domain changes. Expect a small ranking fluctuation for 2-4 weeks, then recovery or improvement as Google recognizes better Core Web Vitals.
Can I keep using WordPress as my CMS with Next.js? Absolutely. This is called "headless WordPress" and it's a popular approach. Use WPGraphQL to expose your content as an API, then consume it from Next.js. Your editors keep the WordPress admin they know. The main downside is you still maintain a WordPress installation — security updates, hosting, the whole stack.
How much does it cost to migrate from WordPress to Next.js? DIY with one developer: 100-300 hours of work. Agency migration: typically $15,000-$75,000 depending on complexity. Ongoing hosting costs usually decrease — Vercel Pro at $20/user/month versus managed WordPress hosting at $50-$300/month. The ROI comes from reduced hosting costs, better performance (which improves conversion rates), and lower maintenance overhead.
Should I use the Pages Router or App Router in Next.js 15? App Router, full stop. In 2026, the App Router is the stable default with Server Components, streaming, and parallel routes. The Pages Router still works and isn't deprecated, but new features and optimizations are App Router-first. Every migration we do at Social Animal uses the App Router exclusively. Check our Next.js development capabilities for more on our approach.
What about forms and dynamic functionality? Next.js API routes (or Server Actions in the App Router) handle form submissions, search, authentication, and any server-side logic. For contact forms, you can use Server Actions with a service like Resend for email delivery. For search, consider Algolia or Meilisearch. For authentication, NextAuth.js (now Auth.js v5) covers most use cases.
Is Vercel the only option for hosting Next.js? No. You can deploy Next.js on Netlify, AWS Amplify, Cloudflare Pages, or self-host with Node.js. However, Vercel is built by the Next.js team, and the integration shows — ISR, edge middleware, image optimization, and analytics all work best on Vercel. The DX gap between Vercel and alternatives has narrowed in 2026, but Vercel remains the path of least resistance.
What if I need to migrate a WooCommerce store? This is a bigger project. Most teams migrate the storefront to Next.js while moving the commerce backend to Shopify (using Storefront API), Medusa.js, or Saleor. Shopify's Hydrogen framework is another option, but if you want full control over the frontend, Next.js with Shopify's API is the most proven path. Expect the e-commerce migration to add 4-8 weeks to your timeline.