Blocked-by: #182
Split out of #182 when its first dispatch falsified the original fix and established the real mechanism. That card keeps the half with a measured one-line solution; this is the half that needs a design.
What will be true after #182 lands
#182 sets dynamicParams = false on the docs route, so an unknown docs slug stops rendering dynamically and resolves to the prerendered /_not-found route instead. That fixes the blank body — the 404 markup will be server-rendered.
But /_not-found sits above the [lang] segment. It cannot read the locale, and apps/docs/app/not-found.tsx hardcodes en for exactly that reason (#168, deliberately, matching what main rendered before the shell moved down).
So after #182: /zh-Hans/docs/no-such-page will serve a correct, visible, server-rendered 404 page — in English, declaring lang="en", to a reader who was browsing in Chinese.
That is strictly better than today's blank page. It is still wrong.
Why this is not a one-liner
The locale has to come from above the [lang] segment, and there is no route param there. The route this lands on is /_not-found, which by construction never sees the locale segment.
The candidate named in #182's investigation: read a middleware-set header through headers(). middleware.ts already computes the locale for every request (it negotiates and redirects on it), so the value exists — the question is getting it into a route that has no params.
That makes /_not-found a dynamic route. For a 404 that is probably an acceptable trade, but it is a real property change and it is unverified — nobody has built it. Treat it as a hypothesis, the way #182's original fix should have been treated.
Prove the mechanism before writing the implementation. #182 is the cautionary case: its proposed fix was reasonable, widely-recommended, and a measured no-op, and that only surfaced because the dev built it and diffed the output rather than trusting the shape.
Constraints for whoever takes this
Worth pricing honestly before dispatch
The payoff is smaller than #182's. A crawler hitting this URL is told 404 either way, correctly, and a 404 body is not indexed. The real audience is a human who followed a stale link while browsing in their own language and gets an English page — worth fixing for a site that ships seven locales, but not urgent, and it should not be priced as an SEO card.
Do not dispatch before #182 has merged: the route this fix targets is only reached once dynamicParams = false is in place.
Blocked-by: #182
Split out of #182 when its first dispatch falsified the original fix and established the real mechanism. That card keeps the half with a measured one-line solution; this is the half that needs a design.
What will be true after #182 lands
#182 sets
dynamicParams = falseon the docs route, so an unknown docs slug stops rendering dynamically and resolves to the prerendered/_not-foundroute instead. That fixes the blank body — the 404 markup will be server-rendered.But
/_not-foundsits above the[lang]segment. It cannot read the locale, andapps/docs/app/not-found.tsxhardcodesenfor exactly that reason (#168, deliberately, matching whatmainrendered before the shell moved down).So after #182:
/zh-Hans/docs/no-such-pagewill serve a correct, visible, server-rendered 404 page — in English, declaringlang="en", to a reader who was browsing in Chinese.That is strictly better than today's blank page. It is still wrong.
Why this is not a one-liner
The locale has to come from above the
[lang]segment, and there is no route param there. The route this lands on is/_not-found, which by construction never sees the locale segment.The candidate named in #182's investigation: read a middleware-set header through
headers().middleware.tsalready computes the locale for every request (it negotiates and redirects on it), so the value exists — the question is getting it into a route that has no params.That makes
/_not-founda dynamic route. For a 404 that is probably an acceptable trade, but it is a real property change and it is unverified — nobody has built it. Treat it as a hypothesis, the way #182's original fix should have been treated.Prove the mechanism before writing the implementation. #182 is the cautionary case: its proposed fix was reasonable, widely-recommended, and a measured no-op, and that only surfaced because the dev built it and diffed the output rather than trusting the shape.
Constraints for whoever takes this
apps/docs/app/not-found.tsxmust keep serving unmatched URLs correctly — that is Every page in all seven locales declares lang="en" on the root html element #168's behaviour and it works today.app/[lang]/not-found.tsx. A 404 under /docs serves a blank error shell, not the 404 page — notFound() from a docs page never renders server-side #182 measured it as a no-op with byte-identical output and an identical route table. It cannot participate in this render path.langattribute and 404 copy in the requested language.curlfrom A 404 under /docs serves a blank error shell, not the 404 page — notFound() from a docs page never renders server-side #182 — a rawgrepcannot distinguish server-rendered markup from the RSC payload and will report success on a page that has none.Worth pricing honestly before dispatch
The payoff is smaller than #182's. A crawler hitting this URL is told 404 either way, correctly, and a 404 body is not indexed. The real audience is a human who followed a stale link while browsing in their own language and gets an English page — worth fixing for a site that ships seven locales, but not urgent, and it should not be priced as an SEO card.
Do not dispatch before #182 has merged: the route this fix targets is only reached once
dynamicParams = falseis in place.