Przejdź do treści

Pages Router do App Router: Migracja w Next.js

Zobacz, jak bezpiecznie migrować z Pages Router na App Router krok po kroku i bez przestojów na produkcji.

Maciej Sala

Founder StriveLab

5 min czytaniaOpublikowano 11 kwietnia 2026 (Aktualizacja 7 lipca 2026)

to główny kierunek rozwoju Next.js — ** , streaming, Server Actions, Parallel Routes i PPR projektowane są przede wszystkim pod app/**. nadal działa i jest wspierany, więc migracja nie musi być natychmiastowa, ale długofalowo jest jednak nieunikniona. Są tego duże korzyści, ponieważ otrzymamy mniejszy JS bundle, szybszy TTFB, zero duplikacji w nested layouts, formularze bez API routes i granularny loading/error handling. Jeśli chcesz najpierw zrozumieć, dlaczego model server-first zmienia architekturę, zacznij od artykułu o React Server Components i Server Actions.

Migracja Pages Router do App Router: oba routery mogą współistnieć

Next.js pozwala używać pages/ i app/ w tym samym projekcie, to najlepsza wiadomość przy migracji. Przenosisz jedną ścieżkę, testujesz, deployujesz i dopiero wtedy bierzesz następną i pamiętaj by nie ** migrować wszystkiego naraz.**

Code
next-app/
├── app/              ← Nowe strony (App Router)
│   ├── layout.tsx
│   ├── page.tsx      ← Strona główna (już zmigrowana)
│   └── blog/
│       └── page.tsx  ← Blog (już zmigrowany)
├── pages/            ← Stare strony (Pages Router)
│   ├── products/
│   │   └── [id].tsx  ← Jeszcze nie zmigrowane
│   └── dashboard.tsx ← Jeszcze nie zmigrowane

app/ i pages/ mogą współistnieć, ale nie powinny definiować tej samej ścieżki. App Router ma priorytet, a duplikaty będą prowadzić do błędu builda, więc przenoś route i usuń jego odpowiednik z pages/.

Pamiętaj, że istnieje jeden efekt uboczny: przejście linkiem między stroną z pages/ a stroną z app/ to twarde przeładowanie (hard navigation), a next/link nie prefetchuje przez granicę routerów. Dopóki będzie trwała migracja, część nawigacji będzie wolniejsza niż w czysto klienckich przejściach. Jest to jednak normalne i znika, gdy wszystkie route wylądują w app/. Nie naprawiaj tego, problem rozwiąże się sam po migracji.

Plan migracji Pages Router do App Router krok po kroku

Faza 1: migracja _app.tsx i _document.tsx do layout.tsx

Code
// PRZED: pages/_app.tsx
import type { AppProps } from 'next/app'
import { ThemeProvider } from '@/components/theme-provider'
import '@/styles/globals.css'
 
export default function App({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <Header />
      <Component {...pageProps} />
      <Footer />
    </ThemeProvider>
  )
}
Code
// PRZED: pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
 
export default function Document() {
  return (
    <Html lang="pl">
      <Head>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}
Code
// PO: app/layout.tsx — łączy _app.tsx i _document.tsx
// ThemeProvider opiera się na React Context, więc jego plik musi mieć
// dyrektywę 'use client' na górze — sam layout zostaje Server Componentem.
import { ThemeProvider } from '@/components/theme-provider'
import { Header } from '@/components/header'
import { Footer } from '@/components/footer'
import '@/styles/globals.css'
 
export const metadata = {
  title: { default: 'Acme', template: '%s | Acme' },
  description: 'Tworzenie stron w Next.js',
}
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <body>
        <ThemeProvider>
          <Header />
          <main>{children}</main>
          <Footer />
        </ThemeProvider>
      </body>
    </html>
  )
}

W App Routerze <Head> z next/head jest zastępowany przez metadata, więc zamiast importować next/head w app/, należy użyć eksportu metadata albo funkcji generateMetadata.

Na tym etapie łatwo zapomnieć o dwóch kwestiach. Po pierwsze, app/layout.tsx musi samodzielnie renderować znaczniki <html> i <body>, ponieważ Next.js nie dodaje ich automatycznie, w przeciwieństwie do _document.tsx. Po drugie, nie należy od razu usuwać _app.tsx ani _document.tsx; dopóki w pages/ istnieją jakiekolwiek strony, te pliki nadal je obsługują, a style i logika z root layoutu nie mają zastosowania do tras z pages/. Można je usunąć dopiero, gdy katalog pages/ będzie całkowicie pusty.

Jeśli _app.tsx zawiera Context Providery (np. dla motywu, koszyka czy stanu autoryzacji), pamiętaj, że w app/ muszą one znaleźć się w osobnym komponencie z dyrektywą 'use client', ponieważ sam root layout pozostaje Server Componentem.

Faza 2: migracja statycznych stron do App Routera

Code
// PRZED: pages/about.tsx
import Head from 'next/head'
 
export default function About() {
  return (
    <>
      <Head>
        <title>O nas — Acme</title>
      </Head>
      <h1>O nas</h1>
      <p>Acme — agencja software'owa</p>
    </>
  )
}
Code
// PO: app/about/page.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'O nas',
  description: "Acme — agencja software'owa, Next.js, React.",
}
 
export default function AboutPage() {
  return (
    <>
      <h1>O nas</h1>
      <p>Acme — agencja software'owa</p>
    </>
  )
}

Zmiany: next/headexport const metadata, nazwa pliku about.tsxabout/page.tsx.

Faza 3: data fetching z getServerSideProps i getStaticProps do Server Components

Code
// PRZED: pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next'
 
export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getAllPosts()
  return {
    paths: posts.map((post) => ({ params: { slug: post.slug } })),
    fallback: 'blocking',
  }
}
 
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const post = await getPost(params?.slug as string)
  if (!post) return { notFound: true }
  return { props: { post }, revalidate: 3600 }
}
 
export default function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}
Code
// PO: app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
 
export const revalidate = 3600 // ISR — odpowiednik revalidate w getStaticProps
 
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((post) => ({ slug: post.slug }))
}
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) return {}
  return { title: post.title, description: post.excerpt }
}
 
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) notFound()
 
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Mapowanie:

Pages RouterApp Router
getStaticPropsasync Server Component (domyślnie SSG)
getServerSidePropsasync Server Component + export const dynamic = 'force-dynamic'
getStaticPathsgenerateStaticParams
revalidate w propsexport const revalidate = N
notFound: truenotFound() z next/navigation
fallback: 'blocking'export const dynamicParams = true (domyślne)

Faza 4: migracja API Routes do Route Handlers

w App Routerze zastępują API Routes z pages/api/. Przy okazji migracji warto rozważyć, czy dany endpoint w ogóle powinien zostać Route Handlerem, czy lepiej przepisać go na Server Action. Tutaj może pomóc artykuł Route Handlers vs Server Actions: kiedy co wybrać:

Code
// PRZED: pages/api/contact.ts
import type { NextApiRequest, NextApiResponse } from 'next'
 
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }
 
  const { name, email, message } = req.body
  // ... logika
  res.status(200).json({ success: true })
}
Code
// PO: app/api/contact/route.ts
import { NextResponse } from 'next/server'
 
export async function POST(request: Request) {
  const { name, email, message } = await request.json()
  // ... logika
  return NextResponse.json({ success: true })
}
 
// Opcjonalnie — jawny handler dla nieobsługiwanych metod nie jest potrzebny
// Next.js automatycznie zwraca 405 dla metod bez handlera

Faza 5: Client Components i dyrektywa 'use client'

W Pages Router wszystkie komponenty są Client Components, natomiast w App Router domyślnie są Server Components. Komponenty z hookami (useState, useEffect, useRouter, onClick) muszą mieć 'use client':

Code
// Ten komponent musi mieć 'use client' w App Router
'use client'
 
import { useState } from 'react'
 
export function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

Najrozsądniej dodać dyrektywę 'use client' do komponentów, które tego wymagają, ale nie dodawaj go do layout.tsx ani page.tsx. Przenoś interaktywność do osobnych Client Components.

Typowe pułapki przy migracji na App Router

useRouter w App Router: inna paczka i inne API

Code
// Pages Router
import { useRouter } from 'next/router' // next/router
 
// App Router
import { useRouter } from 'next/navigation' // next/navigation

API jest inne: router.query nie istnieje w App Router — użyj useParams() i useSearchParams().

Metadata API zamiast next/head

next/head nie działa w app/. Użyj export const metadata (statyczne) lub export async function generateMetadata (dynamiczne).

i18n w App Router: segmenty route zamiast next-i18next

next-i18next był projektowany pod Pages Router i nie przenosi się 1:1. W App Routerze internacjonalizację budujesz na segmentach [locale] plus bibliotece typu next-intl i jest to zmiana architektoniczna. W zwiazku z tym jest to osobny etap, który wymaga odpowiedniego, ostrożnego podejścia. Pełny setup wielojęzycznej strony opisuje internacjonalizacja (i18n) w Next.js App Router, a stronę SEO (hreflang, canonical) — hreflang i canonical w Next.js.

Nested layouts zamiast wzorca getLayout

Code
// PRZED: Pages Router — getLayout pattern
ProductPage.getLayout = (page) => <DashboardLayout>{page}</DashboardLayout>
 
// PO: App Router — nested layout.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
  return <DashboardShell>{children}</DashboardShell>
}

Lista kontrolna migracji Pages Router do App Router

  1. Zastąp _app.tsx i _document.tsx jednym app/layout.tsx.
  2. Przenieś metadane z next/head do eksportu metadata lub funkcji generateMetadata.
  3. Zamień getStaticProps na asynchroniczny Server Component z opcją revalidate.
  4. Zamień getServerSideProps na asynchroniczny Server Component z dynamic = 'force-dynamic'.
  5. Zastąp getStaticPaths funkcją generateStaticParams.
  6. Przenieś pages/api/*.ts do app/api/*/route.ts.
  7. Zamień importy z next/router na next/navigation.
  8. Zastąp odczyt router.query hookami useParams() i useSearchParams().
  9. Oznacz komponenty interaktywne dyrektywą 'use client'.
  10. Zamień getLayout na zagnieżdżony layout.tsx.
Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Next.js

