مهاجرت از Pages Router به App Router در Next.js 15: پلی‌بوک کامل با تخمین زمان (۲۰۲۶)

راهنمای عملی مهاجرت از Pages Router به App Router در Next.js 15 با تخمین واقعی زمان بر اساس اندازه پروژه، تبدیل کد getServerSideProps و API Routes، و راه‌حل مشکلات رایج hydration و کش.

مهاجرت به App Router در Next.js 15 (۲۰۲۶)

به‌روزرسانی: ۵ آگوست ۲۰۲۶

مهاجرت از Pages Router به App Router در Next.js 15 برای پروژه‌های کوچک بین ۳ تا ۵ روز کاری و برای پروژه‌های بزرگ (بیش از ۱۰۰ روت) بین ۳ تا ۶ هفته زمان می‌برد، اما لازم نیست همه چیز را یکجا مهاجرت کنید؛ این دو روتر می‌توانند در کنار هم و در یک پروژه به صورت تدریجی زندگی کنند. من در یک سال گذشته پنج تیم را از Pages Router به App Router منتقل کرده‌ام و در این پلی‌بوک، مسیر مرحله به مرحله، تخمین‌های واقعی زمان، و همه دام‌هایی که هر تیم درگیرشان شد را جمع‌بندی کرده‌ام تا شما مجبور نباشید دوباره آن‌ها را کشف کنید.

  • App Router جایگزینی اجباری نیست؛ Pages Router در Next.js 15 همچنان پشتیبانی می‌شود و می‌توانید هر دو را در یک پروژه اجرا کنید.
  • تخمین واقعی: ۳ تا ۵ روز برای زیر ۲۰ روت، ۲ تا ۳ هفته برای ۲۰ تا ۱۰۰ روت، و ۳ تا ۶ هفته برای بیش از ۱۰۰ روت (به همراه بازنویسی تست‌ها).
  • getServerSideProps به fetch با cache: 'no-store' در Server Component تبدیل می‌شود؛ getStaticProps به fetch با next: { revalidate }.
  • API Routes همچنان کار می‌کنند اما در App Router با Route Handlers جایگزین می‌شوند که امضای Request/Response استاندارد Web دارند.
  • Middleware بین دو روتر مشترک است و نیاز به تغییر ندارد. راستش را بخواهید، این ساده‌ترین بخش مهاجرت است.
  • سنگین‌ترین بخش مهاجرت، بازنویسی هوک‌های وابسته به useRouter از next/router به سه هوک جدید useRouter, usePathname, و useSearchParams از next/navigation است.

آیا در سال ۲۰۲۶ باید به App Router مهاجرت کنم؟

پاسخ صادقانه من: بستگی دارد، و این یک پاسخ فرار نیست. اگر اپلیکیشن شما یک پروژه کوچک با کمتر از ۲۰ صفحه است که در Pages Router به خوبی کار می‌کند، هزینه‌فرصت مهاجرت را در برابر مزایای واقعی وزن کنید. اما اگر تیم شما در حال ساخت قابلیت‌های جدید است، اگر به Partial Prerendering علاقه دارید، اگر از React Server Components برای کم کردن حجم باندل کلاینت استفاده می‌کنید، یا اگر می‌خواهید Streaming و Suspense را به شکل مدرن به کار ببرید، مهاجرت را جدی بگیرید.

در Next.js 15، تیم Vercel به‌روزرسانی‌های جدید Pages Router را عملاً متوقف کرده؛ همه ویژگی‌های جدید (Server Actions، Partial Prerendering، تازه‌سازی کش پیشرفته، و بهینه‌سازی‌های Turbopack) روی App Router متمرکز است. بنابراین Pages Router در وضعیت «نگهداری» قرار دارد، نه رشد. طبق راهنمای رسمی مهاجرت Next.js، App Router مسیر پیشنهادی برای تمام پروژه‌های جدید است.

تخمین زمان مهاجرت بر اساس اندازه پروژه

در تجربه من با پنج مهاجرت واقعی در بیزنس‌های تولیدی، این جدول تخمین دقیقی از زمان مورد نیاز است (با فرض یک توسعه‌دهنده تمام‌وقت با تجربه‌ی Next.js):

