In late 2024, a Series C financial services SaaS company came to us with a problem that was costing them real money. Their marketing site, customer portal, and documentation hub were all running on WordPress with a rats' nest of premium plugins, a $420K/year enterprise CMS licensing stack, and page load times that made their compliance team nervous. They needed to move to a modern headless architecture without a single second of downtime — because in financial services, downtime means regulatory scrutiny, lost trust, and very expensive phone calls from very serious people.

This is the full story of how we pulled it off.

Table of Contents

WordPress to Next.js Migration: Financial SaaS Saves $420K ARR

The Starting Point: A WordPress Monolith Under Pressure

Let me paint the picture. This company — we'll call them FinEdge (NDA, you understand) — had roughly 12,000 pages of content across three distinct web properties:

  1. Marketing site — Product pages, landing pages, blog with 2,400+ posts
  2. Customer portal — Account dashboards, onboarding flows, document management
  3. Documentation hub — API docs, compliance guides, integration tutorials

All three ran on a single WordPress multisite installation hosted on WP Engine's enterprise tier. The plugin situation was... something. They were running 47 active plugins, including WPGraphQL, Advanced Custom Fields Pro, Yoast SEO Premium, WP Rocket, Gravity Forms, and a custom plugin their previous agency built that handled SOC 2 compliance logging for content changes.

The real pain points:

  • Page load times averaging 4.2 seconds on mobile (Google CrUX data)
  • Core Web Vitals failing on 68% of pages — LCP was brutal at 5.1s median
  • $420K/year in licensing across WP Engine enterprise hosting, premium plugins, a WAF, CDN, and a separate staging environment
  • Content editors waiting 8-12 seconds for the WordPress admin to respond during peak hours
  • Security patches required dedicated DevOps time every two weeks — the financial services regulators don't mess around
  • No preview deployments — content team had to push to staging and wait 4 minutes for cache invalidation

Their VP of Engineering told us during the discovery call: "We're spending more on our website infrastructure than on two senior engineers. And it's still slow."

Why Headless Next.js Was the Right Call

We evaluated several options during the architecture phase. Here's what was on the table:

Option Pros Cons Estimated Annual Cost
WordPress (optimized) Familiar to team, no migration needed Still slow, licensing unchanged $420K
Webflow Enterprise Visual editing, fast deployment Limited for portal/app needs, vendor lock-in $180K
Next.js + Sanity Blazing fast, flexible, real-time preview Migration effort, team ramp-up $38K
Next.js + Contentful Strong enterprise features, good DX Per-user pricing scales poorly $95K
Astro + Storyblok Great for static content, lightweight Less mature for dynamic portal needs $42K

We landed on Next.js 14 (App Router) with Sanity as the headless CMS. Here's why:

  • FinEdge's portal had dynamic, authenticated routes that needed server-side rendering. Next.js handles this natively with React Server Components.
  • Sanity's real-time collaboration and GROQ query language gave content editors a dramatically better experience than WordPress.
  • The pricing model (Sanity's Growth plan at $99/month + Vercel Pro) meant infrastructure costs dropped from $420K to roughly $38K annually.
  • Their engineering team already knew React. The ramp-up to Next.js was measured in days, not months.

We did seriously consider Astro for the documentation hub since it's mostly static content, but the operational simplicity of keeping everything in one framework won out. If the docs site had been a standalone project, Astro would've been the pick.

The Migration Architecture

Here's the high-level architecture we designed:

┌─────────────────┐     ┌──────────────────┐
│   Sanity CMS     │────▶│  Next.js on       │
│   (Content)      │     │  Vercel (Edge)    │
└─────────────────┘     └──────────────────┘
         │                        │
         │                        ▼
         │               ┌──────────────────┐
         │               │  Cloudflare       │
         │               │  (DNS + WAF)      │
         │               └──────────────────┘
         │                        │
         ▼                        ▼
┌─────────────────┐     ┌──────────────────┐
│  Media Pipeline  │     │  End Users        │
│  (Cloudinary)    │     └──────────────────┘
└─────────────────┘

The key components:

Content Layer

  • Sanity as the primary CMS for marketing content, blog posts, and documentation
  • Custom Sanity schemas that mapped to their existing WordPress content types
  • Portable Text for rich content (replacing WordPress's Gutenberg blocks)

Application Layer

  • Next.js 14 with App Router, deployed on Vercel's Pro plan
  • React Server Components for the marketing site and docs
  • Client components only where interactivity was genuinely needed (forms, dashboards, interactive charts)
  • Middleware for authentication on portal routes, integrated with their existing Auth0 setup

Infrastructure Layer

  • Vercel for hosting and edge functions
  • Cloudflare for DNS management and additional WAF rules (financial services compliance requirement)
  • Cloudinary for image optimization and transformation — replaced 3 WordPress image plugins

WordPress to Next.js Migration: Financial SaaS Saves $420K ARR - architecture

Zero Downtime Strategy: The Parallel Run

This was the part that kept me up at night. FinEdge couldn't afford even a few minutes of downtime. Their customer portal processes financial transactions, and any interruption triggers mandatory incident reports to regulators.

Here's how we did it:

Phase 1: Content Sync (Weeks 1-3)

We built a custom WordPress-to-Sanity sync pipeline that ran continuously during the migration period:

// Simplified version of our WP-to-Sanity sync worker
import { createClient } from '@sanity/client'
import WPGraphQL from './wp-graphql-client'

const sanity = createClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: 'production',
  token: process.env.SANITY_WRITE_TOKEN,
  apiVersion: '2024-10-01',
  useCdn: false,
})