Często zadawane pytania

Czy muszę migrować cały projekt naraz?

Nie. Pages Router i App Router współistnieją w jednym projekcie i to jest właśnie fundament inkrementalnej migracji. Przenosisz stronę po stronie, testujesz i deployujesz, zaczynając od najprostszych stron statycznych. Jedyne ograniczenie: app/ i pages/ nie mogą definiować tej samej ścieżki, bo App Router ma priorytet, a duplikat będzie psuł builda.

Jak zmapować data fetching z Pages na App Router?

getStaticProps staje się zwykłym async Server Component (domyślnie SSG), getServerSideProps to async component z export const dynamic = force-dynamic, getStaticPaths zamienia się na generateStaticParams, revalidate w props na export const revalidate = N, a notFound: true na wywołanie notFound() z next/navigation.

Czy migracja wpływa na SEO?

Nie, jeśli utrzymasz te same URL-e i strukturę treści. Metadata API generuje te same tagi HTML co next/head. Po migracji sprawdź w Google Search Console, czy zmiany nie miały wpływu na poprawność indeksacji.

Jak długo będzie wspierany Pages Router?

Next.js oficjalnie utrzymuje Pages Router jako stabilny i wspierany, aktualnie nie ma publicznej daty końca wsparcia. Nowe funkcje (Server Components, streaming, Server Actions, Parallel Routes, PPR) są jednak rozwijane głównie pod app/, więc długofalowo migracja powinna kiedyś mieć miejsce.