اندازه پروژهتعداد روتزمان مهاجرت (روز کاری)نکته
کوچککمتر از ۲۰۳ تا ۵ روزمعمولاً یکجا مهاجرت می‌شود.
متوسط۲۰ تا ۵۰۷ تا ۱۲ روزمهاجرت تدریجی توصیه می‌شود.
بزرگ۵۰ تا ۱۰۰۱۲ تا ۲۰ روزحتماً روت به روت مهاجرت کنید.
خیلی بزرگ۱۰۰ به بالا۳ تا ۶ هفتهیک نفر full-time اختصاصی نیاز است.

این اعداد شامل بازنویسی تست‌های کامپوننت‌ها هم می‌شوند، که معمولاً حدود ۳۰٪ کل زمان مهاجرت را می‌گیرد. اگر تست‌های شما با React Testing Library روی رندر کلاینتی نوشته شده‌اند، انتظار بازنویسی مقادیر قابل توجهی از آن‌ها را داشته باشید. Server Component ها را نمی‌توان با render() کلاسیک تست کرد و به Playwright یا Vitest با محیط سرور نیاز خواهید داشت.

نکته مهم دیگر: مهاجرت CSS Modules ساده است، اما اگر از styled-jsx به شکل گسترده در _document.tsx استفاده کرده‌اید، برنامه‌ی جایگزینی به CSS-in-JS مدرن مثل vanilla-extract یا Tailwind را داشته باشید، چون بسیاری از کتابخانه‌های CSS-in-JS سرورساید هنوز با RSC مشکل دارند.

مقایسه Pages Router و App Router

قبل از رفتن سراغ کد، بگذارید یک مقایسه‌ی منصفانه از این دو روتر داشته باشیم. من طرفدار هیچ کدام نیستم؛ هر کدام مصالحه‌های خودشان را دارند:

ویژگیPages RouterApp Router
روش رندر پیش‌فرضClient Component + hydrationServer Component
واکشی دادهgetServerSideProps, getStaticPropsfetch در Server Component + کش
API هاAPI Routes (pages/api)Route Handlers (app/api/route.ts)
Layout ها_app.tsx و nested manuallylayout.tsx در هر پوشه
مدیریت خطاError Boundaries دستیerror.tsx, not-found.tsx
Loading UIState های دستیloading.tsx + Suspense
مانیپولاسیون دادهAPI + fetch در کلاینتServer Actions
Metadata و SEOnext/headMetadata API
پشتیبانی از PPRخیربله
حجم باندل کلاینتمعمولاً بیشترمعمولاً ۲۰ تا ۴۰٪ کمتر
منحنی یادگیریملایم‌تر (مدل ذهنی آشنا)تندتر (مفاهیم RSC جدید)

در گذشته، Pages Router به دلیل مدل ذهنی ساده‌تر، انتخاب من برای نمونه‌های اولیه سریع بود. اما در سال ۲۰۲۶، App Router به بلوغ رسیده و کش سازی و Server Actions به شکلی که در Pages Router هرگز ممکن نبود، توسعه را سریع‌تر می‌کنند. برای درک عمیق‌تر مدل کش‌سازی و Server Actions، مقاله‌ی واکشی داده، کشینگ و Server Actions در Next.js 15 را بخوانید. اگر PPR هم برایتان جذاب است، راهنمای Partial Prerendering در Next.js 15 نقطه‌ی شروع خوبی است.

آیا Pages Router و App Router می‌توانند همزمان اجرا شوند؟

بله، و این ویژگی کلیدی است که مهاجرت‌های واقعی را ممکن می‌کند. Next.js اجازه می‌دهد پوشه‌های pages/ و app/ در یک پروژه کنار هم باشند. روت‌ها بر اساس اولویت رفع می‌شوند: اگر یک روت هم در app/ و هم در pages/ تعریف شده باشد، App Router برنده است.

my-nextjs-app/
├── app/
│   ├── layout.tsx         # روت جدید در App Router
│   ├── page.tsx           # صفحه‌ی اصلی جدید
│   └── dashboard/
│       └── page.tsx       # /dashboard مهاجرت شده
├── pages/
│   ├── _app.tsx           # هنوز استفاده می‌شود برای روت‌های pages
│   ├── about.tsx          # /about هنوز در Pages Router
│   ├── products/
│   │   └── [id].tsx       # /products/:id هنوز در Pages Router
│   └── api/
│       └── legacy.ts      # API قدیمی همچنان کار می‌کند
└── middleware.ts          # مشترک بین دو روتر

