قسمت سیزدهم - 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های واقعی پروژه را به داده واقعی وصل میکند.