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
- Customizing the Page
- Absolute Links and Sub-Path Installs
- Configuring Your Web Server
- Verifying It Works
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.
-
staticforceshows a "404" label, a "Page not found" heading, a short explanation, a "Go to the homepage" button, a search box (only when JavaScript is available, and it replaces the navbar search on this page), and a "Popular sections" list. Visitors without JavaScript see a link to the sitemap instead of the search box. -
sampleshows the same heading and explanation, a homepage link, the "Popular sections" list, and a sitemap link. It has no search.
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).
Choosing the Popular Sections
The link list comes from the first of these that exists:
- A
404_linkslist insiteconfig.yaml. - Your top-level menu (
menu.top). - 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.
Absolute Links and Sub-Path Installs
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:
- Use a local path only. A full URL (
ErrorDocument 404 https://example.com/404.html) makes Apache send a redirect instead of a 404. - If your site is installed in a sub-path, change the line to match, for example
ErrorDocument 404 /docs/404.html.
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.
- Both requests must return
404. - A redirect (any
3xx) counts as a failure. Redirects are never followed. - Any other status, such as
200, is reported as an error. - If the site cannot be reached, you get a warning instead.
--insecurerelaxes TLS certificate verification for this check, just as it does for the others.
php bin/staticforge.php audit:live
php bin/staticforge.php audit:live --url=https://example.com --insecure
Next Steps
- Site Management & Deployment - Build and upload your site.
- Auditing - Run
audit:configandaudit:liveon your project. - Site Configuration - The
404_linkskey and other settings. - Templates - Write a
404.html.twigfor your own theme.