Zum Hauptinhalt springen
••••••••••••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

Reciprocal hreflang in Astro, only when both languages exist

Astro 5's i18n routing handles URL generation and locale detection, but it doesn't emit for you. The naive fix — emit one alternate per supported locale on e…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

Astro 5's i18n routing handles URL generation and locale detection, but it doesn't emit <link rel="alternate" hreflang> for you.



The naive fix — emit one alternate per supported locale on every page — quietly breaks when an entry only exists in one language. The alternate points at a 404, Google flags the reciprocal mismatch, and the hreflang is ignored. Worst case, it weakens the site's overall multilingual signal.



This post walks through the minimum override: walk Content Collections, detect which languages have the entry, then emit <link rel="alternate"> conditionally from Seo.astro.






What the override looks like



Three pieces:





  1. Helper functions in src/lib/posts.ts that ask the collection "which languages have this slug?"


  2. Seo.astro takes an availableLangs prop and emits <link rel="alternate"> only for those languages


  3. Layouts call the helper and pass the result to Seo.astro






Step 1: existence helpers






import { getCollection } from "astro:content";
import type { Lang } from "../i18n";

const LANGS: Lang[] = ["en", "ja"];

export async function blogAvailableLangs(slugWithoutLang: string): Promise<Lang[]> {
const all = await getCollection("blog", ({ data }) => !data.draft);
return LANGS.filter((lang) =>
all.some((e) => e.id === `${lang}/${slugWithoutLang}`),
);
}






Two details to get right:





  • Drop draft: true in the filter: treating a draft-only language as "available" emits a hreflang pointing at a URL the published build won't serve


  • Pass the slug without the language prefix: Content Collections IDs are ja/2026-05-30-foo. entrySlug strips the prefix before the lookup



For paginated category pages, the equivalent helper checks whether page N exists per language with Math.ceil(inCat.length / pageSize).






Step 2: conditional emission in Seo.astro






---
interface Props {
availableLangs?: Lang[];
lang?: Lang;
// ...
}
const { availableLangs = ["en", "ja"], lang: langProp } = Astro.props;
const lang: Lang = langProp ?? getLangFromUrl(Astro.url);

const currentPath = Astro.url.pathname;
const otherLangPath = altPathForOtherLang(currentPath, lang);
const enPath = lang === "en" ? currentPath : otherLangPath;
const jaPath = lang === "ja" ? currentPath : otherLangPath;
const enHref = new URL(enPath, Astro.site).toString();
const jaHref = new URL(jaPath, Astro.site).toString();
const hasEnVersion = availableLangs.includes("en");
const hasJaVersion = availableLangs.includes("ja");
// x-default points to EN when available; otherwise the only available lang.
const xDefaultHref = hasEnVersion ? enHref : jaHref;
---
{hasEnVersion && <link rel="alternate" hreflang="en" href={enHref} />}
{hasJaVersion && <link rel="alternate" hreflang="ja" href={jaHref} />}
<link rel="alternate" hreflang="x-default" href={xDefaultHref} />






availableLangs = ["en", "ja"] as the default makes top-level pages (/, /about/, /blog/) work without explicit configuration — those are always bilingual. Per-entry layouts must override.



x-default is emitted even for single-language posts, pointing at the only available language. Google's documentation recommends declaring at least one supported locale explicitly.






Step 3: wiring layouts






---
import { entrySlug, blogAvailableLangs } from "../lib/posts";

const slug = entrySlug(entry); // "ja/2026-05-30-foo" → "2026-05-30-foo"
const availableLangs = await blogAvailableLangs(slug);
---
<Seo
title={data.title}
description={data.description}
lang={lang}
availableLangs={availableLangs}
// ...
/>






entrySlug strips the language prefix before the helper lookup. Forgetting this turns every comparison into a miss and the helper returns [] — no <link rel="alternate"> ends up in the output, and the build still passes. That's a silent regression worth watching for.






Pitfalls





  • The default value bites if you forget to override: availableLangs = ["en", "ja"] is the default. A per-entry layout that doesn't pass an override emits alternates for both languages even when only one has an entry — the exact failure mode this whole setup was meant to fix


  • entrySlug placement: stripping the prefix is mandatory. Without it, every helper call returns [] silently


  • Paginated routes: /blog/build/3/ can exist in one language and not the other if the post counts diverge. categoryPageAvailableLangs(category, pageNum, pageSize) handles this


  • x-default for single-language posts: emit it anyway. Google recommends declaring at least one supported locale explicitly






The longer write-up — why routing stays on the official integration, how this interacts with @astrojs/sitemap's hreflang output, and the operational failure modes — is on the Aulvem site → Emitting reciprocal hreflang only when both languages exist — Aulvem's i18n customisation

🔍 CTI & Forensik

Cyber Threat Intelligence & Forensik

ATT&CK-Navigator · IoC-Radar · Exploit-Belege
CTI Threat Relationship Graph
Akteure · Techniken · Beziehungen
2 Knoten · 1 Relationen
CVE / Incident Threat Actor Software MITRE ATT&CK CWE Weakness IoC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Reciprocal hreflang in Astro, only when both languages exist

Thematisch verwandte Begriffe: Reciprocal, hreflang, Astro, only · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag