Back to articles

When Canonical Tags Point Across Languages, Hreflang Breaks

SEOReport Team·
hreflangcanonical-tagsinternational-seomultilingual-seotechnical-seo

A localized page whose canonical points at its English twin cancels its own hreflang annotation, and no browser check can see it. Audit data on the checks that catch the contradiction, plus the day our own 9 locale prefixes failed one of them.

A page can carry 2 tags in the same <head> that address the same crawler about the same URL and say opposite things. On https://example.com/fr/pricing, this says the page is the French member of a cluster:

html
<link rel="alternate" hreflang="fr" href="https://example.com/fr/pricing" />

And this, 1 line below it, says the page is a variant that should be folded into the English original:

html
<link rel="canonical" href="https://example.com/pricing" />

Real sites ship both, from the same template, on every localized route they own. This is the failure that remains after the one we documented in July — the missing hreflang self-reference, still failing 58% of 88 hreflang evaluations in our current data. A site can close that loop, get every self-reference right, and still have each localized page asking to be replaced by its English twin.

A cross-language canonical asks Google to fold the localized URL into the original

A canonical tag is a consolidation request: treat this URL as a variant of that one, and index that one. An hreflang annotation is a routing claim: this URL is the version for this language. Point them at different URLs and the site has issued a request that voids its own claim — the French slot in the cluster is filled by a page that disclaims itself.

Google has been explicit that hreflang is a hint rather than a directive, and canonicalization works the same way: the site supplies signals, Google resolves them. That resolution is the whole problem. With a self-canonical localized page, the publisher decides which URL serves French searchers. With a cross-language canonical, the publisher has handed the decision away while believing the cluster is configured — and the outcome that gets reported later is a localized page that ranks nowhere in its own market while the English page appears in its place.

The generators that produce this are ordinary. A canonical helper built from a route definition rather than the request path emits the unprefixed URL for every locale. A CMS whose hreflang setting is per-locale but whose canonical setting is global does the same. So does a migration that added locale prefixes to URLs and left the canonical pointing at where the content originally lived. In each case the page renders perfectly, the annotations look complete in view-source, and the contradiction is 2 attributes apart.

The dataset

hreflang self-reference presentThe homepage is absent from its own hreflang annotations58% of 88 evaluations; 59% of 34 sites
Homepage canonical presentThe homepage declares no canonical URL at all29% of 49 sites

Methodology: latest completed audit snapshot per domain from our current checkset, approximately May 23 – August 31, 2026, anonymized. Denominators differ by check — 34 to 49 sites — because a check only runs when its inputs exist: the self-reference check yields no verdict on a homepage with no hreflang annotations at all, which is why its site count is the smaller one. Evaluation-level and site-level rates differ because some domains were audited more than once in the window. The sample is self-selected — owners who ran an audit — and skews small-to-mid-size, so treat the rates as directional for that segment.

The second row is the quieter risk. A page with no canonical has not contradicted its hreflang annotation, but it has also not defended the annotation: nothing in the page states which URL it considers authoritative, so a locale variant reachable at more than 1 address is resolved entirely by inference. Getting to a correct cluster runs through canonical hygiene first, which is why our canonical guide is the prerequisite for the hreflang implementation guide, not a companion to it.

The contradiction is only visible from both ends of the cluster

A single page cannot report this failure about itself. /fr/pricing looks internally consistent — an hreflang set, a canonical, both well-formed. The defect appears when you hold the annotation and the annotated page side by side, which is why verifying it means fetching the cluster rather than reading 1 document.

Follow each hreflang annotation to its target and record what comes back: the status code, whether the request redirected, whether a noindex directive is present, and whether the target's own canonical names the URL the annotation pointed to. A target whose canonical names a different URL is the cross-language canonical this article is about.

The lang attribute is a second witness. A page whose html lang names a different language from the locale its own hreflang entry claims is contradicting itself in the same way, and it is often the first visible sign that a localized route is serving the original's content.

An alternate that cannot be fetched is a gap in evidence, not proof of a defect. Record it as unverified and retry it before declaring the cluster broken.

