مسیرهای پویا، params ناهمگام و مسیرهای تودرتو

تا اینجا همه‌ی مسیرهای ما ثابت بودند: /about، /posts و مانند آن. اما سایتی مثل DevNotes صدها نوشته دارد و ساختن یک پوشه‌ی جدا برای هرکدام ممکن نیست. به یک مسیر نیاز داریم که بخشی از آدرسش متغیر باشد، مثل /posts/hello-nextjs و /posts/app-router-intro، و همه به یک فایل صفحه برسند. به این مسیرها مسیرهای پویا (Dynamic Routes) می‌گوییم. در این درس یاد می‌گیریم آن‌ها را بسازیم، مقدار متغیر را با params بخوانیم و مسیرهای پویای تودرتو بسازیم.

قطعه‌ی پویا (Dynamic Segment)

اگر نام یک پوشه را داخل کروشه بنویسید، مثل [slug]، آن بخش از آدرس هر مقداری می‌تواند بگیرد. نام داخل کروشه، نام پارامتر است و مقدار واقعی آدرس با همین نام در دسترس صفحه قرار می‌گیرد.

مسیر فایل آدرس نمونه params
app/posts/[slug]/page.tsx /posts/hello-nextjs { slug: "hello-nextjs" }
app/posts/[slug]/page.tsx /posts/app-router { slug: "app-router" }
app/blog/[year]/[month]/page.tsx /blog/2026/10 { year: "2026", month: "10" }

مقدار پارامتر همیشه رشته است، حتی اگر شبیه عدد باشد. در مثال بالا year رشته‌ی "2026" است، نه عدد.

«slug» نام رایج برای بخش خوانای انتهای آدرس است، مثلاً hello-nextjs. پارامتر می‌تواند هر نام دیگری داشته باشد.

params ناهمگام

در Next.js 16، params یک Promise است و دسترسی همگام به آن حذف شده است (در درس «مهم‌ترین تغییرات Next.js 16» دیدیم). برای خواندن مقدار، باید آن را await کنیم، پس تابع صفحه async می‌شود:

tsx
// app/(public)/posts/[slug]/page.tsx
type PostPageProps = {
  params: Promise<{ slug: string }>;
};

export default async function PostPage({ params }: PostPageProps) {
  const { slug } = await params;

  return (
    <main>
      <h1>نوشته: {slug}</h1>
    </main>
  );
}
  • params: Promise<{ slug: string }>: نوع props را مطابق پوشه‌ی [slug] تعریف می‌کنیم؛ نام کلید باید دقیقاً با نام داخل کروشه یکی باشد.
  • await params آبجکت پارامترها را برمی‌گرداند و با Destructuring مقدار slug را می‌گیریم.
  • تابع باید async باشد. این در Server Component مجاز است.
  • دلیل ناهمگام‌شدن، به‌طور خلاصه، این است که Next.js بتواند بخش‌هایی از صفحه را پیش از آماده‌شدن داده‌های درخواست آماده کند. جزئیات این بهینه‌سازی در فصل «Cache و Revalidation در Next.js 16» می‌آید.

params در Client Component

اگر صفحه‌ای Client Component باشد، نمی‌تواند async باشد. در آن‌جا از Hook به نام use در React استفاده می‌شود که Promise را می‌خواند:

tsx
"use client";

import { use } from "react";

type PageProps = { params: Promise<{ slug: string }> };

export default function ClientPostPage({ params }: PageProps) {
  const { slug } = use(params);

  return <h1>نوشته: {slug}</h1>;
}

در این دوره بیشتر صفحات Server Component هستند و از await استفاده می‌کنیم. این نمونه فقط برای شناخت است.

params در Layout

Layoutهایی که داخل یک قطعه‌ی پویا قرار دارند هم params را به‌صورت Promise دریافت می‌کنند، با همان الگو:

tsx
// app/(public)/posts/[slug]/layout.tsx
import type { ReactNode } from "react";

type PostLayoutProps = {
  children: ReactNode;
  params: Promise<{ slug: string }>;
};

export default async function PostLayout({
  children,
  params,
}: PostLayoutProps) {
  const { slug } = await params;

  return (
    <article data-slug={slug}>
      {children}
    </article>
  );
}

Layout فقط پارامترهای خودش و قطعه‌های بالاتر از خودش را می‌بیند، نه پارامترهای قطعه‌های پایین‌تر.

چند قطعه‌ی پویا و مسیرهای تودرتو

می‌توان چند قطعه‌ی پویا را پشت سر هم یا لابه‌لای قطعه‌های ثابت گذاشت. همه‌ی پارامترها در یک آبجکت params جمع می‌شوند:

text
app/
└── tags/
    └── [tag]/
        └── posts/
            └── [slug]/
                └── page.tsx     ← /tags/nextjs/posts/hello-nextjs
