MATIN MOLKARA
SERIES: آموزش Next js — PART 13 OF 28

قسمت سیزدهم - Dynamic Routes در Next.js

به‌جای اینکه برای هر پروژه یک فایل ثابت بسازیم، یک segment پویا تعریف می‌کنیم و مقدار URL را از route می‌گیریم.

Dynamic Routes در Next.js: ساخت صفحه جزئیات پروژه

بعد از اینکه لیست پروژه‌ها را از دیتابیس می‌گیریم و در صفحه نمایش می‌دهیم، قدم طبیعی بعدی این است که برای هر پروژه یک صفحه جزئیات داشته باشیم.
مثلاً کاربر روی یک کارت کلیک کند و وارد مسیری مثل این شود:

/projects/portfolio-dashboard

در Next.js App Router این نوع مسیرها با Dynamic Route Segment ساخته می‌شوند.
یعنی به‌جای اینکه برای هر پروژه یک فایل ثابت بسازیم، یک segment پویا تعریف می‌کنیم و مقدار URL را از route می‌گیریم.


dynamic route چیست؟

در App Router هر بخش از URL یک segment محسوب می‌شود.
بعضی segmentها ثابت هستند، مثل:

/projects
/blog
/dashboard

اما بعضی وقت‌ها مقدار مسیر از قبل مشخص نیست.
مثلاً برای صفحه هر پروژه، تو نمی‌دانی همه URLها چه خواهند بود، چون این مقدار از داده واقعی می‌آید:

/projects/personal-portfolio
/projects/admin-dashboard
/projects/ecommerce-store

در این شرایط، Next.js اجازه می‌دهد یک پوشه با نامی داخل براکت بسازیم:

[slug]

این یعنی آن بخش از URL پویا است و مقدارش از آدرس خوانده می‌شود.

برای مثال:

app/projects/[slug]/page.tsx

در این ساختار، هر URL از نوع /projects/something به همین صفحه می‌رسد و مقدار something از طریق params در اختیار صفحه قرار می‌گیرد.

در داک رسمی Next.js این مقدارها با نام‌هایی مثل path params، route params یا URL params هم شناخته می‌شوند.


ساخت پروژه [slug]

اگر صفحه لیست پروژه‌ها را از قبل داری، ساختار route جزئیات پروژه معمولاً به این شکل خواهد بود:

app/
  projects/
    page.tsx
    [slug]/
      page.tsx

در اینجا:

  • app/projects/page.tsx صفحه لیست پروژه‌ها است
  • app/projects/[slug]/page.tsx صفحه جزئیات هر پروژه است

یعنی:

  • /projects لیست را نشان می‌دهد
  • /projects/portfolio-dashboard جزئیات یک پروژه را نشان می‌دهد
  • /projects/my-nextjs-cms جزئیات پروژه دیگری را نشان می‌دهد

این مدل برای پرتفولیو بسیار مناسب است، چون URLها خوانا، تمیز و قابل اشتراک‌گذاری می‌مانند.


دریافت پارامتر route

در Next.js 16، مقدار params در page به‌صورت Promise به صفحه می‌رسد.
یعنی باید آن را await کنی.

نمونه پایه:

// app/projects/[slug]/page.tsx
export default async function ProjectPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params

  return <div>Project slug: {slug}</div>
}

این کد یعنی اگر کاربر وارد این مسیر شود:

/projects/portfolio-dashboard

مقدار slug برابر خواهد بود با:

portfolio-dashboard

این نکته مهم است، چون در نسخه‌های قدیمی‌تر params رفتار synchronous داشت، اما در Next.js جدید باید آن را به‌عنوان Promise در نظر بگیریم.

اگر از تایپ‌های کمکی خود فریم‌ورک استفاده کنیم، می‌توانی این را تمیزتر هم بنویسی:

// app/projects/[slug]/page.tsx
export default async function ProjectPage(
  props: PageProps<'/projects/[slug]'>
) {
  const { slug } = await props.params

  return <div>Project slug: {slug}</div>
}

پیدا کردن داده بر اساس slug

هدف اصلی از dynamic route فقط خواندن URL نیست.
نکته مهم این است که با استفاده از slug، داده درست را از data layer پیدا کنیم.

اگر در پست‌های قبلی یک تابع مثل getProjectBySlug() ساخته باشیم، اینجا دقیقاً زمان استفاده از آن است.

مثلاً:

// lib/data/projects.ts
import { prisma } from '@/lib/prisma'

export async function getProjectBySlug(slug: string) {
  return prisma.project.findUnique({
    where: { slug },
  })
}

و بعد در صفحه:

// app/projects/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getProjectBySlug } from '@/lib/data/projects'