hreflang=fr, self-referencingrel=canonicalhreflang=frthe site stopped deciding/fr/pricing — html lang=fr/pricing — English originalClaim: serve this URL toFrench searchersWhich URL answers aFrench query?Google picks — usually theEnglish URL
Diagram source
graph TD
    FR["/fr/pricing — html lang=fr"]
    EN["/pricing — English original"]
    FR -->|"hreflang=fr, self-referencing"| CLAIM["Claim: serve this URL to French searchers"]
    FR -->|"rel=canonical"| EN
    EN -->|"hreflang=fr"| FR
    CLAIM --> Q{"Which URL answers a French query?"}
    EN --> Q
    Q -->|"the site stopped deciding"| OUT["Google picks — usually the English URL"]

Our own locale prefixes served English until August 17, 2026

We failed this class of defect on seoreport.dev ourselves. Ahrefs Site Audit reported an hreflang and HTML lang mismatch across 184 URLs on our own site. The lang attribute was the symptom; the content was English too, on every one of the 9 locale prefixes, which made each localized URL a duplicate of its English twin — the same outcome a cross-language canonical produces, arrived at from the other direction.

The cause was 1 function boundary. The document shell took its language from a helper that returns the default language whenever window is undefined. On the server, /fr/pricing therefore rendered lang="en" and seeded the English translation bundle, while the component that emits the head links — which reads the path, not the runtime — emitted a correct fr self-hreflang directly beside it. A client effect switched the language after hydration, so every browser-based check passed and no human saw it. Crawlers do not run that effect.

Fixed on August 17, 2026: the shell derives its language from the path, the localized layout resolves the locale bundle in a loader so server render and hydration share it, and a supported locale with no bundle throws instead of quietly serving English under its prefix. Text direction was wrong too — Arabic had been serving ltr.

The regression test is a raw-HTML run with no JavaScript, because a browser check is blind to this entire failure class. Run red-first against production it reported 440 failures across 128 localized URLs, every page at 100% English carryover. Against the fixed build: 0 failures across 144 URLs. A comparison of lang against hreflang would have failed our own site first.

x-default inherits whatever the fallback page declares

Our sample of sites declaring x-default is too small to publish a failure rate. The HTML also proves less than it appears to: it shows that an x-default annotation is declared, not which audience the publisher intended it to serve. The checkable facts are the neighbouring ones — URL shape, target health, reciprocal annotations and canonical alignment.

What the mechanism implies is worth stating anyway. x-default is the fallback for searchers whose language the site does not serve, so it usually points at the primary-market page. If that page carries a cross-language canonical, the fallback for every unmatched searcher on the planet inherits the contradiction, and the contradiction shows up in the target's health rather than in the x-default line itself.

Verify it in raw HTML, per locale, both directions

Fetch the page the way a crawler does and read 3 things:

curl -s -A "Googlebot" https://example.com/fr/pricing | grep -iE '<html|rel="canonical"|hreflang'

The lang attribute, the self-referencing hreflang entry, and the canonical must agree on 1 locale and 1 URL:

html
<!-- On https://example.com/fr/pricing -->
<html lang="fr">
<link rel="canonical" href="https://example.com/fr/pricing" />
<link rel="alternate" hreflang="en" href="https://example.com/pricing" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/pricing" />
<link rel="alternate" hreflang="x-default" href="https://example.com/pricing" />

Then repeat on 1 page per locale, and fetch every URL the annotations name to confirm each one canonicalizes to itself. The rule is short enough to hand to a developer: every member of an hreflang cluster canonicalizes to itself. A cross-language canonical is correct in exactly 1 situation — when the localized URL is not localized at all and serves the same language as the original — and in that situation the URL does not belong in the cluster either.

Search Console's URL Inspection gives the authoritative second opinion on which canonical Google actually selected, per URL. A paid SEOReport diagnosis runs the loop across the cluster and lists each contradicting pair with the page where it was found. The 9 locale prefixes on seoreport.dev pass now, and they pass because we shipped the defect first, on a site whose entire product is finding it.

Get the complete diagnosis of your site

An evidence-backed report and a prioritized action plan, on a plan with monthly credits.