To add a custom 404 page in Astro, create src/pages/404.astro. Astro builds it to dist/404.html, and Netlify, Vercel, Cloudflare Pages and GitHub Pages serve that file automatically, with a 404 status, for any URL that doesn’t exist. On sites with an adapter, Astro renders the same page for unmatched on-demand routes.
Key Takeaways
- The file must be
src/pages/404.astro(or404.md). Astro outputs404.html.- Most static hosts serve it with a real
404status, no config needed.- Test with
npx astro preview, which serves your 404 page for unknown paths.- Add
noindexand give people a way back: search, popular links, home.
The page
---
// src/pages/404.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Page not found" description="This page doesn't exist.">
<section class="not-found">
<p class="code">404</p>
<h1>This page doesn't exist</h1>
<p>It may have moved, or the link may be mistyped.</p>
<ul class="links">
<li><a href="/">Home</a></li>
<li><a href="/blog/">Blog</a></li>
<li><a href="/contact/">Contact</a></li>
</ul>
</section>
</BaseLayout>
Use your normal layout so the header and footer stay put; a stranded visitor should still recognise the site.
Keep it out of search results
The page shouldn’t be indexed. If your layout renders an SEO component, pass a noindex flag:
<BaseLayout title="Page not found" description="…" noindex>
which renders:
<meta name="robots" content="noindex" />
Also keep it out of your sitemap: open dist/sitemap-0.xml after a build, and if /404/ is listed, exclude it with the integration’s filter option. The Astro SEO checklist shows a head component with a noindex prop.
Make it useful
A good 404 page helps people find what they wanted:
- Search. If your site has search, put the box here first.
- Popular pages. Link the three or four pages most visitors want.
- Recent posts. You can query collections in a 404 page like anywhere else:
---
import { getCollection } from 'astro:content';
const recent = (await getCollection('blog'))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.slice(0, 3);
---
<h2>Recent posts</h2>
<ul>{recent.map((p) => <li><a href={`/blog/${p.id}/`}>{p.data.title}</a></li>)}</ul>
Because the page is built statically, this list is as fresh as your last build.
Test it
The dev server and preview both serve the 404 page for unknown paths:
npx astro build && npx astro preview
# visit http://localhost:4321/does-not-exist/
After deploying, check the status code, not just the page:
curl -I https://www.example.com/does-not-exist/
# HTTP/2 404
A custom page served with 200 is a “soft 404”, which search engines treat as a quality problem.
Hosts
- Netlify, Vercel, Cloudflare Pages, GitHub Pages: serve
404.htmlwith a404status automatically. - Cloudflare Pages note: without a top-level
404.html, Pages treats the site as a single-page app and servesindex.htmlfor every unknown path. Having404.astroavoids that. - Your own server: configure it, for example
error_page 404 /404.html;in nginx.
On-demand sites
With an adapter, unmatched requests render 404.astro. You can also trigger it from an on-demand page when data is missing:
---
export const prerender = false;
const product = await getProduct(Astro.params.id);
if (!product) {
return new Response(null, { status: 404, statusText: 'Not found' });
}
---
Returning a 404 response from a page makes Astro render your 404.astro page. Create src/pages/500.astro to customise server error pages the same way.
For more on how pages map to URLs, see the Astro routing guide, and for sending old URLs somewhere useful instead of a 404, redirects in Astro. The pages docs show where the 404 page lives in a theme, and the official notes are in the Astro pages docs.
FAQ
Where does the 404 page go in Astro?
src/pages/404.astro. Astro builds it to 404.html at the root of dist/.
Why does my host show its own 404 page instead of mine?
Check that dist/404.html exists after the build, and that your host isn’t configured as a single-page app with a catch-all rewrite to index.html.