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
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.