Skip to content
Issue docs

hreflang target does not link back to this page

Importanthreflang_no_return_linkIssue 144

What is this issue?

An hreflang annotation is a claim about two pages at once. When your English page says

<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />

it is telling search engines "the French version of me lives there". Search engines do not take that on trust, because anyone can point an hreflang tag at anyone else's page. They go and look at the French page, and they use the annotation only if the French page names the English page back.

That reply is called the return link. It is not the same tag repeated: the French page answers with its view of your language.

<!-- on https://example.com/en/about -->
<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />

<!-- on https://example.com/fr/a-propos — the return link -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />

For a page to pass this check:

  • Every page it names in its hreflang set names it back, with a tag of that page's own.

This check reports the page that made the unanswered claim, and names the addresses that did not answer.

Example: a shop translates its product pages into French and German. The English page lists all three languages. The German template was copied from a different product, so it lists the German and French versions but not this English one. The English page and the German page are not connected, and the cluster is incomplete.

Why it matters

Google discards an annotation it cannot verify. The return link is the verification. Without it the pair is thrown away, and in practice so is the rest of the cluster — a set of hreflang tags is treated as a group, and a group that does not agree with itself is not a group.

The wrong locale gets served. hreflang is what tells a search engine to show /fr/a-propos to a reader in France and /en/about to a reader in Ireland. When the annotation is discarded, both readers get whichever version the search engine judged strongest on its own — usually the one that has been online longest, or the one with the most links.

The pages compete with each other instead of supporting each other. Two translations of the same page are near-duplicates when they are not connected. Rather than consolidating their signals, they split them.

It is invisible without a crawl of both pages. Nothing on the English page looks wrong. The markup is valid, the language codes parse, the set is self-referencing. The fault is on the other page, which is why a per-page audit cannot find it and why this check compares the whole site at once.

Fixing it makes the annotation usable, which is the whole point of having written it. Because the check is scored per page, resolving it lifts the International lens for every page in the affected cluster.

How to fix it

  1. Read the list of unanswered addresses on the finding. Each one is a page that this page points at and that does not point back.

  2. Add the missing tag on the target page. On each address listed, add a link rel="alternate" in the <head> pointing at this page, carrying the language code of this page:

    <link rel="alternate" hreflang="en" href="https://example.com/en/about" />
  3. Publish the same complete set on every page of the cluster. The most reliable arrangement — and the one every CMS plugin generates — is that all pages in a cluster carry an identical block listing every language including themselves. Then reciprocity is automatic and cannot drift.

  4. Use absolute URLs. href="/fr/a-propos" works, but a full https://example.com/fr/a-propos removes any ambiguity about which host you mean.

  5. Check that the addresses match exactly. A tag pointing at https://example.com/fr/ is answered by a tag pointing back at the address this page is actually served from — not a redirect to it, and not a different spelling of the host.

  6. If the translation no longer exists, remove the tag rather than leaving it pointing at a page that will never answer.

Examples

Example 1 — a one-sided pair

Problematic: the English page claims a French alternate; the French page never mentions it.

<!-- https://example.com/en/about -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />

<!-- https://example.com/fr/a-propos -->
<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />

Corrected: the French page answers.

<!-- https://example.com/fr/a-propos -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />

A template copied from another product points the German page at the wrong English URL.

Problematic:

<!-- https://example.com/en/blue-widget -->
<link rel="alternate" hreflang="de" href="https://example.com/de/blaues-widget" />

<!-- https://example.com/de/blaues-widget -->
<link rel="alternate" hreflang="en" href="https://example.com/en/red-widget" />

The German page answers, but it answers a different question. /en/blue-widget is still unreciprocated.

Corrected:

<!-- https://example.com/de/blaues-widget -->
<link rel="alternate" hreflang="en" href="https://example.com/en/blue-widget" />

Example 3 — what a passing cluster looks like

Every page in the cluster carries the identical block. This is the arrangement that cannot drift, and it is what most CMS plugins emit.

<!-- on ALL THREE of /en/about, /fr/a-propos and /de/ueber-uns -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />
<link rel="alternate" hreflang="de" href="https://example.com/de/ueber-uns" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />

How PixyScan detects this

This is not a per-page check. It cannot be: the answer lives on a different page from the question. PixyScan therefore runs it once, after the crawl, when every page's hreflang set has been recorded.

  1. Collect every page's hreflang set. For each page in the scan, the crawler has already stored the link rel="alternate" tags it declares — the language code and the address of each.

  2. Match each declared address to a page in the scan. Addresses are compared the way the crawl files them: the fragment is dropped, a trailing slash is dropped from the path, campaign parameters are dropped. As a fallback, a target written with www. or a different scheme is matched to the page the crawl stored — unless two crawled pages share that looser address, in which case nothing can say which was meant and the target is left unjudged.

  3. Skip the page's own self-referencing tag. A page naming itself is not another page answering.

  4. Look at the target's own hreflang set. If any tag on it points back at this page — whatever language code that tag carries — the pair is reciprocated and nothing is reported. The language code of the return link is deliberately not compared: the correct reply carries the target's view of this page's language, not the code this page used.

  5. Report the page, listing the addresses that did not answer.

What is deliberately never reported:

  • A target that is not in this scan. A ccTLD or subdomain sibling on another domain is the standard way to build an international site, and the crawl is scoped to one host. We cannot read that page's markup, so we cannot say whether it answers.
  • A target that declares no hreflang at all. That is a different finding, raised against that page, and charging its neighbours for it as well would report one missing tag set several times over.
  • A target that does not answer HTTP 200 or is marked noindex. A page search engines discard has no return link to give; the finding there is "hreflang pointing at a page search engines cannot use".
  • A page that did not itself serve. An error page's markup is not a statement about the site's translations.

What we store

Storage Level

Page Level — the finding is attached to the page whose hreflang set made the unanswered claim, although the answer is computed over the whole scan.


Database Table / Prisma Model

PageInternationalSeo, joined against Url.


Fields Used

Field Type Description
PageInternationalSeo.hreflangTags Json Every link rel="alternate" on the page: { hreflang, href }
PageInternationalSeo.urlId String The page the set belongs to
Url.url String The address the page is filed under, used to match hreflang targets
Url.statusCode Int? Whether the page itself served, so an error page is not judged

Detection Dependencies

  • HTML Document — the link rel="alternate" hreflang tags in the page head
  • The whole scan — every other page's hreflang set, which is what makes this a post-crawl join rather than a per-page check

Further reading