Technical article

Back to the blog

How to implement hreflang correctly on a bilingual website

How I connect the Spanish and English versions of my portfolio, and what I would check before trusting a hreflang implementation.

Published
Reading time
9 minutes
Topics
SEO · Astro · hreflang

A bilingual website does not become international just because the navigation is translated and the pages are duplicated.

Once I published the English version of my portfolio, I had two routes for the same piece of content. An article might live at /blog/.../, with its English version at /en/blog/.../. They cover the same subject, but they should not compete with each other or make Google guess which one to show.

That is where hreflang comes in.

It does not translate a page or guarantee that Google will always show one particular language. It declares that several URLs are localised versions of the same content. Google can use that signal when it tries to show the best version for a searcher’s language or region.

For me, hreflang is part of a wider decision. I wanted the portfolio to grow in Spanish and English without having to remember manual tags every time I published a project or article.

It is one part of the technical SEO work on my portfolio, but I wanted to focus here on the part that often breaks once routes start to grow.

Start with the language map, not the tags

The first problem was not HTML. It was knowing which route matched each page.

Static routes do not always keep the same name. The Spanish page /sobre-mi/ maps to /en/about/, while articles share a slug and only change their prefix.

I centralised those relationships in a helper. Starting from the current route, it returns the pair of URLs that represent the same page in Spanish and English.

export function getLocalePaths(pathname) {
  const locale = getLocale(pathname);
  const otherPath = getAlternate(pathname, locale);

  return locale === 'es'
    ? { es: pathname, en: otherPath }
    : { es: otherPath, en: pathname };
}

It is a small part of the project, but it stops SEO from depending on scattered conditions. The language switcher, <head> tags and sitemap all start from the same route relationship.

If that relationship is missing or points to the wrong page, hreflang will not fix it. It would only be documenting the architecture incorrectly.

What every page receives

With that map, the SEO component creates three links in the <head> of each page.

<link rel="alternate" hreflang="es" href="https://www.alvarobarrena.com/blog/.../" />
<link rel="alternate" hreflang="en" href="https://www.alvarobarrena.com/en/blog/.../" />
<link rel="alternate" hreflang="x-default" href="https://www.alvarobarrena.com/blog/.../" />

The first points to Spanish and the second to English. Both appear on both versions of the page. That detail matters. Google asks every version to include itself and link to its alternates. If the Spanish page points to the English one but the English page does not return the link, the relationship can be ignored.

On this portfolio, x-default points to Spanish because it is the main language and there is no separate language-picker page. I use it as a fallback when none of the declared alternatives is a match. It does not make Spanish a universal version or replace the es and en tags.

Canonical and hreflang have different jobs

During the implementation, I had to separate two questions that are easy to mix together.

A canonical tells search engines which URL is preferred for one specific page. That is why each portfolio version has its own canonical.

<link rel="canonical" href="https://www.alvarobarrena.com/en/blog/.../" />

hreflang tells search engines which other localised versions belong to the same content. An English page should not set the Spanish page as its canonical just because they cover the same topic. They are different URLs for different audiences.

Mixing those two signals can create conflicting instructions. The useful relationship is that each page is canonical to itself and also declares its real alternates.

I also include it in the sitemap

HTML is enough to declare alternate pages. Google supports HTML, HTTP headers and sitemaps for this, without preferring one method over another.

I also keep the relationships in the sitemap because it is already generated from the site’s real projects and articles. Every URL carries the es, en and x-default variants.

<url>
  <loc>https://www.alvarobarrena.com/blog/.../</loc>
  <xhtml:link rel="alternate" hreflang="es" href="https://www.alvarobarrena.com/blog/.../" />
  <xhtml:link rel="alternate" hreflang="en" href="https://www.alvarobarrena.com/en/blog/.../" />
</url>

I would not use all three implementations just to stack signals. In this project, the <head> and sitemap use the same route helper, so keeping both does not add manual work. If your language map lives in several places and needs manual updates, one version can easily fall behind.

What I would check before publishing

You do not need a huge website to get hreflang wrong. The problems are usually small relationships nobody checks after a deploy.

I would check the following.

  • URLs are absolute and include https://.
  • Every page links to itself and all of its translations.
  • The link set is the same on both versions.
  • The code describes language and, only when needed, region. en is not the same as en-GB.
  • The linked page responds correctly and contains an equivalent version in the expected language.
  • Every version has its own canonical.
  • The sitemap does not generate a different relationship from the HTML.

The final point matters most to me on a code-generated site. Copying tags into a template can work for two pages. Once there is a blog, projects and new routes, the problem becomes keeping the rule in place without omissions.

Before calling this done, I would run a build and open a Spanish page and its English equivalent. I would inspect the final HTML, not only the Astro component. Browsers and Google receive the generated output, not the intention behind the source code.

What hreflang cannot solve

hreflang does not determine page language for you. Google detects the language of the content with its own systems. It also cannot make up for a weak translation, an English version with four lines next to a full Spanish article, or pages that do not answer the same intent.

I publish both languages together on this blog so I do not create half-finished routes. English is not a translate button at the end of the process. It is another version of the content that needs a title, summary, links and examples that sound natural in English.

That connects with something I learned while rebuilding the portfolio with Astro. Structure is not only about organising components. It is also about preparing a website so every page has a clear purpose and can still be maintained as it grows.

Implementing hreflang is not difficult on its own. Keeping it right after the next article, project or route change is the real job. Centralising the relationship between languages is the decision that keeps maintenance from becoming a list of things to remember.