async function syncPosts(since: string) {
  const posts = await WPGraphQL.getModifiedPosts(since)
  
  const transaction = sanity.transaction()
  
  for (const post of posts) {
    const sanityDoc = transformWPToSanity(post)
    transaction.createOrReplace(sanityDoc)
  }
  
  await transaction.commit()
  console.log(`Synced ${posts.length} posts`)
}

// Ran every 5 minutes via cron

This meant content editors could keep working in WordPress during the entire migration. Every change they made was automatically synced to Sanity within 5 minutes.

Phase 2: Parallel Deployment (Weeks 4-8)

We deployed the Next.js site on a subdomain (next.finedge.com) and ran both sites simultaneously. Our QA process compared every single page:

  • Visual regression testing with Playwright across 200+ critical pages
  • SEO parity checks (meta tags, structured data, canonical URLs, sitemaps)
  • Performance benchmarks on every page template
  • Accessibility audits (WCAG 2.1 AA — required for financial services)

Phase 3: The Cutover (Week 9)

The actual switch was anticlimactic — which is exactly what you want. We used Cloudflare's load balancing to gradually shift traffic:

  • Hour 0: 5% of traffic to Next.js, 95% to WordPress
  • Hour 2: 25% / 75% (monitoring error rates, Core Web Vitals)
  • Hour 6: 50% / 50%
  • Hour 12: 90% / 10%
  • Hour 24: 100% Next.js, WordPress in read-only mode
  • Week 2: WordPress decommissioned

Zero errors. Zero downtime. The monitoring dashboards were boringly green.

Performance Results: 3x Faster and Then Some

Here are the real numbers, measured 30 days post-migration using Google CrUX data and Vercel Analytics:

Metric WordPress (Before) Next.js (After) Improvement
LCP (p75) 5.1s 1.2s 4.25x faster
FID / INP (p75) 280ms 68ms 4.1x faster
CLS (p75) 0.18 0.02 9x better
TTFB (p75) 1.8s 0.12s 15x faster
Lighthouse Performance 34 96 +62 points
Pages passing CWV 32% 98% +66%
Time to Interactive 6.8s 1.4s 4.9x faster

The "3x faster" headline actually undersells it. On most metrics, we saw 4-5x improvements. TTFB was the star — going from 1.8 seconds to 120 milliseconds thanks to Vercel's Edge Network and ISR (Incremental Static Regeneration).

Organic traffic increased 31% in the first 90 days post-migration. Their SEO team attributed this primarily to Core Web Vitals improvements and faster crawling rates from Googlebot.

The $420K Licensing Savings Breakdown

Let's talk money. Here's exactly where the $420K was going and what replaced it:

