Skip to content

fix/site: Return HTTP 404 instead of 200 for unknown pages - #1860

Merged
marcleblanc2 merged 2 commits into
mainfrom
fix-real-404-status
Sep 9, 2026
Merged

fix/site: Return HTTP 404 instead of 200 for unknown pages#1860
marcleblanc2 merged 2 commits into
mainfrom
fix-real-404-status

Conversation

@marcleblanc2

@marcleblanc2 marcleblanc2 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Linear FE-499: Fix doc site issues

3 problems from the same root cause

  1. https://sourcegraph.com/docs/this-page-does-not-exist returns HTTP 200. The "Page not found" UI renders, but only via the client-side NEXT_NOT_FOUND digest. Any status-code-based link checker (lychee, W3C, etc.) therefore reports the docs site as 100% clean no matter how many links are dead.
  2. Page load times were up to 30s on fresh deploys
  3. MDX render errors HTTP 500 in production, instead of failing during build

Root cause

Two things stacked up in src/app/[...slug]/page.tsx:

  1. generateStaticParams returned {params: {slug}}, the Pages Router shape. App Router expects {slug}, so Next silently ignored it: zero doc pages were prerendered and every page has been rendered on demand by a serverless function since the site launched.
  2. notFound() runs inside the root layout's <Suspense>. By then the shell has already streamed with a 200, so Next can't change the status code.

History

The site was always meant to be static; the function has been broken three different ways:

  • Nov 2023, original routes: allPosts.map(post => { ({slug: ...}); }) — block body, no return, yields undefined per item.
  • Feb 2024, 📦 NEW: Add versioning #48 "Add versioning": rewritten to {params: {slug}}. PR body says "Previously Server routes are now SSG, speeding up page load times significantly." Wrong shape, so nothing changed.
  • May 2024, Docs for Sourcegraph version 5.4 #308: export const maxDuration = 300 added to the route with no explanation. That's the Vercel function timeout — only needed because pages were rendering on demand and the big ones exceeded the default (see 25–30 s renders below).

Fix

  • Return {slug} from generateStaticParams so all doc pages actually prerender (523 routes).
  • Exclude the root document (flattenedPath === ''), which is served by app/page.tsx. Including it fails the build with export path '/' doesn't match '/[...slug]'.
  • export const dynamicParams = false so unknown slugs are rejected at the router with a real 404, before rendering/streaming starts.

not-found.tsx is untouched; users see the same 404 page as before. maxDuration = 300 is removed: the route no longer runs as a serverless function, so the timeout setting had nothing left to configure.

No HTTP 404 response on missing pages

Before: HTTP 200
image

After: HTTP 404
Note: the additional HTTP 204 shown in this screenshot is the Vercel preview toolbar, won't be shown in prod
image

Proof: before vs after on Vercel

Every URL in sitemap.xml (522) was fetched with curl, capturing x-vercel-cache, x-matched-path, x-vercel-id, age, and time_total.

Before — a fresh deployment of main (temporary branch cold-baseline-main, empty commit, since deleted), swept immediately after it went live so nothing was cached:

1st pass (cold) 2nd pass
x-vercel-cache MISS 517 / HIT 4 / PRERENDER 1 HIT 522
x-matched-path /[...slug] on 520/520 same
x-vercel-id pdx1::iad1::… (edge + function region) on 520/520 same
time_total median / p90 / max 0.48 s / 0.68 s / 30.1 s 0.19 s / 0.22 s / 0.64 s

Slowest cold renders: /technical-changelog 30.1 s, /self-hosted/observability/dashboards 24.7 s, then a cluster at ~9 s (concurrent cold lambda starts). Both >15 s pages would 504 without maxDuration = 300.

Production today (sourcegraph.com/docs): all 520 pages already HIT, x-matched-path: /[...slug], function region present. age values cluster in three bursts 34.5–39 h old (deploy was 39 h earlier), i.e. crawlers walked the site and forced every page to render after the last deploy. Any never-requested URL is a MISS that invokes the function and returns a 200 not-found page, which is then cached.

