Skip to content
pageinspection

hreflang: reciprocity, x-default, and the mistakes that void the whole set

hreflang is the only check in this catalogue where being 95% correct is worth roughly the same as doing nothing. It is evaluated as a set, and a set with one bad member is discarded rather than partially honoured. That property explains almost every hreflang problem anyone has.

The short answer

hreflang tells search engines which language and region version of a page to serve, and it fails as a set rather than per page. Every URL in a language group must point at every other one including itself, and a single missing return link can void the whole group. Use language codes, not country codes.

ImportantAudit check · hreflang implementation

Why hreflang fails all at once

An hreflang annotation is a claim about a group: these URLs are the same content for different audiences. A search engine can only act on that claim if the group agrees with itself, so the validation is mutual. Every page in the set must list every other page, including itself, and every listed URL must point back.

When one page in a twelve-language set is missing its return links, the engine cannot tell whether it belongs to the group or was named by mistake. The safe response, and the one it takes, is to ignore the annotations rather than guess. So eleven correct pages get no benefit from work that was eleven-twelfths finished.

There is no partial credit, and no error message anywhere telling you that you did not get it.

What is evaluated across the whole crawl

Three things, evaluated across the whole crawl rather than per page: whether every annotation has a matching return link, whether each declared code is a valid language and region, and whether the URLs named actually resolve with a 200 rather than redirecting or erroring.

It is a site-level check by necessity. Reciprocity is a property of a set, so it cannot be computed from one page in isolation, which is also the limit the widget below reports honestly rather than working around.

Check one page for its language declaration

Paste a URL. A single-page audit can report the page's own lang attribute, which is the cheapest language signal and frequently missing. Reciprocity needs the full crawl, and the widget says so.

Check one page for its language declaration

No signup required. Each free search audits one page, paste any URL to see it in action.

en-UK is not a language code

The single most common invalid value, and it fails silently. The language part is ISO 639-1 and the optional region part is ISO 3166-1 alpha-2, which means the United Kingdom is GB. en-UK looks obviously right to an English speaker and is simply not a code, so the annotation containing it is discarded.

codes.txt
en-UK   invalid   the country code is GB, not UK
en-EU   invalid   the EU is not a country
zh-CN   valid     but consider zh-Hans (script, not region)
pt-BR   valid     Brazilian Portuguese
de      valid     language only, any region
x-default         the fallback for unmatched visitors

Two further traps. The order is language then region, never the reverse, so gb-enis invalid. And region alone is not a thing: there is no way to say "visitors in Germany" without also naming a language, which is what x-default exists to work around.

x-default, and when you genuinely need it

x-defaultnames the page to serve when no other entry matches the visitor. It is not "the English one" and it is not a default language: it is the fallback for everyone your set does not cover.

You need it when you have a language selector, a global landing page, or any set that does not cover most of the world. You do not need it when your set genuinely is exhaustive for your audience, and adding it pointing at an arbitrary language is worse than omitting it, because it commits you to serving that page to unmatched visitors.

The IP-redirect trap is the one that voids sets that otherwise validate.

If /en-us/widgets detects a German visitor and redirects them to /de-de/widgets, then the URL your annotation promised does not return the content it claimed. A crawler following the annotation from a German IP sees a redirect instead of a page and drops the entry. Suggest a locale, never force one.

Three delivery methods, one you should avoid

The annotations can live in three places, and the choice has real consequences for maintenance.

HTML head. The default and the easiest to debug, since view-source shows you the whole set. Cost is page weight: a fifty-language set is fifty elements on every page.

XML sitemap. The right answer at scale. Annotations live in one file, so adding a locale is one edit rather than a template change reaching every page, and the page weight is zero.

sitemap.xml
<url>
  <loc>https://example.com/en-gb/widgets</loc>
  <xhtml:link rel="alternate" hreflang="en-gb"
    href="https://example.com/en-gb/widgets"/>
  <xhtml:link rel="alternate" hreflang="de-de"
    href="https://example.com/de-de/widgets"/>
</url>
<!-- Then a matching <url> block for the German page,
     carrying the same two annotations. Reciprocity is
     still required; the sitemap only moves where the
     claims are stated. -->

HTTP headers. Available, and the one to avoid unless you are annotating non-HTML files such as PDFs. It is invisible in view-source, which makes debugging painful, and it puts SEO configuration in a layer your content team cannot see or edit.

Whichever you pick, pick one. Annotations split across the head and the sitemap are a reliable way to end up with two sets that disagree, and disagreement is the condition that voids both.

Why a voided language set is only Important

Because a broken hreflang set does not remove anything. Every localised page still ranks in its own right, still gets indexed and still serves visitors. What is lost is the engine knowing they are the same content, so the wrong locale surfaces for some searchers and the versions may compete with each other.

The rating is also a judgement about scope: this check only matters at all if you serve multiple locales. For a single-market site there is nothing here to get wrong, which is different from a noindex or a missing canonical, where every site is exposed. See the severity scale for how that weighting works.

Confirming reciprocity

Do not spot-check pages. The failure mode is a property of the whole set, so checking one page tells you almost nothing: it will look correct right up until you discover the one locale that never listed the others.

What you want is a crawl that fetches every URL in the set and verifies the return links in both directions, plus the codes and the status of every target. Then look at the international report as a matrix rather than a list, because a missing return link shows up as a gap in a grid and is invisible in a page-by-page view.

Re-check after adding any locale. Adding a language means editing the annotation block on every existing page in the set, which is the step most likely to be missed, and missing it breaks the locales that were already working.

Questions this check raises

Why is my hreflang being ignored?
Most often because the return links are incomplete. hreflang is reciprocal by design: if page A names page B but B does not name A, the engine cannot verify the relationship and discards the annotation. One missing return link in a set of twelve can invalidate the set.
Is en-UK a valid hreflang value?
No, and it is the most common error in the whole feature. The language subtag is ISO 639-1 and the optional region subtag is ISO 3166-1 alpha-2, which makes the United Kingdom GB. en-UK is silently invalid: nothing errors, and the annotation is simply not applied.
Should hreflang go in the head, the sitemap, or the HTTP header?
The head or the sitemap. The head is the easiest to verify because it is visible in the page source; the sitemap scales better past a few languages and keeps the head small. The header form exists for non-HTML files. Do not use two methods at once, because keeping them in agreement becomes the maintenance burden.