استراتژی توصیه‌شده من: از صفحاتی که کمترین ترافیک را دارند شروع کنید (مثلاً صفحه About، Contact، Privacy Policy)، مکانیزم استقرار را با هر مهاجرت تست کنید، و بعد بروید سراغ صفحات پرترافیک. این رویکرد ریسک را کم می‌کند و به تیم اجازه می‌دهد الگوها را در یک محیط ایمن یاد بگیرد.

تبدیل _app.tsx و _document.tsx به layout.tsx

در Pages Router، فایل _app.tsx برای provider ها و _document.tsx برای اسکلت HTML به کار می‌رفت. در App Router، هر دوی این‌ها با یک فایل واحد app/layout.tsx جایگزین می‌شوند:

// قبل: pages/_app.tsx
import type { AppProps } from 'next/app'
import { ThemeProvider } from '@/lib/theme'
import '@/styles/globals.css'

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  )
}

// قبل: pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
    <Html lang="fa" dir="rtl">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

هر دو فایل بالا در App Router به شکل زیر تبدیل می‌شوند:

// بعد: app/layout.tsx
import type { Metadata } from 'next'
import { ThemeProvider } from '@/lib/theme'
import '@/styles/globals.css'

export const metadata: Metadata = {
  title: 'اپلیکیشن من',
  description: 'با Next.js 15 ساخته شده',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="fa" dir="rtl">
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

نکته مهم: ThemeProvider احتمالاً به context React نیاز دارد و بنابراین باید Client Component باشد. آن را در فایلی جداگانه با 'use client' در بالا تعریف کنید و از layout سرورساید مصرف کنید. این الگوی «مرز کلاینت را حداکثر پایین بکش» یکی از مهم‌ترین اصول App Router است — Provider ها را در یک کامپوننت کلاینتی جدا کنید تا محتوای صفحه بتواند Server Component بماند.

تبدیل getServerSideProps و getStaticProps

این معمولاً بیشترین بخش کد را در طول مهاجرت تغییر می‌دهد. خبر خوب: کد نتیجه معمولاً کوتاه‌تر و خواناتر است. یک مقایسه‌ی سریع:

// قبل: pages/products/[id].tsx
import type { GetServerSideProps } from 'next'

interface Product { id: string; name: string; price: number }

export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  const res = await fetch(`https://api.example.com/products/${params?.id}`)
  if (!res.ok) return { notFound: true }
  const product: Product = await res.json()
  return { props: { product } }
}

export default function ProductPage({ product }: { product: Product }) {
  return <h1>{product.name} - {product.price} تومان</h1>
}

معادل آن در App Router به شکل زیر است. یک فایل کوتاه‌تر و بدون سربار type props:

// بعد: app/products/[id]/page.tsx
import { notFound } from 'next/navigation'

interface Product { id: string; name: string; price: number }

async function getProduct(id: string): Promise<Product | null> {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    cache: 'no-store', // معادل getServerSideProps
  })
  if (!res.ok) return null
  return res.json()
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const product = await getProduct(id)
  if (!product) notFound()
  return <h1>{product.name} - {product.price} تومان</h1>
}

برای getStaticProps، به جای cache: 'no-store' از next: { revalidate: 3600 } استفاده کنید. اگر پارامترهای دینامیک دارید که در زمان بیلد باید تولید شوند، به جای getStaticPaths از تابع generateStaticParams استفاده کنید:

// معادل getStaticPaths در App Router
export async function generateStaticParams() {
  const products = await fetch('https://api.example.com/products').then(r => r.json())
  return products.map((p: Product) => ({ id: p.id }))
}

در Next.js 15، توجه داشته باشید که params و searchParams اکنون Promise هستند. این یک تغییر breaking نسبت به Next.js 14 است و باید await شوند (اولین باری که این را دیدم، چند دقیقه گیج شدم چرا params خالی است). اگر می‌خواهید مدل ذهنی عمیق‌تری از کش سازی داشته باشید، راهنمای واکشی داده و کشینگ ما توضیح می‌دهد که چه زمانی از no-store، force-cache، یا revalidate استفاده کنید.

مهاجرت API Routes به Route Handlers

API Routes در Pages Router هنوز کار می‌کنند و در App Router توسط Route Handlers جایگزین می‌شوند. تفاوت اصلی: Route Handlers از استانداردهای Web (Request و Response) استفاده می‌کنند، نه از شیء اختصاصی Next.js. این یعنی کد شما قابل حمل‌تر است و می‌تواند در Edge Runtime، Cloudflare Workers، یا Deno هم اجرا شود.