After — this PR's preview, also a fresh deployment swept immediately after it went live:

1st pass (cold) 2nd pass
x-vercel-cache PRERENDER 516 / HIT 6 (prebuilt file, first serve) HIT 522
x-matched-path concrete path (e.g. /batch-changes/batch-spec-yaml-reference) 522/522 same
x-vercel-id pdx1::… (edge only, no function region) 522/522 same
time_total median / p90 / max 0.39 s / 0.45 s / 1.2 s 0.19 s / 0.22 s / 0.48 s
never-seen URL 404, x-matched-path: /404, static, no function

Side by side, first-visitor pass: median 0.48 s → 0.39 s, p90 0.68 s → 0.45 s, max 30.1 s → 1.2 s, pages over 2 s 10 → 0. Warm passes are identical (~0.19 s), which is why the on-demand rendering went unnoticed.

Other checks

  • All 522 sitemap URLs return 200 on the preview (no page lost to dynamicParams = false).
  • Real pages carry full prerendered content.
  • Middleware unaffected: .md rewrite → 200, trailing slash → 308, /@6.4/... → 307 to versioned host, /api/og/... → 200.
  • next build: 526/526 static pages, ~22 s locally (Node 20.20.2). Was 9. Vercel preview build passed.
  • npx tsc --noEmit clean.

Results

  • External link checkers will start reporting real broken page links on sourcegraph.com/docs
  • MDX render errors now fail the build instead of 500ing in production
  • First reader of each page after a deploy no longer waits on a cold lambda render (0.5–30 s); everyone gets a CDN file
  • Content freshness unchanged: there was never a revalidate, so pages were already cached until the next deploy
  • Pages with preview: true frontmatter are in allPosts, so they prerender and PreviewGuard still gates them by ?preview. None exist in docs/ today, so that path is untested here.

Amp threads

@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sourcegraph-docs Ready Ready Preview Sep 9, 2026 7:49pm UTC

Request Review

@marcleblanc2 marcleblanc2 changed the title fix: return a real HTTP 404 for unknown doc pages Fix HTTP 404 response for unknown doc pages, and pre-render on build Sep 6, 2026
@marcleblanc2 marcleblanc2 changed the title Fix HTTP 404 response for unknown doc pages, and pre-render on build Fix HTTP 404 response for unknown doc pages, and render during build Sep 6, 2026
@marcleblanc2
marcleblanc2 requested a review from a team September 7, 2026 22:23
@marcleblanc2
marcleblanc2 enabled auto-merge (squash) September 7, 2026 22:43
@marcleblanc2 marcleblanc2 changed the title Fix HTTP 404 response for unknown doc pages, and render during build fix/site: Return HTTP 404 for unknown doc pages, and render the 404 page during build Sep 9, 2026
@marcleblanc2 marcleblanc2 changed the title fix/site: Return HTTP 404 for unknown doc pages, and render the 404 page during build fix/site: Return HTTP 404 instead of 200 for unknown pages Sep 9, 2026
marcleblanc2 and others added 2 commits September 9, 2026 13:47
generateStaticParams returned {params: {slug}} (Pages Router shape), so the
App Router ignored it: no doc page was prerendered and every request rendered
on demand. notFound() then fired inside the root layout's <Suspense>, after
the response had started streaming, so missing pages were served with 200.

Return {slug} so all pages prerender, exclude the root document (served by
app/page.tsx), and set dynamicParams = false so unknown slugs are rejected
at the router with a 404. The existing not-found.tsx page is unchanged.

Amp-Thread-ID: https://ampcode.com/threads/T-01a07590-7d1a-72ec-826b-9081eed4f715
Co-authored-by: Amp <amp@ampcode.com>
The route is now fully prerendered, so it no longer runs as a serverless
function and the timeout setting has no effect.

