diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index beaf507..6805c07 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -15,6 +15,9 @@ interface Props {
// When provided, the language switch links to it; otherwise it falls back to
// the section landing page (/ for EN, /zh/ for ZH).
switchHref?: string;
+ // Absolute URL of the other-language version of THIS page (built by each
+ // page) — drives hreflang so EN/ZH twins don't compete for the same query.
+ altLocaleUrl?: string;
article?: {
publishedTime: string;
modifiedTime?: string;
@@ -30,12 +33,23 @@ const {
ogImage = '/og-default.png',
type = 'website',
switchHref,
+ altLocaleUrl,
article,
} = Astro.props;
const canonical = new URL(Astro.url.pathname, SITE.url).href;
const ogImageUrl = new URL(ogImage, SITE.url).href;
+// hreflang: only emitted when the page has a known counterpart in the other
+// language. x-default points at the English version (English-first site).
+const hreflang = altLocaleUrl
+ ? [
+ { hreflang: 'en', href: lang === 'en' ? canonical : altLocaleUrl },
+ { hreflang: 'zh', href: lang === 'zh' ? canonical : altLocaleUrl },
+ { hreflang: 'x-default', href: lang === 'en' ? canonical : altLocaleUrl },
+ ]
+ : [];
+
// Language switcher: opposite of current page lang.
// If a per-page translation link is provided, use it; else fall back to the section landing.
const isZh = lang === 'zh';
@@ -59,6 +73,11 @@ const nav = isZh
+
+ {hreflang.map((alt) => (
+
+ ))}
+
@@ -69,6 +88,7 @@ const nav = isZh
+ {lang === 'zh' && }
@@ -101,6 +121,7 @@ const nav = isZh