Repository navigation
fix(docs): point an untranslated locale URL at the English page it serves - #188
Merged
Merged
Conversation
…rves `/ja/docs/operate/backup` has no Japanese source. fumadocs falls back to the default language and serves the English body, so the URL was a second copy of the English page that declared itself canonical. #169 removed it from the sitemap and from every hreflang cluster, but "no longer advertised" is not "no longer indexable" — the in-page language switcher links to it from every translated page. Every field that asserts where this content lives now names the English URL whenever the current locale has no real translation: `alternates.canonical`, `og:url`, the `TechArticle` `url`, and each `BreadcrumbList` `item`. Breadcrumb items resolve crumb by crumb, because a translated page can sit under an untranslated ancestor. `og:locale` and `TechArticle.inLanguage` follow the URL they sit next to — a node naming the English URL while claiming `ja` would assert that the English page is Japanese. `alternates.languages` is untouched: an untranslated locale is already absent from the cluster, which is #169's settled shape. Read out of the prerendered HTML: 218 of 553 docs routes change canonical, which equals the count of untranslated locale/page pairs computed independently from `content/docs/`. All 81 English routes are byte-identical. 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 25, 2026 21:42
This was referenced Aug 25, 2026
hotlong
added a commit
that referenced
this pull request
Aug 26, 2026
…gs to (#190) Fixes #185 The generated share card was locale-independent in two compounding places — getPageImage built its URL from page.slugs alone, and the OG route resolved the page with no language argument — so all seven locales served one English card. A Japanese reader sharing a Japanese page got an English preview. getPageImage(page, lang) now carries the locale, and the route passes that language to the loader. lang is required rather than optional-with-a-default: the defect being fixed is a call site that did not pass a language, and a default is what lets the next one reproduce it silently. The locale is spelled out for English too, because an omitted "en" would make /og/docs/es/... ambiguous between the Spanish card and the English card for a page slugged "es". Static params enumerate over translatedLocales() from the English page set, so a page that exists only in English gets exactly one card: 335 cards, matching an independent count off the content tree, not a naive 553. On an untranslated locale route the card URL follows the #188 fallback rule and names the English card — the body being served is the English page, so the card depicting it is the English card. That also makes contentLang a member of translatedLocales() by construction, so no emitted card URL can fall outside the enumeration, which is what makes dynamicParams = false safe. Build cost: 1.15x wall-clock (50.5s to 58.0s) for 4.24x the image count.
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 #174
Option A, per the adjudication comment on the card: when the current locale has no real translation of a page, every field that asserts where this content lives names the English URL instead of this locale's URL.
/ja/docs/operate/backuphas no Japanese source. fumadocs falls back to the default language and serves the English body, so the URL was a second copy of the English page that declared itself canonical. #169 took it out of the sitemap and out of every hreflang cluster, but "no longer advertised" is not "no longer indexable" — the in-page language switcher links to it from every translated page.The predicate is
translatedLocales(slugs), which landed with #169 and already computes exactly this. No second implementation.The set of URL-asserting fields, and what each one now emits
#166 landed after the card was written and put
openGraph,twitterand two JSON-LD blocks into the same function, so the set is larger than the card describes. Enumerated so it can be checked rather than trusted. Example is/ja/docs/operate/backup(English-only page under a non-English prefix); "unchanged" means byte-identical to23a976b.alternates.canonical…/ja/docs/operate/backup…/docs/operate/backupopenGraph.url(og:url)…/ja/docs/operate/backup…/docs/operate/backupTechArticle.url…/ja/docs/operate/backup…/docs/operate/backupBreadcrumbListitem, per crumb…/ja/docs,…/ja/docs/operate,…/ja/docs/operate/backup…/ja/docs,…/docs/operate,…/docs/operate/backupalternates.languages(hreflang + x-default)openGraph.images/twitter.images(og:image)…/og/docs/operate/backup/image.pnggetPageImagetakes no lang); that is #185's card, deliberately untouchedTechArticle.imageLLMCopyButtonmarkdownUrl,ViewOptionsgithubUrl/ja/docs/…mdx, GitHub blob URLpage.pathkeeps the authored filename.Breadcrumb
itemresolves crumb by crumb, not once per request: a translated page can sit under an untranslated ancestor./ja/docs/configure/permissions/positionsis that case — the page andconfigure/permissionsare both translated into Japanese,configureis not, so only the middle crumb moves to the English URL.Two fields that assert a language, not a URL, and why they moved anyway
og:localeandTechArticle.inLanguagesit on the same object as a URL that changed. Leaving them would have introduced an incoherence rather than found one: aTechArticlenode naming the English URL while claiminginLanguage: "ja"asserts that the English page is Japanese. Both now describe the object their URL names —enon a fallback route, unchanged everywhere else.html langis a different claim — it describes the document being served, translated chrome included — and #181 settled it on the route segment. Untouched.Verification
Read out of the prerendered HTML in
.next/server/app, not out of the source. Two production builds of the same tree, one at23a976band one atea353af, compared file by file.The three cases from the card.
ja/docs/operate/backup.html—canonical,og:url,TechArticle.urland the page's own crumb all moved tohttps://docs.objectos.ai/docs/operate/backup;og:localeandinLanguagemovedjatoen; hreflang, both image URLs andhtml langunchanged.ja/docs/configure/permissions/positions.html—canonicalstill self-referential and unchanged, as areog:url,TechArticle.url,og:locale,inLanguage, hreflang and the images. Only theconfigurecrumb moved, which is the ancestor case above.en/docs/operate/backup.html— unchanged. All 81 English routes are byte-identical across every extracted field.Scale, against an independent oracle. 218 of the 553 docs routes change
canonical. The count of untranslated locale/page pairs, computed straight fromcontent/docs/without going through the app code, is also 218 (zh-Hans 17, ja 40, de 41, es 40, fr 40, ko 40). The number is equal, not merely bounded.No regression anywhere else. Whole-document byte comparison of all 577 prerendered HTML files, normalizing only Next's per-build random
buildIdand the content-hashed asset names:en/routes differ. Zero non-docs routes differ (locale home pages, privacy, terms,_not-found).One residual difference, run down rather than waved past. 47 documents also differ inside the RSC flight payload after that normalization. It is not build nondeterminism: two builds of the identical tree are byte-identical in all 577 files. It is React moving where it outlines a subtree into a lazy chunk, because the shortened URL strings shift every later write position. Concretely, on
de/docs/build/automation/approvals.html, chunk1dgoes from["$","td",null,{"children":["$","strong",null,{"children":"Approver"}]}]to["$","strong",null,{"children":"Approver"}]with thetdinlined into the row — the sametdcontaining the samestrongcontaining the same text, with the chunk boundary one level lower. Checked mechanically across all 370: same element tags, same text leaves, zero documents with a genuinely different tree.Gates, all on
ea353af, exit codes captured before any pipe:pnpm turbo run type-check --continue—Tasks: 1 successful, 1 total,Cached: 0 cached, 1 total(executed, not replayed)pnpm turbo run build—Tasks: 1 successful, 1 total,Cached: 0 cached, 1 total, 47.9spnpm turbo run test --force—Tasks: 1 successful, 1 total,cache bypass, force executingnode .github/scripts/check-node-floor.mjs— "Every declared floor clears what the dependency tree requires, and the declarations agree."node .github/scripts/check-translations.mjs— "translations gate passed"node .github/scripts/check-translation-ownership.mjs --actor … --files …— "This PR touches 0 translation artifact(s) and 1 other file(s)."tsc --noEmit --listFilesconfirmspage.tsxis genuinely in the type-check program, so the green covers the edited file rather than merely excluding it.Scope
One file:
apps/docs/app/[lang]/docs/[[...slug]]/page.tsx.lib/seo.tsis read, not edited —translatedLocalesis used as it landed.lib/source.tsand the OG route are untouched (#185). No changeset flow in this repo, nopackages/.Filed while implementing, not fixed here: #187 —
BreadcrumbListadvertises/docs/referenceand/docs/resourcesas crumb items on 98 prerendered pages, and neither path has a page in any locale. Pre-existing since #166; this PR changes which dead URL is emitted, not the fact that it is dead.Generated by Claude Code