// قبل: pages/api/users/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { id } = req.query
  if (req.method === 'GET') {
    const user = await db.user.findUnique({ where: { id: String(id) } })
    return res.status(200).json(user)
  }
  if (req.method === 'DELETE') {
    await db.user.delete({ where: { id: String(id) } })
    return res.status(204).end()
  }
  return res.status(405).json({ error: 'Method not allowed' })
}

معادل آن به شکل Route Handler:

// بعد: app/api/users/[id]/route.ts
import { NextResponse } from 'next/server'

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const user = await db.user.findUnique({ where: { id } })
  return NextResponse.json(user)
}

export async function DELETE(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  await db.user.delete({ where: { id } })
  return new Response(null, { status: 204 })
}

در Route Handlers، به جای بررسی req.method، برای هر method HTTP یک تابع export می‌کنید. Method های پشتیبانی‌نشده به‌طور خودکار پاسخ 405 برمی‌گردانند. این تفکیک تمیزتر است و type safety بهتری می‌دهد. اگر می‌خواهید عمق بیشتری از این الگو و ترکیبش با middleware داشته باشید، مقاله‌ی میدلور Next.js 15 و الگوهای پیشرفته ما را بخوانید.

جایگزینی next/head با Metadata API

یکی از تمیزترین بهبودهای App Router، Metadata API است. به جای مدیریت دستی <title> و <meta> در هر صفحه با next/head، متادیتا را به شکل declarative export می‌کنید:

// قبل: pages/products/[id].tsx
import Head from 'next/head'

export default function ProductPage({ product }) {
  return (
    <>
      <Head>
        <title>{product.name} | فروشگاه ما</title>
        <meta name="description" content={product.description} />
        <meta property="og:image" content={product.image} />
      </Head>
      {/* محتوا */}
    </>
  )
}

// بعد: app/products/[id]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>
}): Promise<Metadata> {
  const { id } = await params
  const product = await getProduct(id)
  return {
    title: `${product.name} | فروشگاه ما`,
    description: product.description,
    openGraph: { images: [product.image] },
  }
}

export default async function ProductPage({ params }) {
  // ...
}

Metadata API یک مزیت مهم دارد: کش fetch بین generateMetadata و کامپوننت صفحه به اشتراک گذاشته می‌شود. یعنی اگر در هر دو تابع، getProduct(id) را صدا بزنید، فقط یک بار درخواست شبکه انجام می‌شود. برای جزئیات کامل، به مستندات رسمی generateMetadata مراجعه کنید.

تبدیل useRouter و ناوبری کلاینت‌ساید

این احتمالاً بخشی است که بیشترین باگ‌های سکوت‌آمیز را در مهاجرت‌های من ایجاد کرده. در Pages Router، هوک useRouter از next/router همه چیز را برمی‌گرداند: pathname, query, push(), و رویدادها. در App Router، این ویژگی‌ها بین سه هوک تقسیم شده‌اند و همه از next/navigation import می‌شوند:

// قبل: در Pages Router
'use client'
import { useRouter } from 'next/router'

function SearchBox() {
  const router = useRouter()
  const q = router.query.q as string
  return (
    <input
      value={q || ''}
      onChange={(e) => router.push(`?q=${e.target.value}`)}
    />
  )
}

// بعد: در App Router
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'

function SearchBox() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()
  const q = searchParams.get('q') ?? ''
  return (
    <input
      value={q}
      onChange={(e) => {
        const params = new URLSearchParams(searchParams)
        params.set('q', e.target.value)
        router.push(`${pathname}?${params.toString()}`)
      }}
    />
  )
}

تفاوت‌های مهم دیگر: رویدادهای router (مثل routeChangeStart) دیگر وجود ندارند و به جای آن‌ها از تغییرات pathname و searchParams در یک useEffect استفاده کنید. متد router.reload() با router.refresh() جایگزین شده که کش سرور را هم تازه می‌کند. و router.push() دیگر یک آبجکت { pathname, query } نمی‌گیرد؛ فقط یک URL رشته‌ای می‌گیرد.

مشکلات رایج و راه‌حل‌ها

این‌ها متداول‌ترین مشکلاتی هستند که تیم‌های من در مهاجرت با آن‌ها برخورد کرده‌اند:

خطاهای Hydration پس از مهاجرت

