🇬🇧translation is currently inBeta

Migration to Next.js: How to Painlessly Speed Up Your Website

Complete guide to migrating a website to Next.js. Step by step, preserving SEO and without downtime. Case study with 60% performance improvement.

Adam Noszczyński
20 min czytania
Development

Migration to Next.js can deliver 60% performance improvement and significantly better SEO. In this guide, I'll show you how to conduct a painless migration without losing Google rankings and downtime.

Why Migrate to Next.js?

Migration Benefits - Real Numbers:

Loading speed improves from 3.2s to 1.2s (62% improvement), Lighthouse Score increases from 67 to 94 points (+27 points), Bounce Rate drops from 45% to 28% (38% decrease), conversion increases by 23% after migration, and hosting costs decrease by 40% thanks to SSG.

Phase 1: Audit and Migration Planning

1. Current Site Analysis

# Performance audit
npx lighthouse https://your-site.com --output html --output-path ./audit-before.html

# Bundle size analysis
npm install -g webpack-bundle-analyzer
webpack-bundle-analyzer dist/static/js/*.js

# SEO crawl
screaming-frog-seo-spider https://your-site.com

2. Resource Inventory

// audit-script.js - website analysis script
const puppeteer = require("puppeteer");
const fs = require("fs");

const auditWebsite = async url => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // Performance analysis
    await page.goto(url, { waitUntil: "networkidle2" });

    const metrics = await page.evaluate(() => {
        const navigation = performance.getEntriesByType("navigation")[0];
        return {
            domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
            loadComplete: navigation.loadEventEnd - navigation.fetchStart,
            firstPaint: performance.getEntriesByName("first-paint")[0]?.startTime,
            firstContentfulPaint:
                performance.getEntriesByName("first-contentful-paint")[0]?.startTime,
        };
    });

    // SEO analysis
    const seoData = await page.evaluate(() => {
        return {
            title: document.title,
            metaDescription: document.querySelector('meta[name="description"]')?.content,
            h1Count: document.querySelectorAll("h1").length,
            imageCount: document.querySelectorAll("img").length,
            linksCount: document.querySelectorAll("a").length,
        };
    });

    // Save report
    const report = { url, metrics, seoData, timestamp: new Date() };
    fs.writeFileSync("./audit-report.json", JSON.stringify(report, null, 2));

    await browser.close();
    return report;
};

// Run audit
auditWebsite("https://your-site.com");

3. Migration Plan - Timeline

Migration timeline covers ten weeks of work. First two weeks are audit and project setup (20 hours), weeks 3-4 are core component migration (40 hours), weeks 5-6 cover routing and API integration (30 hours). Weeks 7-8 focus on SEO and optimizations (25 hours), ninth week is testing and deployment (15 hours), and tenth is go-live and monitoring (10 hours).

Phase 2: Next.js Project Setup

1. Project Initialization

# Create new Next.js project
npx create-next-app@latest project-name --typescript --tailwind --eslint --app

cd project-name

# Install additional dependencies
npm install @next/mdx @mdx-js/loader @mdx-js/react
npm install next-sitemap next-seo
npm install sharp # for image optimization

2. next.config.js Configuration

/** @type {import('next').NextConfig} */
const nextConfig = {
    // Experimental features
    experimental: {
        optimizePackageImports: ["lucide-react", "date-fns"],
    },

    // Redirects from old site
    async redirects() {
        return [
            {
                source: "/old-page",
                destination: "/new-page",
                permanent: true,
            },
            {
                source: "/blog/:slug*",
                destination: "/articles/:slug*",
                permanent: true,
            },
        ];
    },

    // Rewrites for API
    async rewrites() {
        return [
            {
                source: "/api/legacy/:path*",
                destination: "https://old-api.domain.com/:path*",
            },
        ];
    },

    // Headers for SEO and security
    async headers() {
        return [
            {
                source: "/:path*",
                headers: [
                    {
                        key: "X-DNS-Prefetch-Control",
                        value: "on",
                    },
                    {
                        key: "Strict-Transport-Security",
                        value: "max-age=63072000; includeSubDomains; preload",
                    },
                    {
                        key: "X-Frame-Options",
                        value: "DENY",
                    },
                ],
            },
        ];
    },

    // Image optimization
    images: {
        formats: ["image/avif", "image/webp"],
        deviceSizes: [640, 750, 828, 1080, 1200, 1920],
        imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
        domains: ["old-domain.com", "cdn.old-domain.com"],
    },
};

module.exports = nextConfig;

3. Folder Structure

src/
├── app/
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.tsx
│   ├── loading.tsx
│   ├── not-found.tsx
│   ├── sitemap.ts
│   └── robots.ts
├── components/
│   ├── ui/
│   ├── layout/
│   └── common/
├── lib/
│   ├── utils.ts
│   ├── validations.ts
│   └── api.ts
├── hooks/
├── types/
└── data/

