ehsan.blog
~/blog/how-to-design-a-custom-404-page-in-astro — zsh
cat how-to-design-a-custom-404-page-in-astro.md

How to design a custom 404 page in Astro

·4 min read

Broken links happen. Someone mistypes a URL, an old post gets renamed, a search engine holds onto a stale link. When that happens the visitor lands on a page that does not exist — and the default result is an ugly, generic error screen. A custom 404 page turns that dead end into something useful. Here is how I built one in Astro, and why each part matters.

The convention: just name the file 404.astro

Astro has a built-in rule: a file at src/pages/404.astro becomes the site’s not-found page. There is no config to write and nothing to register. Build the site, and Astro produces a 404.html at the root of your output.

That single naming convention is the whole “wiring.” Everything else is design.

What a good 404 page actually needs

A 404 is not just an apology — it is a recovery tool. A visitor who hit a dead end still wants to get where they were going. So a good 404 page should:

  • Clearly say the page was not found (do not be cute to the point of confusion).
  • Give a way back — at minimum a link to the home page.
  • Offer search so they can find what they meant to reach — this blog links to its Pagefind-powered search page.
  • Match the rest of the site so it does not feel like they left.

The real page

Here is this blog’s 404.astro, trimmed slightly for readability:

astro
// blog/src/pages/404.astro
---
import { SITE } from "@/consts"
import BaseLayout from "@/layouts/BaseLayout.astro"
import TerminalWindow from "@/components/TerminalWindow.astro"
---

<BaseLayout
  title={`404 — Not Found — ${SITE.author}`}
  description="Page not found."
>
  <TerminalWindow title="~/blog — signal lost">
    <div class="flex flex-col items-center gap-6 py-10 text-center">
      <p class="glitch font-mono text-5xl font-black ..." data-text="404">
        404
      </p>
      <p class="font-mono text-sm tracking-widest ... uppercase">
        NO SIGNAL — page not found
      </p>
      <p class="max-w-md font-mono text-sm leading-relaxed ...">
        The route you requested doesn't resolve. It may have moved, or never
        existed.
      </p>
      <div class="flex flex-wrap items-center justify-center gap-3">
        <a href="/" class="border ...">← Back to posts</a>
        <a href="/search" class="border ...">Search</a>
      </div>
    </div>
  </TerminalWindow>
</BaseLayout>

Let me walk through the choices.

Reusing the site’s layout

astro
<BaseLayout
  title={`404 — Not Found — ${SITE.author}`}
  description="Page not found."
>

The 404 page is wrapped in BaseLayout — the exact same layout every other page uses. This is deliberate: the header, fonts, colors, and metadata handling all come along for free, so the not-found page looks like part of the site rather than a bare error screen. The title still reads “404 — Not Found” so the browser tab and any bots are honest about what this page is.

SITE.author comes from a small consts.ts file, so the branding stays consistent site-wide without hardcoding my name here.

The message

astro
<p class="glitch ..." data-text="404">404</p>
<p>NO SIGNAL — page not found</p>
<p>The route you requested doesn't resolve. It may have moved, or never existed.</p>

Three levels of clarity: the big 404 code, a short human label, and a sentence explaining what happened. The glitch class and data-text="404" drive a CSS animation that fits the site’s terminal/CRT aesthetic — style, but never at the expense of the plain-English explanation underneath.

The escape routes

astro
<a href="/">← Back to posts</a>
<a href="/search">Search</a>

This is the most important part. Two clear next steps: go home, or search. Without these, a 404 is a dead end and the visitor leaves. With them, they have an obvious way to recover.

How static hosts serve it

Because Astro is a static site generator, 404.astro is compiled to a 404.html file at build time — there is no server running my code per request. So how does it get shown for a bad URL?

Most static hosts (this blog is deployed on Vercel) automatically serve 404.html whenever a requested path does not match any real file, and they return it with the correct HTTP 404 status code. That status matters: it tells search engines “this URL is gone, do not index it,” which keeps your search results clean.

flowchart LR
  A[Visitor requests /missing] --> B{Does /missing.html exist?}
  B -- yes --> C[Serve the page]
  B -- no --> D[Serve 404.html with 404 status]

You do not configure any of this on Vercel for an Astro site — building the 404.astro file is enough.

Build your own

  1. Create src/pages/404.astro.
  2. Wrap it in your normal layout so it matches the site.
  3. State clearly that the page was not found.
  4. Add a home link and, ideally, a search link.
  5. Deploy — your host will serve it for unmatched URLs.

What to remember

  • Naming a file src/pages/404.astro is all it takes; Astro handles the rest.
  • Reuse your base layout so the 404 feels native, not broken.
  • A 404’s real job is recovery — always give a way home and a way to search.
  • Static hosts serve the generated 404.html with a real 404 status for any unmatched URL.
ls ./related
cat ./comments

Comments are not configured yet. Enable GitHub Discussions and paste the giscus repo-id / category-id into src/consts.ts.