export default async function ProjectPage(
  props: PageProps<'/projects/[slug]'>
) {
  const { slug } = await props.params
  const project = await getProjectBySlug(slug)

  if (!project) {
    notFound()
  }

  return (
    <article className="space-y-6">
      <header className="space-y-2">
        <h1 className="text-3xl font-semibold text-white">{project.title}</h1>
        {project.description ? (
          <p className="text-sm leading-7 text-white/70">
            {project.description}
          </p>
        ) : null}
      </header>

      <section className="space-y-4">
        {project.content ? (
          <div className="prose prose-invert max-w-none">
            {project.content}
          </div>
        ) : null}
      </section>
    </article>
  )
}

در این الگو چند نکته مهم وجود دارد:

  • slug از URL گرفته می‌شود
  • query مستقیم داخل UI پخش نشده و در data layer نگه داشته شده
  • اگر پروژه پیدا نشود، با notFound() مسیر به not-found.tsx می‌رود
  • صفحه فقط مسئول render کردن داده نهایی است

این همان الگوی تمیزی است که در بقیه پروژه هم بهتر است حفظ شود.


چرا slug مهم‌تر از id است؟

در بسیاری از پروژه‌ها از نظر داخلی رکوردها id دارند، اما برای URL عمومی معمولاً slug انتخاب بهتری است.

مثلاً این URL:

/projects/portfolio-dashboard

از این یکی بهتر است:

/projects/42

چون:

  • خواناتر است
  • برای SEO مناسب‌تر است
  • اشتراک‌گذاری آن راحت‌تر است
  • از نظر محتوا به کاربر context می‌دهد

به همین دلیل در پروژه‌های محتوایی، پرتفولیو، بلاگ و CMS معمولاً slug یک فیلد مهم در مدل داده است و باید unique باشد.

مثلاً در Prisma:

model Project {
  id          String   @id @default(cuid())
  title       String
  slug        String   @unique
  description String?
  content     String?
  published   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

اگر پروژه پیدا نشد چه کنیم؟

در dynamic route همیشه باید این فرض را داشته باشیم که هر URLای ممکن است وارد شود.
کاربر می‌تواند هر چیزی را دستی داخل آدرس‌بار بنویسد.

پس حتی اگر همه لینک‌های داخل UI درست باشند، باز هم این حالت ممکن است:

/projects/something-that-does-not-exist

در این شرایط، رفتار درست این است که اگر داده‌ای پیدا نشد، notFound() را صدا بزنیم:

if (!project) {
  notFound()
}

این کار باعث می‌شود Next.js صفحه not-found.tsx همان segment را نمایش دهد.
این دقیقاً همان چیزی است که در پست loading، error و not-found توضیح داده بودیم، و اینجا به‌صورت عملی وارد جریان route می‌شود.


آیا این صفحه همیشه داینامیک رندر می‌شود؟

نه لزوماً.
خود dynamic route به این معنی نیست که همه‌چیز فقط در runtime ساخته شود.

در داک رسمی، Next.js توضیح می‌دهد که اگر از generateStaticParams() استفاده کنیم، می‌توانیم بعضی یا همه مسیرهای [slug] را از قبل در build تولید کنیم.

برای مثال:

// app/projects/[slug]/page.tsx
import { getProjects, getProjectBySlug } from '@/lib/data/projects'
import { notFound } from 'next/navigation'

export async function generateStaticParams() {
  const projects = await getProjects()

  return projects.map((project) => ({
    slug: project.slug,
  }))
}

export default async function ProjectPage(
  props: PageProps<'/projects/[slug]'>
) {
  const { slug } = await props.params
  const project = await getProjectBySlug(slug)

  if (!project) {
    notFound()
  }

  return <div>{project.title}</div>
}

اینجا Next.js در زمان build لیستی از slugها را می‌گیرد و برای آن‌ها صفحه تولید می‌کند.

این قابلیت برای پرتفولیو و بلاگ خیلی مفید است، چون معمولاً تعداد پروژه‌ها محدود و قابل پیش‌بینی است.
یعنی می‌توانیم صفحه‌ها را از قبل آماده کنیم و در عین حال ساختار dynamic route را هم حفظ کنیم.


جمع‌بندی

در Next.js App Router، ساخت صفحه جزئیات پروژه با dynamic route بسیار مستقیم و تمیز است.

کافی است:

  • یک پوشه با نام [slug] بسازیم
  • مقدار route را از params بگیریم
  • با آن slug داده را از data layer پیدا کنیم
  • و اگر داده‌ای وجود نداشت، notFound() را صدا بزنیم

در عمل، این الگو یکی از مهم‌ترین بخش‌های هر پرتفولیو، بلاگ یا CMS است، چون URLهای واقعی پروژه را به داده واقعی وصل می‌کند.

// 0 comments
#Next.js#DynamicRoute#آموزش

نظرات (0)

نظر خود را بنویسید