Phase 3: Component Migration

1. Layout and Navigation

// components/layout/MainLayout.tsx
import { Header } from './Header'
import { Footer } from './Footer'
import { Sidebar } from './Sidebar'

interface MainLayoutProps {
  children: React.ReactNode
  showSidebar?: boolean
}

export function MainLayout({ children, showSidebar = false }: MainLayoutProps) {
  return (
    <div className="min-h-screen flex flex-col">
      <Header />

      <main className="flex-1 flex">
        {showSidebar && (
          <aside className="w-64 bg-gray-50">
            <Sidebar />
          </aside>
        )}

        <div className="flex-1">
          {children}
        </div>
      </main>

      <Footer />
    </div>
  )
}

2. Form Migration

// components/forms/ContactForm.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  })
  const [isSubmitting, setIsSubmitting] = useState(false)
  const router = useRouter()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setIsSubmitting(true)

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      })

      if (response.ok) {
        router.push('/thank-you')
      } else {
        throw new Error('Form submission error')
      }
    } catch (error) {
      console.error('Error:', error)
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div>
        <label htmlFor="name" className="block text-sm font-medium text-text">
          Full Name
        </label>
        <input
          type="text"
          id="name"
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          required
        />
      </div>

      {/* Remaining fields... */}

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
      >
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  )
}

3. API Endpoints Migration

// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
})

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const validatedData = contactSchema.parse(body)

    // Integration with external API (e.g., old site)
    const response = await fetch('https://old-api.domain.com/contact', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.OLD_API_TOKEN}`,
      },
      body: JSON.stringify(validatedData),
    })

    if (!response.ok) {
      throw new Error('API Error')
    }

    return NextResponse.json({ success: true })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation error', details: error.errors },
        { status: 400 }
      )
    }

    return NextResponse.json(
      { error: 'Server error' },
      { status: 500 }
    )
  }
}

Phase 4: SEO and Redirects

1. URL Mapping

// lib/redirects.ts
export const redirectMap = {
  '/old-about': '/about',
  '/old-services': '/services',
  '/blog/old-post-1': '/articles/new-post-1',
  '/contact-us': '/contact',
  '/products/category-1': '/services/category-1',
}

// Middleware for redirects
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { redirectMap } from './lib/redirects'

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // Check if redirect needed
  if (redirectMap[pathname]) {
    return NextResponse.redirect(
      new URL(redirectMap[pathname], request.url),
      301
    )
  }

  return NextResponse.next()
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

2. Sitemap and robots.txt

// app/sitemap.ts
import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://your-new-site.com'

  // Static pages
  const staticPages = [
    '',
    '/about',
    '/services',
    '/contact',
    '/privacy-policy',
  ].map((route) => ({
    url: `${baseUrl}${route}`,
    lastModified: new Date(),
    changeFrequency: 'monthly' as const,
    priority: route === '' ? 1 : 0.8,
  }))

  // Dynamic pages (e.g., blog)
  const articles = [
    'nextjs-vs-wordpress-2025',
    'core-web-vitals-nextjs',
    // ... other articles
  ].map((slug) => ({
    url: `${baseUrl}/articles/${slug}`,
    lastModified: new Date(),
    changeFrequency: 'weekly' as const,
    priority: 0.6,
  }))

  return [...staticPages, ...articles]
}

// app/robots.ts
import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/admin/', '/api/'],
    },
    sitemap: 'https://your-new-site.com/sitemap.xml',
  }
}

3. Metadata and Open Graph

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
    title: {
        default: "Your Company - Professional Websites",
        template: "%s | Your Company",
    },
    description:
        "We create modern websites in Next.js. Fast, secure, and SEO optimized.",
    keywords: ["websites", "Next.js", "React", "SEO"],
    authors: [{ name: "Adam Noszczyński" }],
    creator: "Adam Noszczyński",
    metadataBase: new URL("https://your-new-site.com"),
    openGraph: {
        type: "website",
        locale: "en_US",
        url: "https://your-new-site.com",
        title: "Your Company - Professional Websites",
        description: "We create modern websites in Next.js.",
        siteName: "Your Company",
        images: [
            {
                url: "/og-image.jpg",
                width: 1200,
                height: 630,
                alt: "Your Company",
            },
        ],
    },
    twitter: {
        card: "summary_large_image",
        title: "Your Company",
        description: "Professional websites in Next.js",
        images: ["/og-image.jpg"],
    },
    robots: {
        index: true,
        follow: true,
        googleBot: {
            index: true,
            follow: true,
            "max-video-preview": -1,
            "max-image-preview": "large",
            "max-snippet": -1,
        },
    },
};

Phase 5: Testing and Deployment

1. Pre-Deployment Tests

// tests/migration.test.js
const { chromium } = require("playwright");

describe("Migration Tests", () => {
    let browser, page;

    beforeAll(async () => {
        browser = await chromium.launch();
        page = await browser.newPage();
    });

    test("All critical pages load correctly", async () => {
        const criticalPages = ["/", "/about", "/services", "/contact"];

        for (const url of criticalPages) {
            const response = await page.goto(`http://localhost:3000${url}`);
            expect(response.status()).toBe(200);

            // Check for critical elements
            await expect(page.locator("h1")).toBeVisible();
            await expect(page.locator("nav")).toBeVisible();
        }
    });

    test("Redirects work correctly", async () => {
        const response = await page.goto("http://localhost:3000/old-about");
        expect(response.url()).toContain("/about");
    });

    test("Forms submit correctly", async () => {
        await page.goto("http://localhost:3000/contact");
        await page.fill('input[name="name"]', "Test User");
        await page.fill('input[name="email"]', "test@example.com");
        await page.fill('textarea[name="message"]', "Test message");

        await page.click('button[type="submit"]');
        await expect(page).toHaveURL(/thank-you/);
    });

    afterAll(async () => {
        await browser.close();
    });
});