Line Item WordPress Annual Cost Next.js Annual Cost Savings
WP Engine Enterprise hosting $150,000 $150,000
Vercel Pro (Team plan) $2,400
Premium plugin licenses (47 plugins) $28,000 $28,000
Sanity Growth plan $1,188
Cloudinary Pro $2,388
Enterprise WAF (Sucuri) $36,000 $36,000
Cloudflare Pro $2,400
Custom WordPress maintenance contract $96,000 $96,000
CDN (separate from WP Engine) $24,000 $24,000
Staging environment hosting $18,000 $18,000
WordPress security audits (quarterly) $48,000 $48,000
DevOps team time (partial FTE) $120,000 $30,000 $90,000
Totals $520,000 $38,376 $481,624

The actual savings ended up being closer to $482K, not $420K. The original $420K estimate from the discovery phase was conservative — we didn't initially account for the reduction in DevOps time or the elimination of quarterly security audits (Vercel and Cloudflare handle most of what those audits covered).

The ROI math is straightforward. Our migration project cost FinEdge roughly $185K in agency fees over the 10-week engagement. That investment paid for itself in under 5 months.

Technical Deep Dive: Key Implementation Details

Handling 2,400 Blog Posts with ISR

We didn't statically generate all 2,400 blog posts at build time. That would've made deployments painfully slow. Instead, we used ISR with on-demand revalidation:

// app/blog/[slug]/page.tsx
import { sanityFetch } from '@/lib/sanity'
import { postQuery } from '@/lib/queries'

export const revalidate = 3600 // Revalidate every hour as fallback

export async function generateStaticParams() {
  // Only pre-generate the top 100 posts by traffic
  const topPosts = await sanityFetch({
    query: `*[_type == "post"] | order(pageViews desc) [0...100] { "slug": slug.current }`
  })
  return topPosts.map((post) => ({ slug: post.slug }))
}

export default async function BlogPost({ params }) {
  const post = await sanityFetch({
    query: postQuery,
    params: { slug: params.slug },
    tags: [`post:${params.slug}`]
  })
  
  // ... render post
}

When content editors publish or update in Sanity, a webhook hits our revalidation endpoint:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest } from 'next/server'

export async function POST(req: NextRequest) {
  const body = await req.json()
  const secret = req.headers.get('x-sanity-webhook-secret')
  
  if (secret !== process.env.SANITY_WEBHOOK_SECRET) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  // Revalidate specific content
  if (body._type === 'post') {
    revalidateTag(`post:${body.slug.current}`)
    revalidateTag('posts-list')
  }
  
  return Response.json({ revalidated: true })
}

Content updates now appear on the live site in under 3 seconds. Compare that to the 4-minute cache invalidation they had with WordPress + WP Rocket.

Authentication for the Customer Portal

The portal routes needed server-side authentication. We used Next.js middleware combined with their existing Auth0 setup:

// middleware.ts
import { NextResponse } from 'next/server'
import { getSession } from '@auth0/nextjs-auth0/edge'

