Skip to content
All articles

Bilingual static sites with Astro: routing, hreflang and honest fallbacks

How this site serves English and Spanish from one codebase without advertising translations that do not exist.

  • Published
  • 1 min read
  • By José María Sanhueza Solovera

Este artículo también está disponible en Español.

This site is published in English and Spanish. English lives at the root (/blog/) and Spanish under a prefix (/es/blog/). That part is easy; the interesting decisions are about what happens when a page exists in only one language.

One route file per page, not per language

Astro’s i18n routing defines the URL scheme. Instead of duplicating every page under src/pages/es/, each page lives once under a rest parameter and is generated for every locale:

---
import { localeStaticPaths } from '../../i18n/locales';

export const getStaticPaths = localeStaticPaths;
const { locale } = Astro.props;
---

localeStaticPaths() returns { lang: undefined } for English and { lang: 'es' } for Spanish, so the same template produces /resume/ and /es/resume/.

Translations are explicit

Articles are files in src/content/blog/<locale>/. Two articles are translations of each other only when they share a translationKey. The slug can then be localized: this article is /blog/bilingual-astro-sites/ in English and /es/blog/sitios-bilingues-con-astro/ in Spanish.

Signal Emitted when
hreflang alternate a translation with the same key exists
Sitemap xhtml:link the same condition, from the same function
Language switcher links to the translation, or to a section page

Fallbacks that do not lie

When a page has no translation, the language switcher still works: it points to the equivalent section in the other language (for example the Spanish blog index) and says so in its accessible name. It never points to a URL that would return a 404, and the page never declares an hreflang alternate for a language it does not have.

Checking it in CI

A test suite runs against the built dist/ folder. It verifies that every internal link resolves to a file, that every hreflang alternate exists and links back, and that drafts are absent from the listings, the RSS feeds and the sitemap.