In the App Router (Next.js 13+), create app/not-found.tsx. Next.js renders it for any unmatched URL and whenever you call notFound().
In the Pages Router, create pages/404.js. Next.js statically generates it and serves it for every 404. The file name and location is the wiring — Next.js picks it up automatically, and both return a correct HTTP 404 status.
App Router or Pages Router — which do you have?
Next.js has two routing systems, and the 404 file differs between them. Check your project root: an app/ directory means you are on the App Router (the default since Next.js 13.4); a pages/ directory means the Pages Router. Some projects have both during a migration — in that case the App Router takes precedence, so use app/not-found.tsx.
Method 1 — App Router: app/not-found.tsx
Create a file named not-found.tsx at the top level of your app/ directory. Next.js uses it as the UI for unmatched routes across the whole app, and for any notFound() call that is not caught by a closer not-found file.
Create the file
app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<main className="not-found">
<h1>404 — Page not found</h1>
<p>
We couldn't find the page you were looking
for. It may have moved, or the link may be broken.
</p>
<Link href="/">Return to the homepage</Link>
</main>
)
}
That is the entire setup. There is no route to register and no config to change — Next.js maps the file by convention.
Add metadata (optional)
Because not-found.tsx is a Server Component, you can export metadata from it to set the page title:
export const metadata = {
title: 'Page not found',
}
Trigger it for dynamic routes
Here is the part most people miss: for a dynamic route like app/blog/[slug]/page.tsx, Next.js does not know a slug is invalid — your code does. Call notFound() when the record is missing, and Next.js renders the nearest not-found.tsx with a 404 status.
app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export default async function Post({ params }) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return <article>{post.title}</article>
}
Scoped not-found files. You can place a not-found.tsx inside any route segment — say app/shop/not-found.tsx — and a notFound() thrown within that segment renders that file instead of the root one. Useful when one area of the app needs a different not-found experience.
Method 2 — Pages Router: pages/404.js
If your project uses the pages/ directory, create a file named 404.js inside it. Next.js statically generates this page at build time for the best performance and serves it for every 404.
Create the file
pages/404.js
import Link from 'next/link'
export default function Custom404() {
return (
<main className="not-found">
<h1>404 — Page not found</h1>
<p>The link may be broken, or the page may have moved.</p>
<Link href="/">Return to the homepage</Link>
</main>
)
}
Need data on the 404 page?
The default pages/404.js is static. If you need to fetch data (for example, a list of popular posts to suggest), use getStaticProps — getServerSideProps is not supported on this page.
Make sure your 404 page returns a real 404 status
Both not-found.tsx and pages/404.js return a correct HTTP 404 status when your app runs on a Node.js server or a platform that supports it (Vercel does this automatically). One case needs attention: a fully static export.
If you build with output: 'export' and deploy the static files to a generic host or CDN, the host serves a generated 404.html — but only with a 404 status if you configure it to. A static host that returns 200 for unknown paths produces a soft 404. Check your host's "custom error page" or "not-found handling" setting.
Never "fix" a missing route with a client-side redirect to /. The server still answered 200 for a page that does not exist — a soft 404 that wastes crawl budget and confuses search engines. Let the route 404 properly and design a 404 page worth landing on.
What makes a Next.js 404 page actually good
Wiring the file in is the easy 20%. The 404 page earns its place when it recovers the visitor: clear orientation, real links to the sections people actually want, working search, and a tone that matches the rest of the app rather than a stark "404". Because it is a React component, you have the full toolkit — animation, interactivity, data — so there is no excuse for a dead end.
We break down the twelve traits the strongest 404 pages share on the main page.
Skip the blank component — generate it
Enter your site and get a complete, on-brand 404 page tailored to your real colours, fonts, and navigation — delivered as a ready-to-drop-in Next.js component.
Generate my 404 page →Free · no account · no opt-in
Frequently asked questions
What file is the 404 page in Next.js?
In the App Router it is app/not-found.tsx (or .js). In the Pages Router it is pages/404.js. Next.js picks the file up automatically by its name and location — there is nothing else to register.
Does the Next.js 404 page return a real 404 status?
Yes. Both not-found.tsx and pages/404.js return an HTTP 404 status on a Node.js server or a platform like Vercel. With a fully static export, your host must be configured to serve the generated 404 file with a 404 status.
Why is my custom not-found.tsx not showing?
The root file must be exactly app/not-found.tsx. And for dynamic routes, Next.js will not 404 on an invalid parameter by itself — you must call notFound() in that segment so the not-found UI renders.
How do I trigger the 404 page manually?
Import notFound from next/navigation and call it inside a Server Component or route handler. It throws a special error that renders the nearest not-found.tsx and sets a 404 status.
Can I use one 404 design for different parts of the app?
Yes. A root app/not-found.tsx covers the whole app. Add a not-found.tsx inside any route segment to give that section its own not-found experience.