tsx
// app/tags/[tag]/posts/[slug]/page.tsx
type Props = {
  params: Promise<{ tag: string; slug: string }>;
};

export default async function TagPostPage({ params }: Props) {
  const { tag, slug } = await params;

  return (
    <p>
      برچسب: {tag} | نوشته: {slug}
    </p>
  );
}

این مثال فقط برای فهم الگو است و در DevNotes ساخته نمی‌شود. اگر پارامتری نام تکراری داشته باشد یا نوع‌ها با پوشه‌ها هماهنگ نباشند، TypeScript یا خود Next.js هنگام Build خطا می‌دهد.

عملی: مسیرهای پویای DevNotes

دو مسیر پویا به پروژه اضافه می‌کنیم:

  • /posts/[slug]: صفحه‌ی جزئیات یک نوشته در بخش عمومی.
  • /dashboard/posts/[id]/edit: صفحه‌ی ویرایش یک نوشته در داشبورد (نمونه‌ی مسیر پویای تودرتو).

فایل‌های جدید: app/(public)/posts/[slug]/page.tsx و app/(dashboard)/dashboard/posts/[id]/edit/page.tsx. فایل دیگری تغییر نمی‌کند. داده‌ی واقعی را هنوز نداریم؛ در درس «ساخت فهرست و صفحه‌ی جزئیات نوشته‌ها» با داده‌ی نمونه کاملشان می‌کنیم.

tsx
// app/(public)/posts/[slug]/page.tsx
type PostPageProps = {
  params: Promise<{ slug: string }>;
};

export default async function PostPage({ params }: PostPageProps) {
  const { slug } = await params;

  return (
    <main>
      <h1>نوشته: {slug}</h1>
      <p>محتوای این نوشته بعد از اتصال به داده نمایش داده می‌شود.</p>
    </main>
  );
}
tsx
// app/(dashboard)/dashboard/posts/[id]/edit/page.tsx
type EditPostPageProps = {
  params: Promise<{ id: string }>;
};

export default async function EditPostPage({ params }: EditPostPageProps) {
  const { id } = await params;

  return (
    <main>
      <h1>ویرایش نوشته‌ی شماره‌ی {id}</h1>
      <p>فرم ویرایش در فصل «فرم‌ها و Server Actions» ساخته می‌شود.</p>
    </main>
  );
}

خروجی و رفتار مورد انتظار

  • http://localhost:3000/posts/hello-nextjs عنوان «نوشته: hello-nextjs» را نشان می‌دهد.
  • http://localhost:3000/posts/هر-چیز-دیگر هم همین صفحه را با مقدار متفاوت نشان می‌دهد، چون هنوز بررسی نمی‌کنیم نوشته وجود دارد یا نه.
  • http://localhost:3000/dashboard/posts/12/edit عنوان «ویرایش نوشته‌ی شماره‌ی 12» را نشان می‌دهد.
  • آدرس /posts بدون تغییر فهرست را نشان می‌دهد؛ چون مسیر پویا فقط وقتی فعال می‌شود که بخش بعد از posts/ مقداری داشته باشد.

رفتار آدرس‌هایی که نوشته‌ی واقعی ندارند را در درس «Redirect، not-found.tsx و notFound()» درست می‌کنیم.

generateStaticParams: ساخت صفحات پویا از پیش

وقتی صفحه‌ی پویا داریم، Next.js پیش از دیدن آدرس واقعی نمی‌داند چه مقادیری ممکن است. اگر لیست مقادیر را از قبل بدانیم، می‌توانیم آن‌ها را هنگام Build آماده کنیم تا کاربر پاسخ آماده را سریع بگیرد. تابع generateStaticParams دقیقاً همین کار را می‌کند: آرایه‌ای از آبجکت‌های پارامتر برمی‌گرداند که هر آبجکت یک صفحه از پیش ساخته‌شده را مشخص می‌کند.

tsx
// app/(public)/posts/[slug]/page.tsx
type PostPageProps = {
  params: Promise<{ slug: string }>;
};

export async function generateStaticParams() {
  return [
    { slug: "hello-nextjs" },
    { slug: "app-router-intro" },
  ];
}

export default async function PostPage({ params }: PostPageProps) {
  const { slug } = await params;

  return (
    <main>
      <h1>نوشته: {slug}</h1>
    </main>
  );
}
  • تابع باید با نام دقیق generateStaticParams و از همان فایل page.tsx (یا layout.tsx) صادر شود.
  • هر آبجکت باید کلیدهایی هم‌نام با قطعه‌های پویای مسیر داشته باشد؛ اینجا فقط slug.
  • هنگام npm run build دو صفحه‌ی /posts/hello-nextjs و /posts/app-router-intro از قبل ساخته می‌شوند.
  • در حالت توسعه (npm run dev) صفحات همچنان هنگام درخواست ساخته می‌شوند و این پیش‌ساخت را مثل Production نمی‌بینید؛ برای دیدن اثر واقعی باید Build بگیرید.

