Hreflang tags: how to write them, and the one rule that breaks a whole set

Hreflang tags only work as a set: every version must list every other version including itself, and one missing return link can make Google ignore the group. Here is how to write the codes, where the three placements go, and a command that checks reciprocity before you ship.

Implementation8 min read2518 views
Hreflang tags: how to write them, and the one rule that breaks a whole set

Hreflang tags tell a search engine which language and region each version of a page is written for, and they only work as a set. Three things decide whether yours survive: every version must list every other version including itself, the codes must come from two specific standards, and if one page forgets to point back, Google may ignore the whole group. The tags are cheap to write. The return links are what break.

Before you start

You need at least two versions of the same page, and you need to have decided they are genuinely different languages or regions — not the same text on two hostnames. If the language list is still open, picking the languages before the tags is the cheaper order, because translating a page you will later drop wastes the annotations you wrote for it.

One boundary before the mechanics. Hreflang does not redirect anyone, and it does not detect language. Google states plainly that it uses algorithms, not the annotation, to work out what language a page is in. What the annotation buys you is a machine-readable statement of which URL is meant for which audience, so that near-identical translations are understood as variants of one page rather than as rivals.

Hreflang is a claim about who each page is for. It is only believed when every page makes the same claim.

What hreflang actually does, and what it does not

Google's own page on localized versions is short on promises. It does not say hreflang lifts rankings, and it does not say the annotation is required. What it does say is narrower and more useful: localized versions of a page are treated as duplicates only when the main content stays untranslated. Once the body text is genuinely translated, the annotation is how you connect the versions instead of leaving Google to guess.

Two consequences follow from that sentence. A page that is 90 percent English with a translated menu does not qualify — Google's example of that pattern is a forum that translates only the template. And an annotation on a page whose content is not actually translated is describing a distinction that does not exist.

The three places hreflang tags can live

There are exactly three ways to declare the set, and Google treats them as equivalent. You pick one; there is no benefit to running all three, and the documentation warns that managing three implementations is harder than one.

MethodWhere it goesUse it when
HTML linkOne <link> per version, inside <head>Your pages are HTML you control
HTTP headerOne Link: response header per versionYou cannot edit the HTML, or the file is a PDF
SitemapAn xhtml:link block per URLThe set is large and generated from a feed

The HTML form is the one most sites use, and it has one placement rule that is easy to get wrong: the <link> elements must sit inside a well-formed <head>. If a framework injects them after the body has started, they are outside the head and no longer count. Paste the rendered page into an HTML validator and confirm where they landed.

<head>
  <link rel="alternate" hreflang="en" href="https://example.com/page" />
  <link rel="alternate" hreflang="en-GB" href="https://example.com/en-gb/page" />
  <link rel="alternate" hreflang="de" href="https://example.com/de/seite" />
  <link rel="alternate" hreflang="x-default" href="https://example.com/" />
</head>

How to write the codes so Google accepts them

A value is one or two codes separated by a dash. The first is the language, in ISO 639-1; the optional second is the region, in ISO 3166-1 Alpha 2. Only codes listed in those two standards are supported, and the documentation names es-419 as an example of a code that looks reasonable and is not supported.

Two rules catch most of the invalid values. A region code cannot stand alone: en-GB is valid, GB is not, because the first slot is always the language and Google will not infer one from a country. And the region is a country, not a script — zh-Hans is a script subtag, not an ISO 3166-1 country code, so it does not belong in the second slot.

ValueMeansValid
enEnglish, any regionYes
en-GBEnglish for the United KingdomYes
pt-BRPortuguese for BrazilYes
GBRegion with no languageNo
es-419Latin America, a UN region codeNo
x-defaultEvery language not listed aboveYes

x-default is the fallback, not the default language

The x-default value matches any language that is not explicitly listed by another tag on the page. It is for the page you send an unmatched visitor to — a language selector, or a global home page. It is not a synonym for your main language, and a set can be correct without it. Where it earns its place is on a page that redirects by location: without a fallback, a visitor whose language is not in the set can land in a loop.

Do it in this order

