Hreflang and the Return-Tag Requirement
Hreflang annotations are bidirectional by requirement. If page A declares that page B is its Spanish equivalent but page B does not declare A as its English equivalent, the annotation is not honoured — the documented behaviour is that unconfirmed return tags are ignored. This is the mechanic that makes hreflang harder to deploy than it looks, because a correct set is not a property of one page, it is a property of every page in the cluster simultaneously.
Everything else about hreflang is comparatively simple. The return-tag requirement is where implementations fail.
What the annotation does and does not do
Hreflang tells a search engine that two or more URLs are alternate language or regional versions of the same content, so the appropriate one can be served to the appropriate audience. It is a targeting signal.
It is not a duplication signal, and it does not consolidate anything. Two pages joined by hreflang remain two independently indexed pages. This is the most common misconception: teams deploy hreflang expecting it to resolve duplicate-content concerns between an /en-us/ and an /en-gb/ section, and it does not do that — it tells the engine which one to show where. If you want consolidation, you want a canonical, and the two are not interchangeable. The comparison lives in canonical, redirect, or noindex.
It is also not a ranking factor and does not transfer signals between the alternates.
The self-reference
Every page in a cluster must include an hreflang entry for itself. A three-language cluster means three annotations on each of three pages — nine entries in total, not six.
This trips people because the self-reference feels redundant. Mechanically it is what makes the set enumerable: a crawler reading any one page in the cluster learns the full membership list, including which member it is currently looking at.
A cluster of three, expressed in HTML on the English page:
<!-- on https://example.com/en/pricing/ -->
<link rel="alternate" hreflang="en" href="https://example.com/en/pricing/">
<link rel="alternate" hreflang="es" href="https://example.com/es/precios/">
<link rel="alternate" hreflang="de" href="https://example.com/de/preise/">
The Spanish and German pages carry the identical block — same three entries, same order, unchanged. That sameness is the point: the block describes the cluster, not the page, so the correct implementation is to generate one block per cluster and emit it on every member.
Teams that generate the block per-page, listing “the others,” produce sets that are missing the self-reference and are frequently missing an entry somewhere. Generate once, emit everywhere.
x-default
hreflang="x-default" marks the URL to serve when no listed language or region matches the user. It is optional and useful when you have a language selector page or a genuine global default.
<link rel="alternate" hreflang="x-default" href="https://example.com/">
It participates in the return-tag requirement like any other entry: if it is in one page’s block it must be in all of them.
Value syntax, and the one that is usually wrong
The value is a language code, optionally followed by a region code: en, en-gb, pt-br. Language is ISO 639-1; region, when present, is ISO 3166-1 Alpha-2.
The recurring mistakes:
- A region code alone.
hreflang="uk"is not “United Kingdom” —ukis the language code for Ukrainian. There is no way to say “this region, any language”; language is mandatory and region is the optional part.en-gbis what was meant. - Wrong separator or case. The convention is a hyphen; underscores are invalid. Case is not significant in practice but
en-GBis the readable form. - Invented regions.
en-euis not valid — the EU is not an ISO 3166-1 country code. There is no supranational targeting. en-usused to mean “American English content” when the pages are actually identical. If the content is the same, the annotation is describing a difference that does not exist, and you have created two indexable pages to solve a problem you did not have.
Where to put the annotations
Three delivery mechanisms, and the choice is mostly about maintenance cost.
HTML <link> elements in <head>. The default. Readable, easy to verify with a request, and the only option if you have no control over headers or sitemap generation. The cost is page weight: a 30-language cluster is 30 link elements on every page.
HTTP Link headers. The only option for non-HTML documents — a PDF that exists in several languages, for example. Same semantics, delivered as a header:
Link: <https://example.com/es/precios/>; rel="alternate"; hreflang="es"
XML sitemap annotations. The most maintainable option at scale, because the whole cluster graph lives in one generated file rather than being distributed across templates. It also makes the return-tag requirement mechanically easy to satisfy — you are writing the graph, so you can validate it before publishing. The trade-off is that the annotations are invisible to anyone inspecting a page’s HTML, which makes debugging less immediate. More on what sitemaps carry in what an XML sitemap is actually for.
Pick one and use it exclusively. Mixed delivery is valid but makes verification substantially harder, and conflicting annotations across two mechanisms are a category of bug that is very hard to see.
How the sets break
A page is added to one language and not the others. The new page’s block lists the full cluster; the existing pages’ blocks predate it and do not list the new page. Every annotation pointing at the new page lacks a return tag.
A URL changes on one locale. Someone renames /es/precios/ to /es/tarifas/ and adds a 301. The other pages still point at the old URL. A redirected hreflang target is treated as an unconfirmed return, so the cluster silently degrades. This is a routine casualty of ordinary redirect accretion.
Absolute versus relative URLs. Relative hreflang hrefs are not reliably resolved. Always absolute, always including the protocol and host.
Protocol or host mismatch. Half the entries on https://www. and half on https://. Each variant is a distinct URL, so the return tags do not match the outbound ones. Normalising host and protocol in one rule prevents this — see consolidating www and HTTPS in a single hop.
Hreflang pointing at a non-canonical URL. If page A’s hreflang points at /es/precios/?ref=nav while that URL canonicals to /es/precios/, the annotation targets a URL the engine has been told is not the preferred one. Always annotate canonical URLs.
Trailing-slash inconsistency. /es/precios and /es/precios/ are different URLs; see trailing slashes are different URLs.
Validating it yourself
Build the graph and check it for symmetry. Fetch every page in a cluster, extract every hreflang and href pair, and assert that for every edge A→B there is an edge B→A with A’s own language code.
# extract one page's hreflang set
curl -s https://example.com/en/pricing/ \
| grep -o 'hreflang="[^"]*" href="[^"]*"'
That is the whole test, and it is worth putting in CI for any site with more than a couple of locales, because the failure mode is silent. Search Console’s international targeting report surfaces return-tag errors too, but it reports after the fact; a symmetry check catches the problem before deploy.
The durable rule: treat the hreflang cluster as a single object that happens to be rendered in several places. Every operation on a locale — adding, renaming, retiring — is an edit to that object, and it is not finished until every member reflects it.