Development

Migracja do Next.js: jak bezboleśnie przyspieszyć stronę www

Kompletny przewodnik migracji strony do Next.js. Krok po kroku, z zachowaniem SEO i bez przestojów. Case study z 60% poprawą wydajności.

Adam Noszczyński
20 min czytania

Migracja do Next.js może przynieść 60% poprawę wydajności i znacznie lepsze SEO. W tym przewodniku pokażę, jak przeprowadzić bezbolesną migrację bez utraty pozycji w Google i przestojów.

Dlaczego migrować do Next.js?

Korzyści z migracji - realne liczby:

Szybkość ładowania poprawia się z 3.2s do 1.2s (62% poprawa), Lighthouse Score wzrasta z 67 do 94 punktów (+27 punktów), Bounce Rate spada z 45% do 28% (38% spadek), konwersja rośnie o 23% po migracji, a koszty hostingu maleją o 40% dzięki SSG.

Faza 1: Audyt i planowanie migracji

1. Analiza obecnej strony

# Audyt wydajności
npx lighthouse https://twoja-strona.pl --output html --output-path ./audit-before.html

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

# SEO crawl
screaming-frog-seo-spider https://twoja-strona.pl

2. Inwentaryzacja zasobów

// audit-script.js - skrypt do analizy strony
const puppeteer = require("puppeteer");
const fs = require("fs");

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

    // Analiza performance
    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,
        };
    });

    // Analiza SEO
    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,
        };
    });

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

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

// Uruchom audyt
auditWebsite("https://twoja-strona.pl");

3. Plan migracji - timeline

Timeline migracji obejmuje dziesięć tygodni pracy. Pierwsze dwa tygodnie to audyt i setup projektu (20 godzin), tygodnie 3-4 to migracja komponentów core (40 godzin), tygodnie 5-6 obejmują routing i API integration (30 godzin). Tygodnie 7-8 skupiają się na SEO i optymalizacjach (25 godzin), dziewiąty tydzień to testing i deployment (15 godzin), a dziesiąty to go-live i monitoring (10 godzin).

Faza 2: Setup projektu Next.js

1. Inicjalizacja projektu

# Utwórz nowy projekt Next.js
npx create-next-app@latest nazwa-projektu --typescript --tailwind --eslint --app

cd nazwa-projektu

# Zainstaluj dodatkowe dependencje
npm install @next/mdx @mdx-js/loader @mdx-js/react
npm install next-sitemap next-seo
npm install sharp # dla optymalizacji obrazów

2. Konfiguracja next.config.js

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

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

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

    // Headers dla SEO i 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",
                    },
                ],
            },
        ];
    },

    // Optymalizacja obrazów
    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. Struktura folderów

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/

Faza 3: Migracja komponentów

1. Layout i nawigacja

// 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. Migracja formularzy

// 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('Błąd wysyłania formularza')
      }
    } 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-gray-700">
          Imię i nazwisko
        </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>

      {/* Pozostałe pola... */}

      <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 ? 'Wysyłanie...' : 'Wyślij wiadomość'}
      </button>
    </form>
  )
}

3. Migracja API endpoints

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

const contactSchema = z.object({
  name: z.string().min(2, 'Imię musi mieć co najmniej 2 znaki'),
  email: z.string().email('Nieprawidłowy adres email'),
  message: z.string().min(10, 'Wiadomość musi mieć co najmniej 10 znaków'),
})

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

    // Integracja z zewnętrznym API (np. stara strona)
    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('Błąd API')
    }

    return NextResponse.json({ success: true })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Błąd walidacji', details: error.errors },
        { status: 400 }
      )
    }

    return NextResponse.json(
      { error: 'Błąd serwera' },
      { status: 500 }
    )
  }
}

Faza 4: SEO i przekierowania

1. Mapowanie URL-i

// 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 dla przekierowań
// 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

  // Sprawdź czy potrzebne przekierowanie
  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 i robots.txt

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

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://twoja-nowa-strona.pl'

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

  // Dynamiczne strony (np. blog)
  const articles = [
    'nextjs-vs-wordpress-2025',
    'core-web-vitals-nextjs',
    // ... inne artykuły
  ].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://twoja-nowa-strona.pl/sitemap.xml',
  }
}

3. Metadata i Open Graph

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

export const metadata: Metadata = {
    title: {
        default: "Twoja Firma - Profesjonalne strony internetowe",
        template: "%s | Twoja Firma",
    },
    description:
        "Tworzymy nowoczesne strony internetowe w Next.js. Szybkie, bezpieczne i zoptymalizowane pod SEO.",
    keywords: ["strony internetowe", "Next.js", "React", "SEO"],
    authors: [{ name: "Adam Noszczyński" }],
    creator: "Adam Noszczyński",
    metadataBase: new URL("https://twoja-nowa-strona.pl"),
    openGraph: {
        type: "website",
        locale: "pl_PL",
        url: "https://twoja-nowa-strona.pl",
        title: "Twoja Firma - Profesjonalne strony internetowe",
        description: "Tworzymy nowoczesne strony internetowe w Next.js.",
        siteName: "Twoja Firma",
        images: [
            {
                url: "/og-image.jpg",
                width: 1200,
                height: 630,
                alt: "Twoja Firma",
            },
        ],
    },
    twitter: {
        card: "summary_large_image",
        title: "Twoja Firma",
        description: "Profesjonalne strony internetowe w 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,
        },
    },
};

Faza 5: Testing i deployment

1. Testy przed wdrożeniem

// 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}],
      },
    },
  },
};

# Uruchom testy
lhci autorun

Faza 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. Monitoring po wdrożeniu

// 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) {
  // Wyślij do 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,
    });
  }
}

Checklist migracji

✅ Pre-migration

  • [ ] Backup starej strony
  • [ ] Audyt SEO i performance
  • [ ] Mapowanie URL-i
  • [ ] Plan przekierowań
  • [ ] Setup monitoringu

✅ During migration

  • [ ] Komponenty core
  • [ ] Routing i nawigacja
  • [ ] Formularze i API
  • [ ] SEO metadata
  • [ ] Testing na staging

✅ Post-migration

  • [ ] Monitoring 404 errors
  • [ ] Google Search Console
  • [ ] Performance tracking
  • [ ] User feedback
  • [ ] Backup strategy

Wyniki migracji - case study

Metryki przed i po:

| Metryka | Przed | Po | Poprawa | | ---------------- | ------ | ------ | ------- | | 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% |

ROI migracji:

  • Koszt migracji: $15,000
  • Oszczędności roczne: $1,200 (hosting + maintenance)
  • Wzrost konwersji: +$8,000/rok
  • ROI: 61% w pierwszym roku

Podsumowanie

Migracja do Next.js to inwestycja, która się opłaca - 60% poprawa wydajności, lepsze SEO i pozycjonowanie, niższe koszty utrzymania, nowoczesna technologia oraz lepsza UX i konwersja.

Kluczem do sukcesu jest systematyczne podejście, dokładne planowanie i monitoring na każdym etapie.

Potrzebujesz pomocy z migracją? Skontaktuj się z nami - przeprowadzimy Twoją stronę do Next.js bez przestojów i utraty SEO.

Tagi:

Next.js
Migracja
Performance
SEO
React

Gotowy na start swojego projektu?

Skontaktuj się ze mną, aby omówić Twoje potrzeby i otrzymać bezpłatną konsultację.

Ailo client logoCledar client logoMiohome client logoPlenti client logoWebiso client logo+10
Realizuję projekty dla klientów od 6 lat