Five steps, each with something you can check before moving on. The whole set takes an afternoon the first time; after that it is a template you paste.

  1. List the versions. Write down every URL that is a translation or a regional variant of the page, including the one you are editing. Done when the list has at least two entries and every entry is a real, reachable page.
  2. Assign one code per version. Use ISO 639-1 for the language and, only if the region matters, ISO 3166-1 Alpha 2 for it. Done when no code uses a region on its own and none uses a code outside the two standards.
  3. Paste the identical set into every version. The same block goes on all of them, and each block includes a link to the page it sits on. Done when you can copy the block from any version and find the same lines in all the others.
  4. Add x-default if you redirect or auto-detect. Point it at the page that serves everyone else. Done when a visitor with an unlisted language has somewhere to land.
  5. Verify the return links. Every URL in the set must be reachable and must itself carry the set. Done when the check below reports no missing return link.

The deliverable: a reciprocity check

This is the part worth keeping. A hreflang set is a directed graph, and the failure that voids it is a missing edge — one page that links out but is not linked back to. The table is what a correct set looks like; the command below tests yours.

PageMust link toCommon bug
enen, en-GB, de, x-defaultLists only the others, not itself
en-GBen, en-GB, de, x-defaultPoints to en but en never points back
deen, en-GB, de, x-defaultAdded later, set never updated
x-defaultThe same setTreated as a language, given a region
# Print every hreflang link on a page, then check the return links by hand
curl -s -L --compressed -A "Mozilla/5.0" https://example.com/de/seite -o page.html

python3 -c "
import re
html = open('page.html').read()
head = html.split('</head>')[0]
for m in re.finditer(r'hreflang=[\"\']([^\"\']+)[\"\'][^>]*?href=[\"\']([^\"\']+)', head):
    print(m.group(1), m.group(2))
"

Read the output as a graph. Every URL you see must, when you run the same command on it, print a line whose href is the page you started from. If one of them does not, the set is broken at that edge, and Google's documentation says the annotations may be ignored or misinterpreted rather than partly applied.

What goes wrong, and how you would notice

Three failures cover almost every broken set, and each one hides behind a page you are not looking at.

  1. A missing return link. The most common failure, and the least visible, because the page that forgets is not the page you are looking at. You notice it only by running the command on every URL in the set, not just one.
  2. A region code used as a language. Someone writes hreflang="uk" meaning Ukrainian, and uk is the country code for the United Kingdom — Ukrainian is uk in ISO 639-1 but the value is read as a language first, so the collision is real. Check each code against both standards rather than trusting the three-letter intuition.
  3. The set placed outside the head. A tag manager or a client-side script appends the links after load. Google reads the delivered HTML; links injected by JavaScript after that are not in the head it saw. View source, not the rendered DOM.

One thing we did not test: how quickly a corrected set is re-read, and whether a previously ignored group is reconsidered. Google's page does not give a timeline, so this chapter does not state one.

Common questions

Do I need hreflang if my site is only in one language?

No. A single-language site has nothing to connect. Hreflang exists to tell apart versions of the same content, and with one version there is no ambiguity to resolve.

Can I use hreflang instead of canonical tags?

No, and the two answer different questions. Canonical says which URL is the one to keep when the same page is reachable more than once; hreflang says which of several translated pages is for which audience. A translated page is not a duplicate of its source, so it needs no canonical pointing at the original.

What happens if one page in the set is broken?

The documentation says annotations may be ignored or not interpreted correctly when return links are missing. That is a statement about the set, not a guarantee about which pages survive, so treat any missing edge as a reason to fix the set rather than to wait and see.

Does hreflang improve rankings?

Google's page describes hreflang as a way to indicate alternate versions, not as a ranking factor, and we did not measure rankings here. The defensible claim is about duplication and serving: it helps the right version reach the right audience, and it keeps translations from being read as rivals.

How many versions can one set hold?

The documentation does not set a number. The practical ceiling is that every version must carry every other version, so the set grows with the square of the number of versions — which is the real argument for keeping the list short and the languages genuinely distinct.

If you are deciding which versions deserve a set at all, the earlier chapter on choosing languages covers that decision, and the field data on how many homepages actually ship the annotation is in the hreflang survey. Both are worth reading before you add a fourth language. And if you want to see what a crawler receives from each version in the first place, how QueryWin works is the product side of the same question.

Part of the QueryWin handbook · Level 2

Hreflang tags: how to write them, and the one rule that breaks a whole set