Redirect، not-found.tsx و notFound()
دو موقعیت در هر وبسایتی پیش میآید: گاهی باید کاربر را از یک آدرس به آدرس دیگری بفرستیم (مثلاً آدرس قدیمی که منتقل شده) و گاهی آدرسی درخواست میشود که محتوایی ندارد (نوشتهای با آن شناسه وجود ندارد). Next.js برای هر دو ابزار آماده دارد: redirect() برای هدایت، و notFound() همراه فایل not-found.tsx برای صفحهی «پیدا نشد». در این درس هر دو را میشناسیم و به DevNotes اضافه میکنیم.
هدایت (Redirect) چیست؟
Redirect یعنی سرور بهجای نمایش صفحهی درخواستشده، به مرورگر بگوید «به آدرس دیگری برو». در Next.js سه راه برای این کار هست:
| روش | کجا کار میکند | مناسب برای |
|---|---|---|
redirect() |
Server Component، Server Action و Route Handler | هدایتی که به شرط یا دادهی زمان اجرا بستگی دارد |
permanentRedirect() |
همان جاها | هدایت دائمی که به مرورگر و موتورهای جستوجو میگوید آدرس برای همیشه عوض شده |
گزینهی redirects در next.config.ts |
سطح پیکربندی، قبل از مسیریابی | قواعد ثابت، مثل آدرسهای قدیمی |
ناوبری سمت کلاینت با useRouter یا Link (که در درسهای قبل دیدیم) روشهای دیگری برای رفتن به صفحهی دیگر هستند، اما هدایت واقعی سمت سرور هنگام رندر با redirect() انجام میشود.
تابع redirect()
تابع redirect را از next/navigation وارد میکنیم و آدرس مقصد را به آن میدهیم:
import { redirect } from "next/navigation";
export default async function OldPage() {
// کارهای لازم...
redirect("/posts");
}- کد وضعیت پاسخ معمولاً
307(هدایت موقت) است؛ در Server Action کد303استفاده میشود.permanentRedirectکد308میدهد. - آرگومان دوم اختیاری است و نوع ناوبری را مشخص میکند:
"replace"رکورد فعلی تاریخچه را جایگزین میکند و"push"رکورد جدید میافزاید. پیشفرض در Server Action برابرpushو در بقیهی جاهاreplaceاست. - این تابع مقداری برنمیگرداند؛ اجرای تابع همانجا قطع میشود و کد بعد از آن اجرا نمیشود.
- در Client Component فقط هنگام رندر قابل استفاده است. برای هدایت بعد از یک رویداد (مثل کلیک)، از
router.pushاستفاده میکنیم.
مثال مفهومی: ورود به داشبورد
الگوی رایج این است که صفحهی خصوصی بررسی کند کاربر وارد شده یا نه. چون هنوز احراز هویت نساختهایم، این مثال فقط مفهومی است و در پروژه اعمال نمیشود. بخش getCurrentUser را در فصل «Authentication و Security» میسازیم:
// نمونهی مفهومی؛ در DevNotes هنوز اعمال نمیشود
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/auth";
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) {
redirect("/login");
}
return <h1>سلام {user.name}</h1>;
}هدایتهای ثابت در next.config.ts
اگر قاعدهی هدایت به شرایط زمان اجرا بستگی ندارد، بهتر است در پیکربندی تعریف شود. مثلاً فرض کنید بخش نوشتههای ما قبلاً با نام /blog منتشر شده بود و حالا /posts است. میخواهیم آدرسهای قدیمی بشکنند نه. این قاعدهها را به next.config.ts اضافه میکنیم:
فایل متأثر: next.config.ts
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async redirects() {
return [
{
source: "/blog",
destination: "/posts",
permanent: true,
},
{
source: "/blog/:slug",
destination: "/posts/:slug",
permanent: true,
},
];
},
};
export default nextConfig;source: الگوی آدرس قدیمی.:slugیک پارامتر نامدار است و درdestinationدوباره استفاده میشود.permanent: true: هدایت دائمی با کد308. مرورگرها و موتورهای جستوجو مقصد را به خاطر میسپارند. اگر هدایت موقت استfalseبگذارید (کد307).- پس از تغییر
next.config.tsباید سرور توسعه را باCtrl + Cمتوقف و دوباره اجرا کنید. - چون مرورگر هدایت دائمی را کش میکند، هنگام آزمایش اگر بعداً قاعده را عوض کردید ممکن است رفتار قدیمی را ببینید؛ در پنجرهی ناشناس (Incognito) امتحان کنید.
خروجی مورد انتظار
/blog← به/postsمیرود./blog/hello-nextjs← به/posts/hello-nextjsمیرود.
صفحهی «پیدا نشد» و notFound()
وقتی کاربر آدرسی میزند که اصلاً مسیری برایش نیست، Next.js خودش صفحهی ۴۰۴ را نشان میدهد. اما اگر مسیر وجود دارد ولی داده وجود ندارد (مثلاً /posts/hello که نوشتهای با این slug نداریم)، خودمان باید تصمیم بگیریم. تابع notFound() به Next.js میگوید «این منبع وجود ندارد» و صفحهی ۴۰۴ را با کد وضعیت ۴۰۴ نمایش میدهد.
import { notFound } from "next/navigation";
// ...
if (!post) {
notFound();
}
مثل redirect()، این تابع هم برنمیگردد. همین باعث میشود TypeScript بعد از آن بداند post دیگر undefined نیست.
نقش not-found.tsx
فایل not-found.tsx ظاهر صفحهی ۴۰۴ را تعیین میکند. دو رفتار مهم:
not-found.tsxدر ریشهیappهم برای آدرسهای بدون مسیر و هم برایnotFound()هایی که نزدیکتر از آن فایلی ندارند استفاده میشود. این همان فایلی است که در درس «فایلهای ویژهی App Router» ساختیم.- میتوان در هر پوشه یک
not-found.tsxاختصاصی گذاشت. وقتیnotFound()صدا زده شود، Next.js نزدیکترین فایل را بالادستی پیدا میکند و از آن استفاده میکند. این فایلهای تودرتو فقط برایnotFound()اجرا میشوند؛ آدرسهایی که اصلاً مسیر ندارند همیشه بهnot-found.tsxریشه میروند.
مزیت فایل اختصاصی این است که داخل Layoutهای همان بخش رندر میشود. صفحهی ۴۰۴ ریشه فقط داخل Root Layout رندر میشود و هدر و فوتر گروه (public) را ندارد.
عملی: دادهی نمونهی موقت
برای اینکه بتوانیم «نوشته پیدا نشد» را آزمایش کنیم، به منبعی از نوشتهها نیاز داریم. هنوز پایگاه داده نداریم، پس یک فایل موقت با چند نوشتهی نمونه میسازیم. توابع آن را async نوشتهایم تا بعداً در فصل «پایگاه داده با Prisma و SQLite» بتوانیم بدون تغییر در صفحهها، پیادهسازی را با Prisma جایگزین کنیم.
فایل جدید: lib/posts.ts
// lib/posts.ts
// لایهی موقت داده برای آموزش؛ در محیط Production مناسب نیست.
// در فصل «پایگاه داده با Prisma و SQLite» با پایگاه داده جایگزین میشود.
export type Post = {
slug: string;
title: string;
excerpt: string;
body: string;
};
const posts: Post[] = [
{
slug: "hello-nextjs",
title: "سلام Next.js",
excerpt: "اولین نوشتهی DevNotes دربارهی شروع کار با Next.js.",
body: "Next.js یک فریمورک فولاستک مبتنی بر React است.",
},
{
slug: "app-router-intro",
title: "آشنایی با App Router",
excerpt: "نگاهی کوتاه به مسیریابی مبتنی بر پوشهها.",
body: "در App Router هر پوشه یک بخش از آدرس را میسازد.",
},
{
slug: "server-components",
title: "Server Componentها",
excerpt: "چرا کامپوننتها بهصورت پیشفرض روی سرور اجرا میشوند؟",
body: "Server Componentها کد کمتری به مرورگر میفرستند.",
},
];
export async function getAllPosts(): Promise<Post[]> {
return posts;
}
export async function getPostBySlug(slug: string): Promise<Post | undefined> {
return posts.find((post) => post.slug === slug);
}محدودیت این فایل: داده در حافظهی برنامه است، با هر تغییر کد ریست میشود و هیچچیز ذخیره نمیکند. فقط برای آموزش است.
عملی: استفاده از notFound() در صفحهی جزئیات نوشته
فایل متأثر: app/(public)/posts/[slug]/page.tsx. بهجای فهرست ثابت slugها در generateStaticParams، از دادهی نمونه میخوانیم و اگر نوشتهای پیدا نشد notFound() را صدا میزنیم.
// app/(public)/posts/[slug]/page.tsx
import { notFound } from "next/navigation";
import BackButton from "@/components/BackButton";
import { getAllPosts, getPostBySlug } from "@/lib/posts";
type PostPageProps = {
params: Promise<{ slug: string }>;
};
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
notFound();
}
return (
<main>
<BackButton />
<h1>{post.title}</h1>
<p>{post.body}</p>
</main>
);
}عملی: not-found.tsx اختصاصی برای نوشتهها
فایل جدید: app/(public)/posts/not-found.tsx. چون این فایل کنار layout.tsx بخش نوشتهها است، صفحهی ۴۰۴ داخل هدر، فوتر و Layout نوشتهها دیده میشود.
// app/(public)/posts/not-found.tsx
import Link from "next/link";
export default function PostNotFound() {
return (
<main>
<h1>نوشته پیدا نشد</h1>
<p>نوشتهای با این آدرس وجود ندارد یا حذف شده است.</p>
<Link href="/posts">بازگشت به فهرست نوشتهها</Link>
</main>
);
}عملی: آدرس نامعتبر در صفحهی ویرایش
در صفحهی ویرایش داشبورد، شناسه عدد است. اگر کسی شناسهی نامعتبر مثل abc بفرستد، باید ۴۰۴ بگیرد. فایل متأثر: app/(dashboard)/dashboard/posts/[id]/edit/page.tsx
// app/(dashboard)/dashboard/posts/[id]/edit/page.tsx
import { notFound } from "next/navigation";
type EditPostPageProps = {
params: Promise<{ id: string }>;
};
export default async function EditPostPage({ params }: EditPostPageProps) {
const { id } = await params;
const postId = Number(id);
if (!Number.isInteger(postId) || postId < 1) {
notFound();
}
return (
<main>
<h1>ویرایش نوشتهی شمارهی {postId}</h1>
<p>فرم ویرایش در فصل «فرمها و Server Actions» ساخته میشود.</p>
</main>
);
}
این صفحه نوشتهی واقعی ندارد و فقط ساختار شناسه را بررسی میکند. بررسی وجود رکورد را بعد از اتصال به پایگاه داده اضافه میکنیم. اگر در داشبورد not-found.tsx اختصاصی نداشته باشیم، صفحهی ۴۰۴ سراسری ریشه نشان داده میشود.
خروجی و رفتار مورد انتظار
| آدرس | نتیجه |
|---|---|
/posts/hello-nextjs |
عنوان «سلام Next.js» و متن نوشته |
/posts/does-not-exist |
صفحهی «نوشته پیدا نشد» داخل هدر و فوتر سایت، با کد وضعیت ۴۰۴ |
/dashboard/posts/12/edit |
صفحهی ویرایش شمارهی ۱۲ |
/dashboard/posts/abc/edit |
صفحهی ۴۰۴ سراسری (فارسی، بدون هدر و فوتر عمومی) |
/blog/hello-nextjs |
هدایت به /posts/hello-nextjs |
/some/random/path |
صفحهی ۴۰۴ سراسری ریشه |
برای دیدن کد وضعیت، در مرورگر ابزار توسعهدهنده را باز کنید، به تب Network بروید و ردیف سند اصلی صفحه را ببینید.
تفاوت redirect()، notFound() و خطا
| ابزار | چه زمانی؟ | نتیجه |
|---|---|---|
redirect() |
کاربر باید جای دیگری برود | هدایت به آدرس دیگر |
notFound() |
منبع درخواستشده وجود ندارد (خطای قابلانتظار) | صفحهی ۴۰۴ از not-found.tsx |
پرتاب خطا (throw new Error) |
اتفاق غیرمنتظره، مثل قطع پایگاه داده | رابط error.tsx (فصل «Loading، Error و Streaming») |
«پیدا نشدن» یک وضعیت عادی و قابلپیشبینی است؛ پس با notFound() مدیریت میشود، نه با پرتاب خطای عمومی. تمایز خطاهای قابلانتظار و غیرمنتظره را در درس «خطاهای قابلانتظار، notFound() و مدیریت خطاهای غیرمنتظره» کامل میبینیم.
نکتهی مهم: redirect و notFound داخل try/catch
این دو تابع برای قطع اجرا یک «خطای ویژه» پرتاب میکنند. اگر آنها را داخل بلوک try صدا بزنید، بلوک catch خطا را میگیرد و هدایت یا ۴۰۴ انجام نمیشود:
// اشتباه: catch خطای redirect را میگیرد
try {
const post = await getPostBySlug(slug);
if (!post) redirect("/posts");
} catch (error) {
console.error(error);
}
// درست: redirect و notFound بیرون از try
let post;
try {
post = await getPostBySlug(slug);
} catch (error) {
console.error(error);
throw error;
}
if (!post) notFound();
الگو: کار خطادار (مثل خواندن داده) را داخل try بگذارید و تصمیمهای redirect و notFound را بعد از آن، بیرون از try بنویسید.
نکتههای مهم
-
همهی ۴۰۴ها یک جا نیستند: آدرس بدون مسیر همیشه به
not-found.tsxریشه میرود؛ فایلهای اختصاصی فقط برایnotFound()هستند. -
کد وضعیت در Streaming: اگر پاسخ صفحه از قبل شروع به ارسال (Streaming) کرده باشد، ممکن است سرور دیگر نتواند کد وضعیت را به ۴۰۴ تغییر دهد و Next.js بهجای آن از یک تگ
noindexبرای موتورهای جستوجو استفاده میکند. این موضوع را در فصل «Loading، Error و Streaming» بررسی میکنیم. - مقصد هدایت را اعتبارسنجی کنید: اگر آدرس مقصد از ورودی کاربر (مثل Query String) میآید، فقط آدرسهای داخلی سایت را بپذیرید تا هدایت به سایتهای دیگر ممکن نشود. در فصل «Authentication و Security» دوباره میبینیم.
-
هدایت سمت سرور مجوزدهی نیست: هدایتشدن به
/loginبرای کسی که وارد نشده یک تجربهی کاربری است؛ دسترسی واقعی باید در خود داده و عملیات سرور هم بررسی شود (فصلهای «Authentication و Security» و «Proxy و کنترل دسترسی»).
ساختار پروژه پس از این درس
devnotes/
├── next.config.ts ← هدایتهای /blog
├── lib/
│ └── posts.ts ← دادهی نمونهی موقت (جدید)
├── app/
│ ├── not-found.tsx ← ۴۰۴ سراسری
│ ├── (public)/
│ │ └── posts/
│ │ ├── not-found.tsx ← ۴۰۴ بخش نوشتهها (جدید)
│ │ └── [slug]/
│ │ └── page.tsx ← از lib/posts.ts میخواند
│ └── (dashboard)/
│ └── dashboard/posts/[id]/edit/
│ └── page.tsx ← با شناسهی نامعتبر ۴۰۴ میدهد
└── components/
├── BackButton.tsx
└── SearchBox.tsxجمعبندی
redirect()ازnext/navigationدر سمت سرور هدایت میکند (معمولاً با کد307، در Server Action با303) وpermanentRedirect()هدایت دائمی (308) است.- برای قواعد ثابت و قدیمی، گزینهی
redirectsدرnext.config.tsمناسبتر است و پس از تغییر آن باید سرور را دوباره اجرا کرد. notFound()وقتی منبع وجود ندارد صفحهی ۴۰۴ را از نزدیکترینnot-found.tsxنشان میدهد.- هر دو تابع با پرتاب خطای ویژه کار میکنند، پس نباید داخل
tryصدا زده شوند. - در DevNotes اکنون دادهی نمونه در
lib/posts.ts، هدایتهای/blogو ۴۰۴ اختصاصی برای نوشتهها داریم. - در درس بعد، «Prefetching و رفتار Navigation»، میبینیم Next.js چگونه مسیرها را پیشاپیش بارگیری میکند و ناوبری را سریع میکند.
