From 7e0802f913e8cf793966bd284bea67e0125dcd1f Mon Sep 17 00:00:00 2001 From: hoelee Date: Sat, 19 Sep 2026 21:41:22 +0800 Subject: [PATCH] SEO: real robots.txt + hreflang pairs + BlogPosting schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit robots.txt was absent at the origin, so Cloudflare served its Free-plan Content Signals Policy placeholder — a file with no User-agent, no Sitemap and no directives. Adds src/pages/robots.txt.ts (origin 200 → CF merges instead of substituting) pointing at sitemap-index.xml, disallowing /pagefind/, and expressing search=yes / ai-input=yes / ai-train=no in line with the two-lane SEO+GEO strategy in docs/seo-reference.md. Citation crawlers (Google-Extended, OAI-SearchBot, PerplexityBot, ClaudeBot) stay allowed; only training-only harvesters blocked. hreflang was never emitted despite full EN/ZH i18n: both language versions sat in the sitemap with no cross-reference. BaseLayout gains altLocaleUrl and emits en/zh/x-default per page; every post pair, category pair, and landing page now cross-links. Also adds BlogPosting JSON-LD (@id-linked to the site-wide Person node) so Google has a rich-result-eligible node per article, and creates the missing /zh/about/ page — the EN about page pointed its language switch at a 404, and /about/ was the only unpaired page after hreflang landed. --- src/layouts/BaseLayout.astro | 21 +++++++++ src/pages/about.astro | 7 ++- src/pages/categories/[category].astro | 2 + src/pages/categories/index.astro | 2 + src/pages/index.astro | 2 +- src/pages/posts/[...slug].astro | 25 +++++++++++ src/pages/posts/index.astro | 8 +++- src/pages/robots.txt.ts | 54 ++++++++++++++++++++++++ src/pages/zh/about.astro | 40 ++++++++++++++++++ src/pages/zh/categories/[category].astro | 2 + src/pages/zh/categories/index.astro | 2 + src/pages/zh/index.astro | 1 + 12 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 src/pages/robots.txt.ts create mode 100644 src/pages/zh/about.astro 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