⏱ 7 min read

404 Pages

Sooner or later a visitor will follow a broken link or mistype an address. A good 404 page tells them what happened and points them somewhere useful instead of leaving them at a blank browser error.

This page shows how to add one, how to customize it, and, most importantly, how to make sure your web server answers with a real 404 status code.

Contents


Creating the Page

A 404 page is an ordinary content file that uses the 404 template and is written to public/404.html. Create content/404.md:

---
title: 'Page not found'
description: 'The page you were looking for does not exist.'
template: 404
noindex: true
sitemap: false
search_index: false
no_llms: true
---

Sorry, we could not find that page.

What each key does:

Key Effect
template: 404 Renders the page with the theme's 404.html.twig and writes it to public/404.html.
noindex: true Adds <meta name="robots" content="noindex, follow"> so search engines do not index the page.
sitemap: false Keeps the page out of sitemap.xml.
search_index: false Keeps the page out of the site search index.
no_llms: true Asks the AEO tooling to leave the page out of llms.txt. Only relevant if you use that package.

StaticForge also excludes 404.html from the sitemap by its output path, so the sitemap stays clean even if you leave sitemap: false out. The other keys are still worth setting.

New sites created with site:init get this file automatically. Existing sites are not touched: create content/404.md yourself with the frontmatter above.


Customizing the Page

The two bundled themes each ship a 404.html.twig.

The bundled templates supply the headings and wording; the body text of content/404.md is not displayed by them. To change the wording, copy the theme's 404.html.twig into your own theme and edit it (see Templates).

The link list comes from the first of these that exists:

  1. A 404_links list in siteconfig.yaml.
  2. Your top-level menu (menu.top).
  3. A single link to the home page.
404_links:
  - title: "Getting Started"
    url: "guide/quick-start.html"
  - title: "Features"
    url: "features/index.html"
  - title: "Blog"
    url: "https://blog.example.com/"

Each item needs a title and a url. A url starting with http is used as written. Any other value is treated as relative to SITE_BASE_URL, so guide/quick-start.html and /guide/quick-start.html both work. Items missing either key are skipped. See Site Configuration for the reference entry.

Custom Themes

A theme that does not ship a 404.html.twig cannot render a page with template: 404. If you use a custom theme, add a 404.html.twig that extends your base layout. See Templates for what the bundled version does and what to include.


A web server shows the 404 page for any missing address, at any depth: /nope, /blog/2024/nope, and so on. A relative link like guide/index.html would resolve differently at each depth and break.

For that reason the bundled 404 templates build every link and asset URL from the site base URL (site_base_url in templates, set by SITE_BASE_URL in .env). Set it to a full URL:

SITE_BASE_URL="https://example.com/"

If your site lives in a sub-path, include it: https://example.com/docs/. The template links then include the sub-path, but your web server's 404 setting must also point at the sub-path (see the Apache section below).

audit:config checks this for you. When content/404.md or content/404.html exists and SITE_BASE_URL is not a full URL, it prints a warning asking you to set one:

php bin/staticforge.php audit:config

Configuring Your Web Server

Generating public/404.html is only half the job. Your server must be told to send that file with a 404 status whenever a URL does not exist.

Apache

make:htaccess generates a production .htaccess that now ends with this line:

ErrorDocument 404 /404.html
# Print to the screen
php bin/staticforge.php make:htaccess

# Save to htaccess.txt
php bin/staticforge.php make:htaccess --write

# Save to a file of your choice
php bin/staticforge.php make:htaccess --write --output=my-htaccess.txt

Nothing modifies an .htaccess you already have on the server. Copy the ErrorDocument line into your existing file yourself (or merge the generated file).

Two rules for that line:

nginx

Add this to your server block:

error_page 404 /404.html;

nginx keeps the 404 status when it serves the page.

Dev Server

site:devserver serves public/404.html for any URL that does not exist, with a real 404 status. If public/404.html has not been built yet, it falls back to a plain built-in 404 page.

php bin/staticforge.php site:devserver

Cloudflare Pages

Host behaviour for 404.html has not been verified for this guide yet.

Netlify

Host behaviour for 404.html has not been verified for this guide yet.

GitHub Pages

Host behaviour for 404.html has not been verified for this guide yet.


Verifying It Works

Why the Status Code Matters

If a server answers a missing URL with your friendly page but a 200 OK status (a "soft 404"), search engines treat every made-up address as a real page. The same happens if unknown URLs are redirected to /404.html or to the home page. Always serve the 404 page in place, with status 404, and never redirect to it.

Check with curl

curl -I https://example.com/no-such-page

The first line of the response must be HTTP/1.1 404 Not Found (or HTTP/2 404). A 200 or a 301/302 means the server is misconfigured.

Check with audit:live

audit:live includes a soft-404 check against your deployed site. It requests a random address that does not exist, once without an extension and once ending in .html, since servers often treat the two differently.

php bin/staticforge.php audit:live
php bin/staticforge.php audit:live --url=https://example.com --insecure

Next Steps