From a927ef55db4fd0762ff682f5359f4d2832f5497d Mon Sep 17 00:00:00 2001 From: Ben Bachem <10088265+bezbac@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:05:33 +0200 Subject: [PATCH 1/2] fix: Set correct headers for localized academy pages --- app/academy/(en)/[[...slug]]/page.tsx | 9 ++++++++- app/academy/japan/[[...slug]]/page.tsx | 10 +++++++++- app/japan/page.tsx | 9 +++++---- components/DocBodyChrome.tsx | 4 +++- lib/localization.ts | 25 +++++++++++++++++++++++++ lib/mdx-page.ts | 10 ++++++++-- scripts/generate-sitemap-excludes.js | 1 + 7 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 lib/localization.ts diff --git a/app/academy/(en)/[[...slug]]/page.tsx b/app/academy/(en)/[[...slug]]/page.tsx index dba4517760..31d954340d 100644 --- a/app/academy/(en)/[[...slug]]/page.tsx +++ b/app/academy/(en)/[[...slug]]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation"; import { 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,13 @@ 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); + return buildSectionMetadata(page, "academy", "Academy", slug, { + languages: buildLocalizedAlternates({ + slug, + defaultLocale: "en", + routes: { en: "/academy", "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..a79c759114 100644 --- a/app/academy/japan/[[...slug]]/page.tsx +++ b/app/academy/japan/[[...slug]]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation"; import { academyJaSource } 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,13 @@ 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); + return buildSectionMetadata(page, "academy/japan", "Academy", slug, { + languages: buildLocalizedAlternates({ + slug, + defaultLocale: "en", + routes: { 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}`); + } + + 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", From 6d22a99c720445e7d95ca487787626bf9c01b5bb Mon Sep 17 00:00:00 2001 From: Ben Bachem <10088265+bezbac@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:58:22 +0200 Subject: [PATCH 2/2] Check if localized page exists before adding alternate link --- app/academy/(en)/[[...slug]]/page.tsx | 9 +++++++-- app/academy/japan/[[...slug]]/page.tsx | 9 +++++++-- lib/localization.ts | 4 +++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/academy/(en)/[[...slug]]/page.tsx b/app/academy/(en)/[[...slug]]/page.tsx index 31d954340d..6b9f28a82b 100644 --- a/app/academy/(en)/[[...slug]]/page.tsx +++ b/app/academy/(en)/[[...slug]]/page.tsx @@ -1,6 +1,6 @@ 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"; @@ -22,11 +22,16 @@ export async function generateMetadata({ const { slug = [] } = await params; const page = academySource.getPage(slug); if (!page) return { title: "Not Found" }; + const hasJapanesePage = Boolean(academyJaSource.getPage(slug)); + return buildSectionMetadata(page, "academy", "Academy", slug, { languages: buildLocalizedAlternates({ slug, defaultLocale: "en", - routes: { en: "/academy", "ja-JP": "/academy/japan" }, + routes: { + en: "/academy", + ...(hasJapanesePage ? { "ja-JP": "/academy/japan" } : {}), + }, }), }); } diff --git a/app/academy/japan/[[...slug]]/page.tsx b/app/academy/japan/[[...slug]]/page.tsx index a79c759114..f68a538bf9 100644 --- a/app/academy/japan/[[...slug]]/page.tsx +++ b/app/academy/japan/[[...slug]]/page.tsx @@ -1,6 +1,6 @@ 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"; @@ -40,11 +40,16 @@ export async function generateMetadata({ const { slug = [] } = await params; const page = academyJaSource.getPage(slug); if (!page) return { title: "Not Found" }; + const hasEnglishPage = Boolean(academySource.getPage(slug)); + return buildSectionMetadata(page, "academy/japan", "Academy", slug, { languages: buildLocalizedAlternates({ slug, defaultLocale: "en", - routes: { en: "/academy", "ja-JP": "/academy/japan" }, + routes: { + ...(hasEnglishPage ? { en: "/academy" } : {}), + "ja-JP": "/academy/japan", + }, }), }); } diff --git a/lib/localization.ts b/lib/localization.ts index 873f3cf3be..82f12084fb 100644 --- a/lib/localization.ts +++ b/lib/localization.ts @@ -19,7 +19,9 @@ export function buildLocalizedAlternates({ languages[locale] = buildPageUrl(`${basePath}${slugPath}`); } - languages["x-default"] = languages[defaultLocale]; + if (languages[defaultLocale]) { + languages["x-default"] = languages[defaultLocale]; + } return languages; }