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.
This commit is contained in:
@@ -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
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<!-- hreflang: this page + its other-language twin -->
|
||||
{hreflang.map((alt) => (
|
||||
<link rel="alternate" hreflang={alt.hreflang} href={alt.href} />
|
||||
))}
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:site_name" content={SITE.name} />
|
||||
@@ -69,6 +88,7 @@ const nav = isZh
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:locale" content={SITE.locale} />
|
||||
{lang === 'zh' && <meta property="og:locale:alternate" content="en_US" />}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
@@ -101,6 +121,7 @@ const nav = isZh
|
||||
<script type="application/ld+json" set:html={JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
'@id': `${SITE.url}/#person`,
|
||||
name: SITE.author,
|
||||
alternateName: SITE.handle,
|
||||
url: SITE.url,
|
||||
|
||||
Reference in New Issue
Block a user