2. Performance Testing

# Lighthouse CI
npm install -g @lhci/cli

# .lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: [
        'http://localhost:3000/',
        'http://localhost:3000/about',
        'http://localhost:3000/services',
      ],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', {minScore: 0.9}],
        'categories:accessibility': ['error', {minScore: 0.9}],
        'categories:best-practices': ['error', {minScore: 0.9}],
        'categories:seo': ['error', {minScore: 0.9}],
      },
    },
  },
};

# Run tests
lhci autorun

Phase 6: Go-Live Strategy

1. Blue-Green Deployment

# .github/workflows/deploy.yml
name: Deploy to Production

on:
    push:
        branches: [main]

jobs:
    deploy:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v3

            - name: Setup Node.js
              uses: actions/setup-node@v3
              with:
                  node-version: "18"
                  cache: "npm"

            - name: Install dependencies
              run: npm ci

            - name: Run tests
              run: npm test

            - name: Build application
              run: npm run build

            - name: Deploy to Vercel
              uses: amondnet/vercel-action@v20
              with:
                  vercel-token: ${{ secrets.VERCEL_TOKEN }}
                  vercel-org-id: ${{ secrets.ORG_ID }}
                  vercel-project-id: ${{ secrets.PROJECT_ID }}
                  vercel-args: "--prod"

2. Post-Deployment Monitoring

// lib/monitoring.ts
export function setupMonitoring() {
  // Real User Monitoring
  if (typeof window !== 'undefined') {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(sendToAnalytics);
      getFID(sendToAnalytics);
      getFCP(sendToAnalytics);
      getLCP(sendToAnalytics);
      getTTFB(sendToAnalytics);
    });
  }
}

function sendToAnalytics(metric: any) {
  // Send to Google Analytics 4
  if (typeof gtag !== 'undefined') {
    gtag('event', metric.name, {
      event_category: 'Web Vitals',
      event_label: metric.id,
      value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
      non_interaction: true,
    });
  }
}

Migration Checklist

✅ Pre-Migration

  • [ ] Backup old site
  • [ ] SEO and performance audit
  • [ ] URL mapping
  • [ ] Redirect plan
  • [ ] Monitoring setup

✅ During Migration

  • [ ] Core components
  • [ ] Routing and navigation
  • [ ] Forms and API
  • [ ] SEO metadata
  • [ ] Testing on staging

✅ Post-Migration

  • [ ] 404 error monitoring
  • [ ] Google Search Console
  • [ ] Performance tracking
  • [ ] User feedback
  • [ ] Backup strategy

Migration Results - Case Study

Metrics Before and After:

| Metric | Before | After | Improvement | | ---------------- | ------ | ------ | ------- | | Load Time | 3.2s | 1.2s | 62% | | Lighthouse | 67 | 94 | +27 | | Bounce Rate | 45% | 28% | 38% | | Conversion | 2.3% | 2.8% | 23% | | Server Costs | $200/m | $120/m | 40% |

Migration ROI:

  • Migration cost: $15,000
  • Annual savings: $1,200 (hosting + maintenance)
  • Conversion increase: +$8,000/year
  • ROI: 61% in the first year

Summary

Migration to Next.js is an investment that pays off - 60% performance improvement, better SEO and rankings, lower maintenance costs, modern technology, and better UX and conversion.

The key to success is a systematic approach, thorough planning, and monitoring at every stage.

Need help with migration? Contact us - we'll migrate your site to Next.js without downtime and SEO loss.

root.pages.case-study.root.pages.case-study.cta-title

Book consultation 30 min.

Ailo client logoCledar client logoMiohome client logoPlenti client logoWebiso client logo+4
I've been working on projects for clients for 11 years

Tagi:

Next.js
Migration
Performance
SEO
React