Amp-Thread-ID: https://ampcode.com/threads/T-01a07590-7d1a-72ec-826b-9081eed4f715
Co-authored-by: Amp <amp@ampcode.com>
@marcleblanc2
marcleblanc2 merged commit c85fbb3 into main Sep 9, 2026
2 checks passed
@marcleblanc2
marcleblanc2 deleted the fix-real-404-status branch September 9, 2026 19:50
marcleblanc2 added a commit that referenced this pull request Sep 11, 2026
Linear [FE-499: Fix doc site
issues](https://linear.app/sourcegraph/issue/FE-499/fix-doc-site-issues)

## Problem

- Our docs site has hundreds of broken links
- `dev/check-links.mjs` finds broken internal links and anchors, but it
isn't run automatically, so PRs can easily break links (renaming a
heading, moving or deleting a page) without anyone noticing

## Solution

- Updated the script to also work as a PR check, with additional
functions beyond what's run when used as a CI test in Vercel builds
- PR check to run the script and report if the PR breaks links
- It runs the script (with `--check-anchors`) on both the PR head and
its merge base, and diffs the findings
- This catches both directions:

- **Outbound**: a changed page links to a page or `#heading` that
doesn't exist
- **Inbound**: the PR renames a heading or removes/moves a page that
other, unchanged pages link to — those show up as findings in files the
PR didn't touch

- Pre-existing broken links are ignored by the PR check
- The comment is created / updated in place, and once the PR is fixed,
the PR check passes and the comment is updated to say so
- A PR that never broke anything gets no comment

## Verification

PR check comment in test PR:
#1895 (comment)

### Broken links found

<img width="1826" height="1628" alt="Screenshot 2026-09-09 at 20 05 31"
src="https://github.com/user-attachments/assets/930cde1f-50b1-46c0-9421-bd75a257c17d"
/>

### Broken links fixed

<img width="910" height="168" alt="Screenshot 2026-09-09 at 20 06 31"
src="https://github.com/user-attachments/assets/1cc26276-5cba-4595-85fc-8e10955aabab"
/>

## Absolute self-links and external links

- Absolute links to this site (`https://sourcegraph.com/docs/…`,
`http://…`, `//…`, `www.`, the legacy `https://docs.sourcegraph.com/…`)
fail the check even when the target exists: they leave the Vercel
preview and local dev, and hide moved pages behind redirects. The
finding names the relative link, following `src/data/redirects.ts` when
the page moved. Version-pinned links (`/@5.1/…`) stay external
- External links on lines this PR added are requested (HEAD, then GET on
an error status, following redirects); only 404 and 410 are findings, so
rate limits, bot blocks, 5xx and network errors never fail a PR.
Placeholder hosts (`*.example.com`, `localhost`, templated `<host>`) are
skipped
- Findings with a fix become one suggested-change review comment per
line, which the author can apply from the PR. Suggestions already on the
PR are not posted again
- #1899 clears the 67 existing absolute self-links so this check starts
from zero

Test PR: #1900 (report comment + one review suggestion; the `#sampling`
anchor deliberately does not exist, so that link gets no suggestion; a
second run posted nothing new)

## Related

- Draft PR #1562 proposes a daily Slack digest with a separate
reimplementation of this script
  - Instead, this PR improves on the existing script, and gates PRs
- PR #1860 enabled external link
checkers to find broken links again

## Amp threads

- [Broken link PR
check](https://ampcode.com/threads/T-01a0753f-0f4f-7478-b36c-87466e7c0261)
- [Asset case
mismatch](https://ampcode.com/threads/T-01a07597-43c0-751b-8c49-6e5809e714d2)
- [Docs - Fix broken heading
links](https://ampcode.com/threads/T-01a07623-9d65-7356-96b8-2bebb31ffa5a)
- [Self-links and external
links](https://ampcode.com/threads/T-01a08a01-44c1-775b-84d0-d67ff9501905)

---------

Co-authored-by: Amp <amp@ampcode.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants