diff --git a/apps/docs/app/[lang]/privacy/page.tsx b/apps/docs/app/[lang]/privacy/page.tsx index de564e9..1ecb2c6 100644 --- a/apps/docs/app/[lang]/privacy/page.tsx +++ b/apps/docs/app/[lang]/privacy/page.tsx @@ -51,6 +51,22 @@ const content = { }, }; +/** + * The locales this page is actually written in: the keys of the `content` + * record above, which is where this page's copy lives. + * + * `app/sitemap.ts` reads this rather than restating the pair, so a translation + * added to `content` is advertised to crawlers by that edit alone. The renderer + * below still serves English for any other locale, but that fallback is not a + * translation and must not be advertised as one — which is why this is derived + * from `content`, not from `i18n.languages`. + * + * A named export next to a page's default is valid App Router: Next's generated + * route validator (`.next/types/validator.ts`) constrains the *known* page + * exports and ignores additional ones. + */ +export const contentLocales = Object.keys(content); + export default async function PrivacyPage({ params, }: { diff --git a/apps/docs/app/[lang]/terms/page.tsx b/apps/docs/app/[lang]/terms/page.tsx index 6327754..cad168a 100644 --- a/apps/docs/app/[lang]/terms/page.tsx +++ b/apps/docs/app/[lang]/terms/page.tsx @@ -59,6 +59,22 @@ const content = { }, }; +/** + * The locales this page is actually written in: the keys of the `content` + * record above, which is where this page's copy lives. + * + * `app/sitemap.ts` reads this rather than restating the pair, so a translation + * added to `content` is advertised to crawlers by that edit alone. The renderer + * below still serves English for any other locale, but that fallback is not a + * translation and must not be advertised as one — which is why this is derived + * from `content`, not from `i18n.languages`. + * + * A named export next to a page's default is valid App Router: Next's generated + * route validator (`.next/types/validator.ts`) constrains the *known* page + * exports and ignores additional ones. + */ +export const contentLocales = Object.keys(content); + export default async function TermsPage({ params, }: { diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts index 197084a..09728c3 100644 --- a/apps/docs/app/sitemap.ts +++ b/apps/docs/app/sitemap.ts @@ -2,19 +2,11 @@ import type { MetadataRoute } from 'next'; import { source } from '@/lib/source'; import { i18n } from '@/lib/i18n'; import { languageAlternates, localeUrl, translatedLocales } from '@/lib/seo'; +import { contentLocales as privacyLocales } from './[lang]/privacy/page'; +import { contentLocales as termsLocales } from './[lang]/terms/page'; export const revalidate = false; -/** - * Locales in which `privacy` and `terms` are actually written. Those two pages - * carry their copy in a `content` record inside their own route component - * (`app/[lang]/privacy/page.tsx`, `app/[lang]/terms/page.tsx`), which falls back - * to English (`content[lang] ?? content.en`) for anything not listed there — - * the same fallback-shaped defect the docs pages have. Keep this in sync with - * those two records. - */ -const STATIC_PAGE_LOCALES = ['en', 'zh-Hans']; - export default function sitemap(): MetadataRoute.Sitemap { const entries: MetadataRoute.Sitemap = []; @@ -23,8 +15,16 @@ export default function sitemap(): MetadataRoute.Sitemap { // The root exists in every locale: it is a language dispatch page that // redirects to that locale's /docs. { path: '', priority: 1, locales: i18n.languages }, - { path: 'privacy', priority: 0.3, locales: STATIC_PAGE_LOCALES }, - { path: 'terms', priority: 0.3, locales: STATIC_PAGE_LOCALES }, + // `privacy` and `terms` carry their copy in a `content` record inside their + // own route component, and render `content[lang] ?? content.en` for every + // other locale — the same fallback shape the docs pages had. So the locales + // they can honestly advertise are the keys of those records, and each module + // exports its own: this file derives the set instead of restating it, and a + // translation added to either record reaches the sitemap by that edit alone. + // Read separately because they are separate records — they agree today, and + // nothing makes them have to. + { path: 'privacy', priority: 0.3, locales: privacyLocales }, + { path: 'terms', priority: 0.3, locales: termsLocales }, ]; // Documentation pages. `source.getPages()` with no argument returns every