tabteca
Sign in
ES EN

hreflang: SEO for a bilingual site

How to get Google to index both versions of your site instead of treating one as a copy of the other. With the canonical mistake that erases an entire language from the index.

Getting Google to index your site in two languages takes two things together: each language needs its own URL, and each URL has to declare itself canonical. If the English version declares the Spanish one canonical, you are telling the search engine it is a duplicate and should be ignored — and it will never be indexed, however well translated it is.

That is the one-line mistake that wipes out half the content of a bilingual site. Let’s take it apart, because there is a prior decision that shapes everything else.

1. Choose how the language sits in the URL

Three options, and the decision is hard to reverse:

Form Example For Against
Subdomain en.example.com Total separation Authority split across domains
Path prefix example.com/en/blog What Google understands best Every internal link has to be rewritten
Query parameter example.com/blog?lang=en No link changes at all Weaker signal; you must be strict about everything else

The path prefix is the default choice if you are starting from scratch. It needs the least explaining.

We chose the parameter, and it is worth being honest about why: the site already had dozens of routes and more than eighty internal links, and adding /en/ meant rewriting all of them, including every form’s return path, with a high chance of leaving broken links. The trade-off is a weaker signal and no room for slips in what follows.

What doesn’t work in any of the three forms: serving different languages at the same URL based on the browser or country. A crawler sees one version and the rest doesn’t exist.

2. Make each language’s canonical point at itself

This is the step that decides whether any of it works.

<!-- at /blog/my-post -->
<link rel="canonical" href="https://example.com/blog/my-post" />

<!-- at /blog/my-post?lang=en -->
<link rel="canonical" href="https://example.com/blog/my-post?lang=en" />

The typical failure is building the canonical from the path alone and dropping the language. It looks fine, it validates fine, and it means one of the two versions never gets indexed. If your canonical is built in one place — a base layout — fixing it is one line; if it is written page by page, it is only a matter of time before one is forgotten.

3. Declare the alternates in both directions

Each version lists all versions, including itself:

<link rel="alternate" hreflang="es" href="https://example.com/blog/my-post" />
<link rel="alternate" hreflang="en" href="https://example.com/blog/my-post?lang=en" />
<link rel="alternate" hreflang="x-default" href="https://example.com/blog/my-post" />

Three rules that must hold simultaneously:

  • Reciprocal. If A declares B, B must declare A. Otherwise Google discards the whole cluster.
  • Absolute. Full URLs with protocol and domain, never relative.
  • Self-including. Each page declares itself too.

x-default is where to send anyone who fits none of the declared languages. Point it at whatever you serve when there is no signal at all.

4. Generate all of it in one place

If the canonical and the hreflang tags are written into each template, one day somebody adds a page and forgets. Put it in the base layout, computed from the current path, and the entire class of problem disappears.

Same with the list of languages: a constant, not a repeated literal. The day you add a third one, it is a single line.

5. Repeat the alternates in the sitemap

The hreflang in the HTML and the one in the sitemap say the same thing, and it is worth having both:

<url>
  <loc>https://example.com/blog/my-post</loc>
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/blog/my-post" />
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/blog/my-post?lang=en" />
</url>

On sites where the language rides in a parameter, this is what gives the secondary version a URL the search engine can tell apart. How to build it so it never goes stale is in sitemap.xml and robots.txt.

6. Translate for real, or say so

The final trap. If the translation doesn’t exist and you serve the original language, you have two URLs with identical content and a tag promising different languages. Google notices and stops trusting the cluster.

Two honest ways out:

  • Don’t declare the alternate that doesn’t exist yet.
  • Or serve it with a visible notice saying the translation is missing.

We do the second, and not out of convenience: a silent fallback looks like a site bug, so nobody ever gets round to writing the missing translation. With the notice, the problem is visible and gets fixed.

And one detail everyone forgets: the <html> lang attribute has to change with the content. If the text is English and lang says Spanish, screen readers pronounce it wrongly and you are sending a contradictory signal.

How to verify it works

Four checks, all with curl:

  1. Request the secondary version and confirm the canonical includes the language.
  2. Confirm the hreflang tags are reciprocal between the two versions.
  3. Confirm <html lang> matches the language of the text.
  4. Confirm both URLs are in the sitemap with their alternates.

If you have an SEO check script, put all four in it. This is the kind of thing that breaks when someone refactors a layout and nobody is warned for months, as covered in technical SEO for developers.

Frequently asked questions

Can I put the language in a query parameter instead of the path? You can, and it works if each version’s canonical includes the parameter and the hreflang tags are reciprocal. It is a weaker signal than a path prefix and forgives nothing, so it only makes sense when rewriting every route would be the bigger risk.

Is the content-language meta tag useful? Essentially no: Google hasn’t used it for anything meaningful in years. What counts is hreflang, the <html> lang attribute, and above all the actual language of the text.

Do I need hreflang with only one language? No. It exists purely to group equivalent versions in different languages or regions. With one language, it is noise.

What about the same language across countries, like es-ES and es-MX? You declare them with language and region codes (es-ES, es-MX) and the same rules apply. It is only worth it when the content genuinely differs — prices, taxes, regulation; if it is the same text, you are creating duplicates with extra steps.

Will Google just translate my site if I don’t? It may offer the user a machine translation, but that indexes nothing new and doesn’t rank you in the other language. Appearing in English results requires English content with its own URL.

The directory is the other half of this

115 services with a genuinely free plan, each one with its limits spelled out. No sign-up needed to start looking.

Explore the directory

← All articles

Keep reading

5 min read

Free SEO tools for a small project

The seven pieces you need to measure and fix the SEO of a small project without paying anything, and what to check in each one's free plan before you set it up.

SEOFree planGuides

6 min read

How to get AI assistants to cite your site

ChatGPT, Perplexity and Google's AI summaries cite specific sources. What makes them pick yours: direct answers, HTML without JavaScript, structured data and an llms.txt.

SEOAIGuides

5 min read

Structured data: what to mark up and what not

JSON-LD without the hype: which types actually do something, how to stop the schema promising things the page doesn't show, and why it doesn't raise rankings but does raise clicks.

SEOStructured dataGuides