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 وارد می‌کنیم و آدرس مقصد را به آن می‌دهیم:

tsx
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» می‌سازیم:

tsx
// نمونه‌ی مفهومی؛ در 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

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 می‌گوید «این منبع وجود ندارد» و صفحه‌ی ۴۰۴ را با کد وضعیت ۴۰۴ نمایش می‌دهد.

tsx
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

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() را صدا می‌زنیم.

tsx
// 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 نوشته‌ها دیده می‌شود.

tsx
// 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

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 خطا را می‌گیرد و هدایت یا ۴۰۴ انجام نمی‌شود:

tsx
// اشتباه: 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 و کنترل دسترسی»).

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

text
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 چگونه مسیرها را پیشاپیش بارگیری می‌کند و ناوبری را سریع می‌کند.