Co z next/router i router.query po migracji?

W App Router importujesz useRouter z next/navigation zamiast next/router, a API jest inne i router.query nie istnieje. Zamiast niego używasz useParams() do parametrów ścieżki i useSearchParams() do query stringa. To jedna z najczęstszych pułapek przy przenoszeniu interaktywnych komponentów.

Co zrobić z i18n opartym o next-i18next?

next-i18next był projektowany pod Pages Router. W App Routerze częściej wybiera się next-intl albo własną warstwę i18n opartą o segmenty route. Jeśli migrujesz projekt wielojęzyczny, zaplanuj i18n jako osobny etap, a nie część pierwszego PR-a, ponieważ to zmiana o charakterze architektonicznym.

Czy da się zautomatyzować część migracji do App Routera?

Częściowo. Next.js dostarcza oficjalny pakiet @next/codemod z transformacjami do najbardziej mechanicznych zmian. Najważniejszą jest next-async-request-api, który dodaje await do asynchronicznych params i searchParams (zmiana z Next.js 15). Uruchamiasz ją przez npx @next/codemod@latest next-async-request-api . Codemody nie przepiszą jednak logiki data fetchingu (getStaticProps na Server Component) ani routingu — to wciąż praca ręczna. Traktuj je jako akcelerator nudnych zamian, nie zamiennik strategii migracji.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
App Router czy Pages Router — co wybrać?

Next.js 13 wprowadził App Router , czyli nowy sposób budowania aplikacji oparty na React Server Components . Ale Pages Router nigdzie nie zniknął. Dwa routery, dwa podejścia, jedna decyzja do podjęcia na starcie projektu.

Maciej Sala

Maciej Sala

Founder StriveLab

Wielojęzyczna strona w Next.js App Router (i18n)

W Pages Router Next.js miał wbudowane wsparcie dla i18n i wystarczyło dodać konfigurację i18n w next.config.js , by framework zarządzał routingiem językowym automatycznie. App Router nie ma tego mechanizmu i właśnie dlatego i18n trzeba zaimplementować samodzielnie lub użyć do tego celu biblioteki. Artykuł jest o zbudowaniu wielojęzycznej strony w Next.js.

Maciej Sala

Maciej Sala

Founder StriveLab

Astro.js vs Next.js w 2026 — kompleksowe porównanie frameworków

Artykuł nie jest manifestem o porzuceniu jednej technologii na rzecz drugiej, lecz bardziej analizą, w której staram się pokazać obiektywnie, w których obszarach Astro dostarcza bezdyskusyjną przewagę wydajnościową, a w jakich scenariuszach Next.js pozostaje niezastąpionym wyborem.

Maciej Sala

Maciej Sala

Founder StriveLab