Repository navigation
fix(docs): serve a docs 404 from the prerendered not-found route - #192
Merged
Merged
Conversation
A slug that no page matches was rendered dynamically so the page could call notFound(). That throw escapes both the RSC and the SSR render, and Next answers from getErrorRSCPayload -- an empty error shell, with the real 404 tree carried only in the RSC payload for the client. Crawlers and no-JS clients got a blank body under a correct 404 status. No not-found boundary participates in that server render, at any depth, so the fix routes around the dynamic render instead: dynamicParams=false sends an unknown docs slug to the prerendered /_not-found route, the same ordinary page render that has always served /no-such-page correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5zjYc2BoFV2NjKBBapC7C
hotlong
marked this pull request as ready for review
August 26, 2026 01:02
This was referenced Aug 26, 2026
hotlong
added a commit
that referenced
this pull request
Aug 26, 2026
…213) app/not-found.tsx sits above the [lang] segment and hardcoded en, so a reader browsing in their own language got English 404 copy. A synchronous script reads the locale from the first path segment and sets documentElement.lang, the title and the copy together. Reading a middleware-set header through headers() was built and rejected by measurement: app/not-found.tsx is the root not-found boundary, so a dynamic API in it takes the whole app from 577 prerendered documents to 1. With no prerendered param set left, dynamicParams = false stops rejecting -- /foo.bar/privacy back to 200, /foo.bar/docs back to 500, docs 404s back to the 594-byte empty shell. That undoes #180, #182, #192 and #209 while passing every gate. The route stays prerendered. The served document remains byte-identical across all eleven unmatched URLs (479133a8...), 577 prerendered files, and the seven healthy control pages are byte-identical once content-hashed asset URLs are masked. suppressHydrationWarning is required, not decorative: without it React patches the English copy back in. Zero hydration complaints measured under next dev. With scripting disabled the page is byte-for-byte what main serves today. lang is en between parse and script execution; the page carries noindex and a 404 body is not indexed, so no consumer of parse-time lang exists here. Closes #191
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #182
(Angle-bracket markup omitted throughout — this repo's sanitizer strips it, including inside code fences.)
One line ships:
export const dynamicParams = falseonapps/docs/app/[lang]/docs/[[...slug]]/page.tsx. A one-line flag needs the reason, because it is not a workaround for the boundary — it removes the render the boundary cannot survive.Root cause
notFound()raised while a request is being rendered dynamically escapes both the RSC and the SSR render. Next catches it in the SSR error handler and builds the response fromgetErrorRSCPayload, which seeds the document as an empty error shell and ships the real 404 tree only in the RSC payload, for the client to render after hydration. The status was already a correct 404; the body was blank until client-side React took over, so crawlers and no-JS clients got nothing.No
not-foundboundary participates in that server render — at any depth. That was measured, not assumed: a boundary in the locale segment, that same boundary emitting its own document shell, the shell relocated to the root layout above the boundary, and the experimentalglobal-not-foundconvention were each built and probed, and each left the served bytes unchanged. The card's proposedapp/[lang]/not-found.tsxis byte-for-byte a no-op, which is why this PR does not add one.The path that already worked is not a boundary at all: an unmatched URL such as
/no-such-pageis served by the prerendered/_not-foundroute, an ordinary page render. Two different mechanisms, and only one of them ever produced HTML.dynamicParams = falseroutes an unknown docs slug to that same working route instead of rendering this page so it can throw.notFound()stays inpage.tsx— it still answers a slug that is enumerated but unresolvable, and it is the type-level narrowing forpage.The property change
An unenumerated page now needs a rebuild to appear, rather than rendering on demand. Recorded as PM judgement, in the PM's words:
Maintainer has a veto window on the card.
Verification
Served over HTTP from production builds, plus
next dev. Same-base A/B: both sides built fromb078736, the control being this branch with the flag removed, so nothing is attributed across #190./docs/no-such-pagelang=en, no error shell/zh-Hans/docs/no-such-pagelang=en/ja/docs/no-such-page/docs/build/no-such-page/no-such-page/docs,/zh-Hans/docslang=en/lang=zh-HansStatus is 404 on every 404 row, before and after.
/docs,/zh-Hans/docs,/no-such-pageand/zh-Hans/no-such-pageis identical. The only byte that differs anywhere in those documents is Next's per-build id ("b":"CGHSq3fMjA5qkNUR8HTIL"against"b":"aPwufDC26x71NejjSd_yL"), same length, everything else equal.next devgives the same result, and no missing-root-layout validator break. The only dev warning is the pre-existing middleware-deprecation notice, present onmaintoo./docs/build/data-modeland/zh-Hans/docs/build/flowsboth 308 to their new paths.pnpm turbo run type-check(script name echoed, so it really ran),pnpm turbo run build,pnpm turbo run test.Reproducing this — the card's original command was wrong
A raw
grepover the response cannot tell server-rendered markup from the streamed RSC payload, and reports this defect as already fixed. On the broken build it returns 1 for/docs/no-such-page, and it returns 1 for a healthy 200/docsas well. The 404 copy is in the payload either way. Strip the script bodies first:Worth keeping: the next person to verify a 404 on this site will reach for the naive command, and it will tell them everything is fine.
Not in scope
A docs 404 still declares
enrather than the locale it was requested under, because/_not-foundsits above the locale segment. That half is tracked in #191 and remains open; it is not addressed here.Generated by Claude Code