
Next.js 16 دو قابلیت متفاوت برای مدیریت 404 ارائه میدهد: not-found.js و قابلیت تازهمعرفیشده global-not-found.js. این دو، دو مسئلهٔ متفاوت را حل میکنند. بیشتر آموزشها هنوز 404 را بهعنوان یک فایل ساده میبینند که هم قدیمی است و هم در برخی ساختارها اصلاً درست کار نمیکند. در این مقاله هر دو روش، نحوهٔ صحیح فراخوانی آنها در مسیرهای داینامیک، و نکتهٔ SEO ای که واقعاً تعیین میکند گوگل صفحهٔ شما را 404 واقعی بداند یا نه، بررسی میشود.
app/not-found.tsxاین فایل زمانی رندر میشود که notFound() داخل یک route segment فراخوانی شود، یا وقتی URLای با هیچکدام از segmentهای زیرمجموعهٔ آن بخش از DOM تطابق نداشته باشد.
import Link from "next/link";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "404 — صفحه پیدا نشد",
robots: { index: false, follow: true },
};
export default function NotFound() {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-3 px-4 text-center">
<h1 className="text-3xl font-bold">404 — صفحه پیدا نشد</h1>
<p className="max-w-md text-muted-foreground">
صفحهای که دنبالش هستید وجود ندارد یا جابهجا شده است.
</p>
<Link href="/" className="mt-4 underline">
بازگشت به صفحهٔ اصلی
</Link>
</div>
);
}دو نکتهٔ مهم:
not-found.tsx بهطور پیشفرض یک Server Component است — همینطور نگهش دارید، مگر واقعاً نیاز به تعامل کاربر داشته باشید. دلیلی ندارد برای یک صفحهٔ خطای استاتیک، JS سمت کلاینت ارسال کنید.robots: { index: false } را در متادیتای این صفحه تنظیم کنید. صفحهٔ 404 هیچ جایگاهی در ایندکس گوگل ندارد و فراموش کردن این مورد یکی از رایجترین نشتیهای SEO در سایتهای App Router است.app/global-not-found.tsx (جدید در Next.js 16)not-found.tsx فقط URLهای تطابقنیافتهای را میگیرد که درون همان درخت layout قرار دارند که خودش در آن است. اگر اپلیکیشن شما چند root layout دارد (مثلاً app/(admin)/layout.tsx و app/(shop)/layout.tsx)، یا root layout شما زیر یک segment داینامیک قرار دارد (مثل app/[locale]/layout.tsx در پروژههای چندزبانه)، هیچ layout واحدی برای رندر کردن 404 وجود ندارد — پس درخواست برای URLای که اصلاً در هیچجای اپلیکیشن وجود ندارد، جایی برای fallback شدن ندارد.
global-not-found.tsx دقیقاً همین مشکل را حل میکند. این فایل کاملاً مسیر رندر معمول layoutهای شما را دور میزند، به همین دلیل باید یک سند کامل HTML برگرداند:
import "./globals.css";
import { Vazirmatn } from "next/font/google";
import type { Metadata } from "next";
const vazirmatn = Vazirmatn({ subsets: ["arabic"] });
export const metadata: Metadata = {
title: "404 — صفحه پیدا نشد",
description: "صفحهای که دنبالش هستید وجود ندارد.",
robots: { index: false, follow: true },
};
export default function GlobalNotFound() {
return (
<html lang="fa" dir="rtl" className={vazirmatn.className}>
<body>
<div className="flex min-h-screen flex-col items-center justify-center gap-3 px-4 text-center">
<h1 className="text-3xl font-bold">404 — صفحه پیدا نشد</h1>
<p>این صفحه وجود ندارد.</p>
<a href="/" className="mt-4 underline">
بازگشت به صفحهٔ اصلی
</a>
</div>
</body>
</html>
);
}از not-found.tsx استفاده کنید اگر اپلیکیشن شما فقط یک root layout دارد — سادهتر است و از پوستهٔ UI مشترک شما استفاده میکند.
از global-not-found.tsx استفاده کنید اگر چند root layout دارید یا root layout شما زیر یک segment داینامیک است (رایج در پروژههای i18n). از آنجا که این فایل مسیر رندر معمول را دور میزند، dependencyهایش (فونتها، CSS) را حداقلی نگه دارید — این فایل مستقیماً روی مسیر بحرانی هر لینک شکستهای قرار میگیرد که اپلیکیشن شما دریافت میکند.
notFound() در مسیرهای داینامیک — و رفع یک باگ رایجاینجا جاییست که بیشتر نمونهکدها (از جمله نسخهای که با آن شروع کردید) اشتباه میکنند. fetch() در پاسخهای 4xx یا 5xx نه throw میکند و نه مقدار falsy برمیگرداند — با موفقیت resolve میشود، در حالی که res.ok === false است. شرط if (!res) هیچوقت یک پست گمشده را تشخیص نمیدهد؛ فقط خطاهای سطح شبکه را میگیرد.
همچنین در Next.js 16، پراپ params بهصورت async است و باید await شود:
import { notFound } from "next/navigation";
type Props = {
params: Promise<{ id: string }>;
};
export default async function PostPage({ params }: Props) {
const { id } = await params;
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { revalidate: 3600 },
});
if (!res.ok) {
notFound();
}
const post = await res.json();
return <article>{post.title}</article>;
}notFound() بهصورت داخلی throw میکند تا رندر را متوقف کرده و کنترل را به نزدیکترین not-found.tsx بسپارد — آن را داخل try/catchای که ممکن است این throw را قورت بدهد قرار ندهید، و آن را بهصورت شرطی بعد از side effectهایی که باید skip شوند فراخوانی نکنید.
این بخش واقعاً روی رتبهبندی تأثیر میگذارد و مختص نحوهٔ stream کردن پاسخها در App Router است:
not-found.tsx برمیگرداند.notFound() پایینتر در DOM فراخوانی میشود یا نه، ارسال میشوند)، و معنای 404 فقط در محتوای رندرشده منعکس میشود.در عمل، این موضوع بهندرت Googlebot را گیج میکند — چون محتوا را رندر کرده و میخواند — اما میتواند crawlerهای ساده، ابزارهای uptime monitoring، یا اسکریپتهای سفارشیای را که response.status === 404 را روی یک مسیر stream شده چک میکنند، دچار مشکل کند. اگر status code 404 برای یک integration خارجی اهمیت دارد (یک قانون purge در CDN، یک مانیتور redirect، یک bot غیر-JS)، حتماً header واقعی را با curl -I روی مسیر deployشده تست کنید، نه صرفاً بر اساس مستندات، چون رفتار streaming به render mode مسیر شما بستگی دارد.
robots: { index: false } روی هر صفحهٔ 404 — هم در سطح مسیر و هم global.| روش | دلیل |
|---|---|
نگهداشتن not-found.tsx بهصورت Server Component | دلیلی برای ارسال JS کلاینت برای یک حالت خطای استاتیک نیست |
استفاده از global-not-found.tsx فقط در صورت داشتن چند root layout | در غیر این صورت تکرار غیرضروری پوستهٔ UI است |
چک کردن res.ok بهجای truthiness قبل از فراخوانی notFound() | fetch() روی status کدهای خطای HTTP reject نمیشود |
تنظیم صریح noindex | Next.js این مورد را خودکار تشخیص نمیدهد |
ترجیح notFound() بر redirect() برای محتوای گمشده | معنای صحیح را برای SEO و آنالیتیکس حفظ میکند |
در Next.js 16، مدیریت درست 404 صرفاً ساختن not-found.tsx با کمی متن نیست — انتخاب درست بین قابلیت سطح مسیر و سطح global بر اساس ساختار layout شما، fetch کردن داده با چک res.ok بهجای تکیه بر throw کردن fetch()، و علامتگذاری صریح صفحه با noindex تا هرگز برای رتبهبندی با محتوای واقعی رقابت نکند. اگر این سه مورد را درست انجام دهید، بقیهٔ موارد — bounce rate، crawl budget، تجربهٔ کاربری — بهطور طبیعی درست میشوند.
اگر این را برای یک سایت production میسازید (نه یک دمو)، پیشنهادم این است که بهصورت پیشفرض از not-found.tsx استفاده کنید، مگر از قبل بدانید که به چند root layout نیاز دارید — سراغ global-not-found.tsx نروید مگر واقعاً لازم باشد، چون شما را از layout مشترک و فونتلودینگ مشترک خارج میکند که در یک اپلیکیشن با یک root layout، فقط نگهداری بیشتر است بدون هیچ سودی. همچنین پیشنهاد میکنم یک input جستجوی سبک یا لیست «صفحات پرطرفدار» به خود UI صفحهٔ 404 اضافه کنید، نه فقط یک لینک به صفحهٔ اصلی — مخصوصاً برای یک سایت پورتفولیو/بلاگ، بخش زیادی از ترافیک 404 شما کاربرانی هستند که slug یک پست بلاگ را اشتباه تایپ کردهاند، و دادن راهی برای پیدا کردن مستقیم پست درست، bounce rate را خیلی بیشتر از یک لینک سادهٔ «برو به خانه» کاهش میدهد.