CategoryContent and search

Multilingual SEO in Astro: declaring versions and regions

7 min read

A multilingual site has one job before any other: tell the search engine that the English, Portuguese and Spanish URLs are the same page in three languages. Without that declaration, each translation is a separate document competing for the same query.

Why the relation between versions comes before the translation

Google Search Central states in Localized versions of your pages that hreflang exists so the engine understands that pages are localized variations of the same content.

Without the annotation, the engine sees three URLs with similar meaning and no stated relation. Each one becomes a candidate for the same query.

There is no penalty. What happens is that the engine chooses for you, and the choice tends to favor the version with the strongest signals rather than the version the reader needs.

Language, region and alternative are three different declarations

Most sites blur these three, and that is where the error starts.

  • The language declaration is the lang attribute on the html element. The HTML standard defines it as the language of the element’s content, and it says nothing about the other versions.
  • The region declaration is the second half of a code such as pt-BR, written in the ISO 3166-1 Alpha 2 format (Localized versions of your pages). A code such as pt alone names the language and no region.
  • The alternative declaration is rel="alternate" hreflang, which links one URL to another. It is the only one of the three that describes a relation between documents.

Astro’s i18n documentation uses a locale key such as pt-br to name the route folder. The i18n option of @astrojs/sitemap maps that key to a language attribute. The key names the address, the attribute names the language, and the two don’t have to be identical.

How hreflang is written and what reciprocity requires

Google documents three ways to publish the annotation: link elements in the head, HTTP response headers, and the sitemap.

The head form looks like this:

<link rel="alternate" hreflang="en" href="https://www.example.com/blog/post/" />
<link rel="alternate" hreflang="pt-BR" href="https://www.example.com/pt-br/blog/post/" />
<link rel="alternate" hreflang="es" href="https://www.example.com/es/blog/post/" />
<link rel="alternate" hreflang="x-default" href="https://www.example.com/blog/post/" />

Two rules come with it, and Google states both as guidelines for every method.

  • Each language version must list itself as well as all other language versions.
  • Alternate URLs must be fully qualified, including the transport method.

The part that usually goes missing is the page pointing to itself. A Portuguese page that lists English and Spanish but not itself leaves the set incomplete, and the engine may discard the group. Google states that tags which don’t point to each other are ignored.

The x-default value marks the page for readers whose language isn’t listed. Google names language selectors and auto-redirecting home pages as the case for it.

What the sitemap adds to the annotation

The sitemap carries the same relation in XML, through xhtml:link entries that list every variant, the page itself included (Localized versions of your pages).

<url>
  <loc>https://www.example.com/pt-br/blog/post/</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://www.example.com/blog/post/" />
  <xhtml:link rel="alternate" hreflang="pt-BR" href="https://www.example.com/pt-br/blog/post/" />
  <xhtml:link rel="alternate" hreflang="es" href="https://www.example.com/es/blog/post/" />
</url>

The sitemap protocol requires the namespace declaration in urlset, entity escaping for every value, and UTF-8 encoding.

Google’s sitemap documentation adds a limit on lastmod: the value is used only when it is consistently and verifiably accurate. Google ignores priority and changefreq.

For a blog, the accurate lastmod per language is the date in that language’s frontmatter. Stamping the build date on every URL teaches the engine to ignore the field.

The orphan hreflang and how to catch it in the build

The most common defect is an alternate URL that returns a 404. It appears when a slug changes in one language, when a translation is removed, or when a path is written by hand.

The page with the tag is still valid, so its report shows nothing new. The group just loses a member.

This check should run in the build. After the site compiles, read the generated HTML, collect every hreflang href, and confirm that each path exists as a directory with an index.html.

The script is short, and it fails the build when an alternate points to a file that was never generated.

What to do when the translation doesn’t exist

Three options exist, and only one of them is free.

  • Publish the annotation for the versions that exist. The versions you never wrote aren’t versions.
  • Skip the automatic redirect to English. Google’s guide asks for a language switch instead of a redirect based on the visitor.
  • Serve a real translation or serve nothing. The same guide warns against boilerplate translations and asks for a clear language indication on the page.

A machine-translated page with the same slug is a duplicate in a new language, and the annotation won’t repair it.

What hreflang doesn’t solve

The annotation is a statement about relation. It isn’t a ranking signal, and it isn’t a language detector.

  • Google states that it doesn’t use hreflang or the html lang attribute to detect the language of a page, and relies on algorithms instead.
  • Real duplicate content stays duplicated. When two URLs hold the same text in the same language, Google’s guide points to a preferred version with rel="canonical", together with hreflang.
  • A weak translation stays weak. The annotation decides which version is shown, not how well that version matches the query.
  • Mirrored pages carry a maintenance cost. Every language is a page to build, review and update, and a site with no owner per language drifts out of sync.

Google describes hreflang as a way to report variations so the engine can understand them. That is all it promises: understanding.

Where the research disagrees with the documentation

The first divergence is about the language switcher. Nielsen Norman Group advises sites to detect country and language from the browser settings, so the shopper never opens the switcher (NN/g, 6 Tips for Improving Language Switchers on Ecommerce Sites).

Google’s guide asks for the opposite, a visible link to the other language versions (Managing multi-regional and multilingual sites). It warns that an automatic redirect hides versions from the crawler.

The two positions aren’t symmetrical. Google reads the crawler, which sends no Accept-Language header and usually arrives from the United States. NN/g reads the shopper, who is interrupted by a choice the browser already knows.

The second divergence is about translation. NN/g accepts a machine translation with a visible label and the original next to it (NN/g, Translate User-Generated Content for Global Audiences).

Google warns against translating only the boilerplate and keeping the bulk of the content in one language (the same guide). The rule in this post sits closer to Google: a real translation or nothing.

NN/g shows the case where a labeled machine translation beats an empty review section. That is a real middle path, and it doesn’t excuse a page with the same slug in a new language.

The third divergence is about scope. NN/g found that nonnative speakers navigate a foreign-language site through images and layout (NN/g, Imagery Helps International Shoppers Navigate Ecommerce Sites).

That qualifies the option of serving nothing. An untranslated page with clear visuals is a studied practice, and translating every language is often not practical.

What to decide in Astro: routes, slug and sitemap

Three structural decisions carry the annotation.

  • Route shape. The Astro i18n guide places content in [locale] folders under src/pages/. With prefixDefaultLocale: false, the default language lives at the root and every other language carries a prefix.
  • One slug per language. Keep the slug in the frontmatter and never derive it from the title, because a title edit would move the alternate URLs.
  • One source of truth for the code. Derive the hreflang value from the route prefix through a single map. The html lang attribute, the Open Graph locale, the sitemap and the inLanguage property of the structured data then can’t disagree (schema.org, inLanguage).

The i18n option of @astrojs/sitemap writes the xhtml:link block from the same locale map. The integration doesn’t emit x-default, so add it in serialize when the site needs a catchall.

The build is where all of this is verified. A page count, an alternate check and an asset audit catch the three defects that reach production: a missing version, an orphan alternate, and a cover image that no page references.

Sources

More articles

See all articles