Hreflang Has No Return Tag and Google Ignores the Cluster

Search Console reports "No return tags" on your hreflang cluster. Page A points to page B, but B doesn't point back to A. Google ignores the whole annotation.

Search Console → “International Targeting” or “Pages → Hreflang” reports “No return tags” or “Hreflang annotation has no return tag.” Your English article correctly points to its Chinese translation with <link rel="alternate" hreflang="zh">, but the Chinese page doesn’t link back to English. Google’s hreflang rules require bidirectional confirmation: if A says “my translation is B,” then B must say “my translation is A.” Otherwise Google treats the entire annotation as unreliable and may ignore it.

The symptom can be subtle: the Chinese page ranks for English queries despite a perfectly good English version existing, or wrong-language versions appear in country-specific SERPs. The fix is making every hreflang reference reciprocal.

Common causes

1. EN template emits hreflang, ZH template forgot

The English page layout has hreflang links to all language alternates. The Chinese page layout was copy-pasted from a single-language template and never got hreflang code.

How to spot it: curl https://yoursite.com/zh/articles/foo/ | grep hreflang. If nothing, the Chinese template is missing hreflang entirely.

2. Hreflang URLs don’t match the URL the page is served at

EN page says <link rel="alternate" hreflang="zh" href="https://yoursite.com/zh/articles/foo/">. ZH page is actually served at https://yoursite.com/zh/articles/foo (no trailing slash) or https://www.yoursite.com/zh/articles/foo/ (different host). Google can’t match the round trip.

How to spot it: Compare the URL each side declares for the alternate vs. the canonical URL of the actual page. Trailing slashes, www vs no-www, http vs https — all matter.

3. Hreflang language code mismatch

EN says hreflang="zh", ZH says hreflang="zh-CN". Google considers zh and zh-CN different annotations and won’t pair them.

How to spot it: Grep all hreflang values across the cluster. Must be exact string match.

4. One side has hreflang, the other has a 404 / 301

EN page links to ZH. ZH page was deleted (404) or redirects elsewhere (301). The return tag can’t exist on a missing page.

How to spot it: For each hreflang target, curl -I and confirm 200 OK. Anything else breaks the cluster.

5. Hreflang in HTML head but page is canonicalized to a different URL

The ZH page has hreflang back to EN, but the ZH page also has <link rel="canonical" href="..."> pointing to a third URL. Google follows canonical first and reads hreflang from the canonical target — which doesn’t have the return tag.

How to spot it: For each hreflang member, check its canonical. Canonical must point to itself (or the hreflang must be on the canonical target).

6. Self-referencing hreflang missing

Each language version must include hreflang for itself. EN page should have BOTH hreflang="en" (self) and hreflang="zh" (other). Missing self-reference breaks the cluster.

How to spot it: View source. Count hreflang entries. Should equal the number of language versions, including the current page.

7. Hreflang declared in sitemap, but conflicting tags in HTML

Some teams put hreflang only in sitemap.xml. Others put it in HTML <head>. If both exist and disagree, Google may pick one and miss return tags.

How to spot it: Compare sitemap hreflang block with HTML hreflang tags for the same URL. They must match exactly.

Shortest path to fix

Step 1: Pick one declaration method and stick with it

Three valid methods (pick one, not multiple):

  • HTML <head> tags (most common)
  • HTTP response headers (Link: header)
  • XML sitemap <xhtml:link> blocks

For HTML, every language version must include hreflang for every language including itself.

Step 2: Implement hreflang in a shared layout

Build a single helper that takes the current article’s translation key and emits the full cluster:

---
const { translationKey, currentLang } = Astro.props;
const translations = await getCollection('articles', a =>
  a.data.translationKey === translationKey
);
---
{translations.map(t => (
  <link rel="alternate" hreflang={t.data.lang}
    href={`https://yoursite.com/${t.data.lang}/articles/${t.data.urlSlug}/`} />
))}
<link rel="alternate" hreflang="x-default"
  href={`https://yoursite.com/en/articles/${currentSlug}/`} />

This guarantees every page in the cluster sees the same list and links to all members including itself.

Step 3: Match URL formats exactly

Pick a canonical URL form: https, host with/without www, trailing slash or not. Use it consistently:

# bash check across pages
for url in "https://yoursite.com/en/articles/foo/" "https://yoursite.com/zh/articles/foo/"; do
  curl -s "$url" | grep -oE 'hreflang="[^"]+" href="[^"]+"'
done

Confirm every hreflang URL is exactly the canonical for that language version.

Step 4: Use consistent language codes

ISO 639-1 two-letter codes (en, zh), or extended (en-US, zh-CN). Pick one style and never mix. For Chinese, prefer zh-Hans (simplified) and zh-Hant (traditional) if you need to distinguish scripts; otherwise just zh.

Step 5: Fix canonicals

Each hreflang cluster member must rel=canonical to itself, not to another language version. Cross-language canonical is a common bug that breaks hreflang:

<!-- WRONG: ZH page canonical to EN -->
<link rel="canonical" href="https://yoursite.com/en/articles/foo/">

<!-- RIGHT: ZH page canonical to itself -->
<link rel="canonical" href="https://yoursite.com/zh/articles/foo/">

Step 6: Validate cluster integrity in CI

// scripts/check-hreflang.mjs
import fs from 'node:fs';
import { glob } from 'glob';

const errors = [];
const files = glob.sync('src/content/articles/**/*.mdx');
const byKey = {};

for (const f of files) {
  const fm = parseFrontmatter(f);
  byKey[fm.translationKey] ??= [];
  byKey[fm.translationKey].push({ file: f, lang: fm.lang });
}

for (const [key, members] of Object.entries(byKey)) {
  if (members.length < 2) continue;  // single-lang, skip
  const langs = members.map(m => m.lang);
  for (const m of members) {
    // each member must implicitly include itself
    if (!langs.includes(m.lang)) errors.push(`Missing self: ${m.file}`);
  }
}
if (errors.length) process.exit(1);

Step 7: Resubmit and watch

Search Console → International Targeting (legacy report) and Pages report. “No return tags” warnings should clear within 1-2 weeks of recrawl.

When this is not on you

If a Translation team manually publishes only one language and the other is delayed, hreflang will warn during the gap. That’s expected; warnings clear once both sides exist.

Easy to misdiagnose as

A canonical conflict alone. The two interact: bad canonical makes hreflang ineffective. Always fix canonical-points-to-itself first, then hreflang.

Prevention

  • Use a single shared layout helper for hreflang; never hand-write per-page.
  • CI check that every translation cluster has matching translationKey across files.
  • One canonical URL format site-wide (trailing slash, host, protocol).
  • Canonical points to self for every language version.
  • Audit hreflang quarterly with Screaming Frog or a custom script.

FAQ

  • Do I need x-default? Recommended but not required. Useful for global landing pages with language selectors.
  • Can hreflang point to a different domain? Yes — hreflang="ja" can point to yoursite.jp if your Japanese site lives there. Same return-tag rule applies.

Tags: #SEO #Troubleshooting #Indexing #Search Console #hreflang #international