diff --git a/app/academy/(en)/[[...slug]]/page.tsx b/app/academy/(en)/[[...slug]]/page.tsx index dba4517760..6b9f28a82b 100644 --- a/app/academy/(en)/[[...slug]]/page.tsx +++ b/app/academy/(en)/[[...slug]]/page.tsx @@ -1,8 +1,9 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; -import { academySource } from "@/lib/source"; +import { academyJaSource, academySource } from "@/lib/source"; import { DocsChromePage } from "@/components/DocsChromePage"; import { buildSectionMetadata } from "@/lib/mdx-page"; +import { buildLocalizedAlternates } from "@/lib/localization"; type PageProps = { params: Promise<{ slug?: string[] }>; @@ -21,7 +22,18 @@ export async function generateMetadata({ const { slug = [] } = await params; const page = academySource.getPage(slug); if (!page) return { title: "Not Found" }; - return buildSectionMetadata(page, "academy", "Academy", slug); + const hasJapanesePage = Boolean(academyJaSource.getPage(slug)); + + return buildSectionMetadata(page, "academy", "Academy", slug, { + languages: buildLocalizedAlternates({ + slug, + defaultLocale: "en", + routes: { + en: "/academy", + ...(hasJapanesePage ? { "ja-JP": "/academy/japan" } : {}), + }, + }), + }); } export function generateStaticParams() { diff --git a/app/academy/japan/[[...slug]]/page.tsx b/app/academy/japan/[[...slug]]/page.tsx index 3aa91c5262..f68a538bf9 100644 --- a/app/academy/japan/[[...slug]]/page.tsx +++ b/app/academy/japan/[[...slug]]/page.tsx @@ -1,8 +1,9 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; -import { academyJaSource } from "@/lib/source"; +import { academyJaSource, academySource } from "@/lib/source"; import { DocsChromePage } from "@/components/DocsChromePage"; import { buildSectionMetadata } from "@/lib/mdx-page"; +import { buildLocalizedAlternates } from "@/lib/localization"; type PageProps = { params: Promise<{ slug?: string[] }>; @@ -15,6 +16,7 @@ export default async function JaAcademyPage({ params }: PageProps) { return ( Translation by{" "} @@ -38,7 +40,18 @@ export async function generateMetadata({ const { slug = [] } = await params; const page = academyJaSource.getPage(slug); if (!page) return { title: "Not Found" }; - return buildSectionMetadata(page, "academy/japan", "Academy", slug); + const hasEnglishPage = Boolean(academySource.getPage(slug)); + + return buildSectionMetadata(page, "academy/japan", "Academy", slug, { + languages: buildLocalizedAlternates({ + slug, + defaultLocale: "en", + routes: { + ...(hasEnglishPage ? { en: "/academy" } : {}), + "ja-JP": "/academy/japan", + }, + }), + }); } export function generateStaticParams() { diff --git a/app/japan/page.tsx b/app/japan/page.tsx index 5277badc7b..f814163d58 100644 --- a/app/japan/page.tsx +++ b/app/japan/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { JapanLanding } from "@/components/japan/JapanLanding"; +import { buildLocalizedAlternates } from "@/lib/localization"; export const metadata: Metadata = { title: "Langfuse Cloud Japan — 東京でホストされるLLMオブザーバビリティ", @@ -7,10 +8,10 @@ export const metadata: Metadata = { "Langfuse Cloud Japan — LLMのトレース、プロンプト管理、評価、メトリクスを、AWS ap-northeast-1(東京)とClickHouseで運用。", alternates: { canonical: "https://langfuse.com/japan", - languages: { - en: "https://langfuse.com/japan", - ja: "https://langfuse.com/japan", - }, + languages: buildLocalizedAlternates({ + defaultLocale: "ja-JP", + routes: { "ja-JP": "/japan" }, + }), }, }; diff --git a/components/DocBodyChrome.tsx b/components/DocBodyChrome.tsx index 41e85b29ee..f51f9d664a 100644 --- a/components/DocBodyChrome.tsx +++ b/components/DocBodyChrome.tsx @@ -13,6 +13,7 @@ import type { ReactNode } from "react"; type Props = { children: ReactNode; + lang?: string; /** * When false, renders a plain flex-1 div without prose chrome. * Used by wide/marketing sections (pricing, etc.). @@ -33,6 +34,7 @@ type Props = { */ export function DocBodyChrome({ children, + lang, withProse = true, versionLabel, }: Props) { @@ -48,7 +50,7 @@ export function DocBodyChrome({ } return ( - +
["languages"]; + +export function buildLocalizedAlternates({ + slug = [], + defaultLocale, + routes, +}: { + slug?: string[]; + defaultLocale: string; + routes: Record; +}): Languages { + const slugPath = slug.length > 0 ? `/${slug.join("/")}` : ""; + const languages: Record = {}; + + for (const [locale, basePath] of Object.entries(routes)) { + languages[locale] = buildPageUrl(`${basePath}${slugPath}`); + } + + if (languages[defaultLocale]) { + languages["x-default"] = languages[defaultLocale]; + } + + return languages; +} diff --git a/lib/mdx-page.ts b/lib/mdx-page.ts index 5014bd9e56..58bb8e7a7f 100644 --- a/lib/mdx-page.ts +++ b/lib/mdx-page.ts @@ -110,7 +110,10 @@ export function buildSectionMetadata( section: string, sectionTitle: string, slug: string[], - opts?: { canonicalFallback?: string | null }, + opts?: { + canonicalFallback?: string | null; + languages?: NonNullable["languages"]; + }, ): Metadata { const pageData = page.data; const pagePath = `/${section}${slug.length > 0 ? `/${slug.join("/")}` : ""}`; @@ -135,7 +138,10 @@ export function buildSectionMetadata( return { title: seoTitle, description: page.data.description ?? undefined, - alternates: { canonical: canonicalUrl }, + alternates: { + canonical: canonicalUrl, + ...(opts?.languages ? { languages: opts.languages } : {}), + }, ...(pageData.noindex ? { robots: { index: false, follow: true } } : {}), openGraph: { images: [{ url: ogImage }], diff --git a/scripts/generate-sitemap-excludes.js b/scripts/generate-sitemap-excludes.js index 9a7acf4197..3152169eae 100644 --- a/scripts/generate-sitemap-excludes.js +++ b/scripts/generate-sitemap-excludes.js @@ -68,6 +68,7 @@ function contentPathToRoute(filePath) { changelog: "changelog", faq: "faq", handbook: "handbook", + academy: "academy", security: "security", blog: "blog", customers: "users",