معمولاً چون یک Client Component سعی می‌کند window, localStorage, یا document را در فاز اولیه رندر مصرف کند. راه حل: به useEffect منتقل کنید، یا از الگوی dynamic import با ssr: false استفاده کنید (فقط از Client Component ها). طبق مستندات React 19، خطاهای hydration اکنون پیام‌های واضح‌تری با اشاره به عنصر مشکل‌ساز نمایش می‌دهند.

Context در Server Components کار نمی‌کند

Context API فقط در Client Component ها کار می‌کند. اگر state سراسری در Server Component لازم است، به جای آن از cookies، searchParams، یا الگوی «cache function» React 19 استفاده کنید. اگر واقعاً به context نیاز دارید، Provider را به عنوان Client Component بسازید و در layout.tsx بگذارید.

باگ‌های کش‌سازی غیرمنتظره

Next.js 15 مدل کش‌سازی جدیدی دارد که رفتار پیش‌فرض را از agressive-cache به sensible-defaults تغییر داده. اگر داده‌های شما به شکل غیرمنتظره کش نمی‌شوند (یا برعکس)، استراتژی کش هر route را به شکل صریح تعیین کنید: export const revalidate = 60 یا export const dynamic = 'force-dynamic'. این بخش را جدی بگیرید — من دو بار در ماه اول مهاجرت به این خاطر باگ داشتم.

CSS-in-JS از کار می‌افتد

کتابخانه‌های CSS-in-JS runtime-based (مثل styled-components و emotion) نیاز به config خاص برای SSR در App Router دارند. من پیشنهاد می‌کنم به Tailwind، CSS Modules، یا vanilla-extract مهاجرت کنید؛ این‌ها با RSC سازگارتر هستند و باندل کوچک‌تری تولید می‌کنند. برای پروژه‌های بزرگ، این تصمیم می‌تواند یک هفته به مهاجرت اضافه کند اما ارزشش را دارد.

تست‌ها خراب می‌شوند

Server Components async هستند و با jest-environment-jsdom استاندارد کار نمی‌کنند. به Vitest با @testing-library/react نسخه ۱۶+ مهاجرت کنید، یا برای صفحات مهم E2E test با Playwright بنویسید. برای منطق تجاری خالص که هیچ ربطی به React ندارد، تست‌های واحد را می‌توانید نگه دارید.

سوالات متداول

آیا Pages Router در Next.js 15 منسوخ شده است؟

نه، Pages Router هنوز کاملاً پشتیبانی می‌شود و در Next.js 15 هیچ اعلان deprecation رسمی برایش داده نشده. اما توسعه‌ی ویژگی‌های جدید (Server Actions, PPR, Turbopack اختصاصی) روی App Router متمرکز است، بنابراین Pages Router در حالت «نگهداری» قرار دارد.

آیا App Router سریع‌تر از Pages Router است؟

در بیشتر موارد، بله. App Router به دلیل Server Components باندل کلاینت کوچک‌تری تولید می‌کند (معمولاً ۲۰ تا ۴۰٪ کمتر)، Streaming واقعی از سرور به کلاینت را ممکن می‌کند، و از Partial Prerendering پشتیبانی می‌کند که TTFB (Time To First Byte) را به شکل چشمگیری کاهش می‌دهد.

آیا می‌توانم API Routes قدیمی را نگه دارم و فقط صفحات را مهاجرت کنم؟

بله، این یک استراتژی رایج و توصیه‌شده است. API Routes در pages/api/ کار خود را ادامه می‌دهند حتی وقتی همه‌ی صفحات به app/ منتقل شده‌اند. می‌توانید بعداً و در فاز جداگانه‌ای، API Routes را به Route Handlers مهاجرت کنید.

Middleware چطور؟ آیا نیاز به تغییر دارد؟

خیر، فایل middleware.ts در ریشه‌ی پروژه بین Pages Router و App Router مشترک است و روی هر دو نوع روت اجرا می‌شود. این ساده‌ترین بخش مهاجرت است و تقریباً کاری برای انجام نیست.

آیا next-i18next با App Router کار می‌کند؟

خیر، next-i18next فقط برای Pages Router طراحی شده. برای App Router باید به کتابخانه‌ی next-intl یا next-international مهاجرت کنید. اگر می‌خواهید سریع شروع کنید، راهنمای i18n در Next.js 15 با next-intl و پشتیبانی RTL ما را بخوانید.

Jasmine Patel
درباره نویسنده Jasmine Patel

Web framework specialist comparing Next.js to everything else so you don't have to. Migrates teams off legacy stacks for fun.