export async function middleware(req) {
  if (req.nextUrl.pathname.startsWith('/portal')) {
    const session = await getSession(req, NextResponse.next())
    
    if (!session?.user) {
      return NextResponse.redirect(new URL('/api/auth/login', req.url))
    }
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/portal/:path*']
}

This runs at the edge, so unauthenticated requests get redirected before they even hit the application server. Fast and secure.

301 Redirect Map

We had roughly 340 URLs that changed structure during the migration. A financial services site absolutely cannot have broken links — every inbound link from regulatory filings, partner sites, and historical content needs to resolve correctly.

We built a redirect map in next.config.js and supplemented it with a dynamic redirect lookup from Sanity for editor-managed redirects:

// next.config.js (partial)
module.exports = {
  async redirects() {
    return [
      // Static redirects for known URL changes
      ...require('./redirects.json').map(r => ({
        source: r.from,
        destination: r.to,
        permanent: true,
      })),
    ]
  },
}

Six months post-launch, Google Search Console shows zero 404 errors from the migration. Every single redirect is working.

Lessons Learned the Hard Way

1. WordPress Gutenberg Blocks Are a Pain to Convert

We underestimated the effort to convert Gutenberg blocks to Sanity's Portable Text. FinEdge had used 23 different block types, including custom blocks their previous agency built. Budget at least 20% more time than you think for content transformation.

2. Content Editor Training Is Not Optional

Sanity's Studio is intuitive, but it's not WordPress. We ran three 90-minute training sessions and created a custom Sanity Studio with guided workflows. The content team went from skeptical to enthusiastic within two weeks, but that training investment was critical.

3. Financial Services Compliance Adds Complexity

Every deployment needed an audit trail. Every content change needed to be logged with timestamps and user attribution. We built a custom Sanity plugin that logs all document mutations to an append-only audit table in their existing PostgreSQL database. This took an extra week that wasn't in the original scope.

4. Don't Forget About Forms

Gravity Forms was handling 14 different form types on the WordPress site. We replaced them with React Hook Form + Zod validation on the frontend and server actions on the backend, with submissions going to their existing HubSpot CRM. This migration alone took a full week.

Timeline and Team Structure

Total project duration: 10 weeks

Week Focus Team
1 Architecture, Sanity schema design, content audit 2 devs, 1 architect
2-3 Content sync pipeline, Sanity Studio customization 2 devs, 1 content strategist
4-5 Marketing site build (Next.js) 3 devs
6-7 Portal migration, authentication, forms 3 devs
8 Documentation hub, SEO audit, redirect map 2 devs, 1 SEO specialist
9 QA, visual regression, performance testing 2 devs, 1 QA
10 Gradual traffic cutover, monitoring, WordPress decommission 2 devs, 1 DevOps

Peak team size was 4 people. Most of the project ran with 2-3 developers. This isn't a 15-person, 6-month engagement — it's a focused, experienced team executing a well-planned migration.

If you're considering a similar migration for your organization, we've documented our headless CMS development approach and our pricing structure is transparent. We're also happy to jump on a call to talk through your specific situation — reach out here.

FAQ

How long does a WordPress to Next.js migration typically take? For a site of this complexity (12,000 pages, customer portal, documentation hub), 10 weeks is realistic with an experienced team. Simpler marketing sites with 100-500 pages can be migrated in 4-6 weeks. The biggest variable is content complexity — how many custom post types, block types, and plugin-dependent features you're running.

Can you migrate WordPress to Next.js without any downtime? Yes, but it requires planning. The key is running both systems in parallel with a content sync pipeline, then using DNS-level traffic shifting to gradually move users to the new site. We've done this successfully for multiple clients. The critical requirement is that your content stays in sync across both systems during the transition period.

How much does a WordPress to headless CMS migration cost? It depends heavily on scope. A straightforward marketing site migration might run $30K-$60K. An enterprise migration like FinEdge's — with a customer portal, compliance requirements, and 12,000 pages — was $185K. The ROI calculation matters more than the absolute cost. FinEdge's investment paid for itself in under 5 months through licensing savings alone.

Is Next.js actually faster than WordPress? In virtually every case, yes — significantly faster. WordPress generates HTML on each request (unless heavily cached), and even with caching plugins like WP Rocket, you're limited by PHP's response time and the weight of the WordPress ecosystem. Next.js with ISR or static generation serves pre-built pages from the edge. We typically see 3-5x improvements in Core Web Vitals.

What headless CMS should I use with Next.js? It depends on your team and requirements. Sanity excels for custom content modeling and real-time collaboration. Contentful is strong for enterprise teams who want a more structured, opinionated approach but gets expensive per-seat. Storyblok is great if visual editing is a priority. For simpler sites, even Markdown files in a Git repo can work. We evaluate this on a per-project basis — there's no universal answer.

Do you lose SEO when migrating from WordPress to Next.js? Not if you do it right. The three things that matter: comprehensive 301 redirect mapping so no existing URLs return 404s, preserving all meta tags and structured data, and submitting updated sitemaps to Google Search Console immediately after cutover. FinEdge saw a 31% increase in organic traffic within 90 days, largely driven by Core Web Vitals improvements.

What happens to WordPress plugins after migration? Each plugin's functionality needs to be replicated or replaced. Some are straightforward — SEO plugins get replaced by metadata in your Next.js components, caching plugins become unnecessary, and form plugins get replaced with React form libraries. Others, like custom compliance logging plugins, need bespoke replacement code. This is why a thorough plugin audit during discovery is essential.

Can content editors still use a visual editor after moving to headless? Yes. Sanity Studio provides a customizable editing interface with real-time preview. It's different from WordPress's block editor, but most content teams prefer it after the initial learning curve. Sanity's Presentation tool now offers true visual editing with click-to-edit functionality on the live preview. We also set up preview deployments on Vercel so editors can see exactly how their content will look before publishing.