در فصل «Rendering و Data Fetching» می‌بینیم این صفحات از پیش‌ساخته در مدل‌های رندر (Static، SSG و ISR) چه جایگاهی دارند و در درس «لایه‌ی دسترسی به داده»، این آرایه‌ی ثابت را با داده‌ی واقعی جایگزین می‌کنیم.

dynamicParams: با مقدارهای خارج از فهرست چه کنیم؟

اگر کاربر آدرسی بزند که در خروجی generateStaticParams نیست چه می‌شود؟ گزینه‌ی dynamicParams تعیین می‌کند:

مقدار رفتار برای پارامتری که از قبل ساخته نشده
true (پیش‌فرض) صفحه هنگام درخواست ساخته می‌شود و نمایش داده می‌شود.
false صفحه‌ی ۴۰۴ برگردانده می‌شود.
tsx
// app/(public)/posts/[slug]/page.tsx
export const dynamicParams = false;

با این خط، فقط دو نوشته‌ی لیست‌شده در generateStaticParams معتبرند و بقیه‌ی آدرس‌ها ۴۰۴ می‌دهند. آن را برای آزمایش اضافه کنید، آدرس /posts/something-else را باز کنید، ۴۰۴ ببینید و بعد خط را حذف کنید؛ چون در DevNotes نوشته‌های جدید هر لحظه اضافه می‌شوند و نباید فهرست ثابت بماند.

یک نکته‌ی نسخه‌محور: توضیحات این بخش برای حالت پیش‌فرض است که cacheComponents در آن خاموش است. وقتی در فصل «Cache و Revalidation در Next.js 16» آن را روشن کنیم، رفتار صفحات پویا و گزینه‌های مربوط به آن تغییر می‌کند و آن‌جا دقیقاً بررسی‌شان می‌کنیم.

نکته‌های مهم

  • مقدار پارامتر رشته است: برای استفاده‌ی عددی (مثلاً شناسه‌ی نوشته) باید آن را تبدیل و اعتبارسنجی کنید، مثلاً Number(id) و بررسی Number.isNaN. به ورودی آدرس هرگز اعتماد نکنید.
  • کاراکترهای فارسی در آدرس: کاراکترهای غیرلاتین در آدرس به‌صورت کدگذاری‌شده (مثل %D9%86...) وارد params می‌شوند و باید با decodeURIComponent برگردانده شوند. به همین دلیل در این دوره برای slug از حروف لاتین استفاده می‌کنیم.
  • نام کلید و نام پوشه یکی باشد: اگر پوشه [slug] است ولی در کد params.id بخوانید، مقدار undefined می‌گیرید و TypeScript هم معمولاً خطا می‌دهد.
  • قطعه‌ی ثابت اولویت دارد: اگر هم app/posts/new/page.tsx و هم app/posts/[slug]/page.tsx وجود داشته باشد، آدرس /posts/new به صفحه‌ی ثابت می‌رود و بقیه‌ی مقادیر به [slug].
  • همه‌ی این مسیرها از قبل عمومی‌اند: مسیر ویرایش نوشته در داشبورد هنوز محافظت نشده است. کنترل دسترسی را در فصل «Authentication و Security» می‌سازیم.

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

text
app/
├── (public)/
│   └── posts/
│       ├── page.tsx                  ← /posts
│       └── [slug]/
│           └── page.tsx              ← /posts/:slug (جدید)
└── (dashboard)/
    └── dashboard/
        └── posts/
            ├── page.tsx
            ├── new/
            │   └── page.tsx
            └── [id]/
                └── edit/
                    └── page.tsx      ← /dashboard/posts/:id/edit (جدید)

جمع‌بندی

  • پوشه‌ای با نام داخل کروشه، مثل [slug]، یک قطعه‌ی پویا می‌سازد و هر مقداری را در آن بخش آدرس می‌پذیرد.
  • در Next.js 16، params یک Promise است؛ در Server Component با await و در Client Component با use خوانده می‌شود.
  • می‌توان چند قطعه‌ی پویا را پشت سر هم یا تودرتو با قطعه‌های ثابت ترکیب کرد؛ همه‌ی پارامترها در یک آبجکت می‌آیند و مقادیرشان رشته هستند.
  • generateStaticParams فهرست مقدارهایی را که باید از قبل ساخته شوند مشخص می‌کند و dynamicParams تعیین می‌کند مقدارهای خارج از آن فهرست ۴۰۴ شوند یا هنگام درخواست ساخته شوند.
  • در DevNotes مسیرهای /posts/[slug] و /dashboard/posts/[id]/edit اضافه شدند.
  • در درس بعد، «searchParams و Query String»، یاد می‌گیریم پارامترهای بعد از علامت ? در آدرس را بخوانیم.