diff --git a/scripts/docs-site/build.mjs b/scripts/docs-site/build.mjs
index 4da0e61a7..d91d2f9fd 100644
--- a/scripts/docs-site/build.mjs
+++ b/scripts/docs-site/build.mjs
@@ -292,7 +292,7 @@ function layout({ page, nav, activeTab, html, toc, prev, next }) {
${escapeHtml(title)}
${canonicalUrl ? `` : ""}
-${page.hidden ? '' : ""}
+${hreflangLinks(page)}${page.hidden ? '' : ""}
@@ -909,6 +909,28 @@ function pageRoute(page) {
return page.slug === "index" ? (prefix || "/") : `${prefix}/${page.slug}`;
}
+function hreflangLinks(page) {
+ // hreflang alternates require absolute URLs; skip when no canonical origin is set
+ // or when the page is excluded from indexing.
+ if (!canonicalOrigin || page.hidden) return "";
+ // Collect every locale that publishes this same slug, using the current page for
+ // its own locale and skipping any locale variant that is itself hidden.
+ const variants = [];
+ for (const locale of locales) {
+ const variant = locale.code === page.locale ? page : allPageByKey.get(pageKey(locale.code, page.slug));
+ if (variant && !variant.hidden) variants.push(variant);
+ }
+ // Nothing to cross-link if the page exists in only one locale.
+ if (variants.length < 2) return "";
+ const links = variants.map(
+ (variant) => ``,
+ );
+ // x-default points at the English variant when available, otherwise the current page.
+ const defaultPage = variants.find((variant) => variant.locale === "en") ?? page;
+ links.push(``);
+ return `${links.join("\n")}\n`;
+}
+
function pageMarkdownRoute(page) {
const prefix = page.locale === "en" ? "" : `/${page.locale}`;
return page.slug === "index" ? `${prefix || ""}/index.md` : `${prefix}/${page.slug}.md`;
diff --git a/scripts/docs-site/smoke.mjs b/scripts/docs-site/smoke.mjs
index b9d1bd03f..cab565486 100644
--- a/scripts/docs-site/smoke.mjs
+++ b/scripts/docs-site/smoke.mjs
@@ -128,6 +128,15 @@ if (!/class="tab-link active" href="(?:\/docs)?\/it\/channels"/.test(itChannels)
if (!/
Overview<\/h2>/.test(itChannels)) {
throw new Error("it channels: localized sidebar is missing");
}
+if (!itChannels.includes(``)) {
+ throw new Error("it channels: self-referential hreflang alternate is missing");
+}
+if (!itChannels.includes(``)) {
+ throw new Error("it channels: hreflang alternate back to English is missing");
+}
+if (!itChannels.includes(``)) {
+ throw new Error("it channels: x-default hreflang alternate is missing");
+}
const index = fs.readFileSync(path.join(site, "index.html"), "utf8");
if (!index.includes(``)) {
throw new Error(`index: canonical link should use ${expectedOrigin}`);
@@ -135,6 +144,12 @@ if (!index.includes(``)) {
if (!index.includes(``)) {
throw new Error(`index: og:url should use ${expectedOrigin}`);
}
+if (!index.includes(``)) {
+ throw new Error("index: self-referential hreflang alternate is missing");
+}
+if (!index.includes(``)) {
+ throw new Error("index: x-default hreflang alternate is missing");
+}
const gettingStarted = fs.readFileSync(path.join(site, "start/getting-started/index.html"), "utf8");
const gettingStartedOgImage = `${expectedOrigin}/og/start/getting-started.png`;
if (!fs.existsSync(path.join(site, "og/start/getting-started.png"))) {