Missing pages return 404
A random path returns HTTP 404 with the site's own page, not 200.
- Severity
- high
- Tier
- Passive: runs on any URL
- Category
- Launch hygiene
- Fix
- Free, in full, below
- Id
- not-found-page
What this means
Passing means a URL on your domain that does not exist answers with HTTP status 404 and your own not-found page. Failing means the server answers 200 with some page, or redirects the visitor to the home page, which is a soft 404. The status code is what crawlers and link checkers read; the page is what the visitor reads.
How we test it
We request one path that cannot exist, generated from your host name so it is different for every site, and read the status and the body. A 404 status with an HTML page passes. A 404 that serves the host's default error page rather than your own warns. A 200, or a 3xx that lands on the home page, fails.
Why it matters
A soft 404 lets search engines index garbage and hides real breakage. Every mistyped or deleted URL becomes a page in the index with the home page's content under the wrong address, and your broken-link reports stay empty because nothing ever returns an error. The kit's decision table calls for a real page returning HTTP 404 for exactly this reason, and names the catch-all rewrite as the usual cause when a site fails.
The fix
The host serves a 404 status on its own when the not-found page is in the place it expects. The work is to put a real page there and remove anything that answers first.
Static site: copy the kit's
templates/404.htmlto the web root, next toindex.html, and fill inPRODUCT_NAME,YOUR_DOMAIN,YEARandORG_NAME. The template already carries<meta name="robots" content="noindex">, a skip link, the site nav and a link home. Keep the nav so a lost visitor can recover. Next.js: createapp/not-found.tsxwith the same content.Open
vercel.json. If there is a catch-all rewrite that sends every path toindex.html, that rewrite answers before the 404 page can. Add the 404 before the catch-all, or remove the catch-all. The kit'svercel.jsontemplate has no rewrites at all, which is the simplest state.Leave the security headers block as it is. It applies to every route, including this one, so the 404 page carries the same headers as the rest of the site.
Commit, push, and wait once, 60 to 90 seconds. Then verify with a path that cannot exist:
curl -sI https://YOUR_DOMAIN/this-page-does-not-exist-$(date +%s) | grep -iE "^(HTTP|content-type)"
curl -s https://YOUR_DOMAIN/this-page-does-not-exist | grep -ci "<h1"
Expected: HTTP/2 404 and content-type: text/html from the first command, 1 from the second.
If the first command prints 200, the catch-all rewrite is still serving index.html; go back to step 2. If it prints 404 but the body is the host's default page rather than yours, the file is not named 404.html at the root of the output, or the Next.js file is not at app/not-found.tsx.
- Run the kit's
verify-site.shonce against the live site. The lineunknown URL returns 404must read PASS.
A deeper runbook for this class is in the Fix Kit, Module 01.
Prompt for your AI
Paste this into the coding agent that built the app.
Run: curl -sI https://YOUR_DOMAIN/this-page-does-not-exist-$(date +%s) | head -3 and show me the status line. If the site is static, create 404.html at the web root next to index.html: html lang, charset and viewport metas, a title, meta name="robots" content="noindex", the site nav, one h1 reading Page not found, and a link to /. If it is Next.js, create app/not-found.tsx with the same content. Open vercel.json and show me every entry under rewrites. If a catch-all sends every path to index.html, move the 404 before it or remove it, and tell me which you did. Do not touch the headers block. Commit and push. Wait once, 90 seconds, then stop waiting; no polling loops. Run: curl -s https://YOUR_DOMAIN/this-page-does-not-exist | grep -ci "<h1" and show me the output. Expected: 1. Run: curl -sI https://YOUR_DOMAIN/this-page-does-not-exist-$(date +%s) | grep -iE "^(HTTP|content-type)" and show me the output. Expected: HTTP/2 404 and content-type: text/html.
Grade your own URL
Every grade lists this check with its evidence for your site. Two free grades a day.