Securealy
Check · Launch hygiene · high · passive

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.

  1. Static site: copy the kit's templates/404.html to the web root, next to index.html, and fill in PRODUCT_NAME, YOUR_DOMAIN, YEAR and ORG_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: create app/not-found.tsx with the same content.

  2. Open vercel.json. If there is a catch-all rewrite that sends every path to index.html, that rewrite answers before the 404 page can. Add the 404 before the catch-all, or remove the catch-all. The kit's vercel.json template has no rewrites at all, which is the simplest state.

  3. 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.

  4. 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.

  1. Run the kit's verify-site.sh once against the live site. The line unknown URL returns 404 must 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.

Next

Grade your own URL

Every grade lists this check with its evidence for your site. Two free grades a day.