hreflang is the only major SEO signal where a partial implementation is routinely worse than none at all. A missing tag costs you an optimisation. A malformed cluster can cost you the pages.
That asymmetry is why we treat it as a build-time concern rather than a checklist item. Below are the four failure modes we find most often, in descending order of how much damage they do.
1. Cross-locale canonicalisation
The single most destructive one, and the easiest to introduce by accident.
A Chinese page carries <link rel="canonical" href="https://example.com/pricing/">
pointing at the English original. The author’s intent was “these are the same
page”. The signal’s meaning is “this URL is a duplicate of that one; index that
one instead.”
Google obliges. The Chinese page leaves the index. Not deranked — gone.
<!-- WRONG: the zh page points its canonical at the en page -->
<!-- at https://example.com/zh/pricing/ -->
<link rel="canonical" href="https://example.com/pricing/" />
<!-- RIGHT: every page is its own canonical; hreflang expresses the pairing -->
<link rel="canonical" href="https://example.com/zh/pricing/" />
<link rel="alternate" hreflang="en" href="https://example.com/pricing/" />
<link rel="alternate" hreflang="zh-Hans" href="https://example.com/zh/pricing/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/pricing/" />
Canonical answers “which URL should represent this content”. hreflang answers “which version should this user get”. They are different questions and the first one is destructive when answered wrongly.
2. A cluster pointing at a 404
hreflang annotations are validated as a set. If a page in the cluster returns a
404, a 301, or a noindex, the confirmation link between the pages breaks — and
in practice Google frequently discards the annotations for the entire cluster,
not just the broken edge.
This is why the failure is so common on content sites specifically: someone adds a new English article, the template emits alternates for all configured locales, and the Chinese translation does not exist yet. Every article on the site now ships an hreflang pointing at a URL that has never existed.
The architectural fix is to derive the alternates from the content, not from the locale table. A page emits an alternate for a locale if and only if a published translation exists for it. That is a query, not a configuration constant.
3. Non-reflexive clusters
Every page in a cluster must list every page in the cluster, including itself.
A page that omits its own self-referential hreflang is routinely ignored.
This one is almost always a templating bug: someone writes otherLocales(current)
and maps over it, producing a matrix that is missing its diagonal. It looks
tidier. It does not work.
// WRONG: excludes the current locale, so the cluster is never confirmed
LOCALES.filter((l) => l !== current).map(toAlternate)
// RIGHT: reflexive — every page lists every locale, itself included
LOCALES.map(toAlternate)
4. x-default pointing at a redirector
x-default is the fallback for a user whose language and region give you no
useful signal. It should point at a real, indexable page — normally your primary
locale’s version of that same page.
Two common wrong answers. Pointing it at a language-selection splash page, which sends your least-informed visitors to a fork in the road instead of to content. And pointing it at a geo-redirecting root URL, which means the crawler canonicalises whatever it happens to be served that day.
How to verify a fix
Do not verify hreflang by reading the template. Verify it against the built output, because the template is exactly where the bug is.
- Reflexivity. For every page, assert its own URL appears among its own alternates.
- Absoluteness. Relative hreflang hrefs are not honoured. Assert every href
starts with
https://. - Reachability. Assert every hreflang target resolves to a real file in the build output. This is the check that catches failure mode 2, and it is a twenty-line script.
- Reciprocity. Assert that if A lists B, B lists A.
- One x-default. Exactly one, and it resolves.
We run all five over dist/ on every build. It is not sophisticated, and it has
caught more real defects than any other check we have.