Next.js guide

How to build a custom 404 page in Next.js

One file is all it takes — for both the App Router and the Pages Router. Here is exactly where it goes, with code, and how to keep a correct 404 status.

Updated May 2026 · App Router & Pages Router · ~7 min read
Quick answer

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.

1

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&apos;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.

2

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',
}
3

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>
}
Tip

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.

1

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>
  )
}
2

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 getStaticPropsgetServerSideProps 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.

Watch out

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.

Related guides