Hreflang Warning: Fix No Return Tags and Wrong Language Codes

Fix the most common hreflang errors: missing return tags, wrong language/region codes, and canonical conflicts. Includes the post-2022 diagnosis workflow now that Search Console's International Targeting report is gone.

Fastest fix: the error is almost always a missing return tag — Page A points to Page B, but Page B doesn’t point back. Make every locale’s <head> emit the same list of <link rel="alternate" hreflang="..."> tags (including a self-reference and an x-default), confirm each target returns HTTP 200, and verify each page’s <link rel="canonical"> points to itself, never across locales. If those three things are true, the warning clears.

hreflang tells Google “this page has an equivalent version in another language/region — show that version to users searching in that language.” Per Google’s localized-versions docs, the rules are strict enough that hand-written hreflang is almost always broken: every locale must reference every other locale, including itself (return tags), language codes must be ISO 639-1, region codes ISO 3166-1 alpha-2, plus an x-default — and one missing or wrong entry invalidates the whole cluster. Google’s exact rule: “If two pages don’t both point to each other, the tags will be ignored.”

Where to even see these warnings (this changed): Search Console’s “International Targeting” report — the page that used to list “No return tags” errors — was deprecated in August 2022 and removed shortly after. As of June 2026 there is no Search Console report that flags hreflang problems. You now diagnose hreflang with the URL Inspection tool and a third-party crawler (covered below). If a guide still tells you to open “International Targeting,” it’s out of date.

Common causes

1. Missing return tag (most common)

Example:

<!-- on /zh/article -->
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/article/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/article/" />

<!-- on /en/article (broken: forgot the zh return tag) -->
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/article/" />
<!-- missing: <link rel="alternate" hreflang="zh" href="...zh..."> -->

If the English page doesn’t reciprocate to Chinese, the entire pair breaks. This is the error that used to surface as “No return tags” in Search Console.

How to confirm (June 2026): Search Console no longer reports this, so use one of these instead:

  • Paste both URLs into the hreflang.org checker and let it verify reciprocity.
  • Or fetch each page and diff its hreflang block — the two pages must emit an identical set of <link rel="alternate"> tags:
for u in /en/article /zh/article; do
  echo "== $u =="
  curl -s "https://yourdomain.com$u/" \
    | grep -oE '<link[^>]*rel="alternate"[^>]*hreflang="[^"]*"[^>]*>'
done
# Both blocks should be byte-for-byte the same (order aside).

2. Wrong language / region codes

WrongRightWhy
zhzh or zh-Hans / zh-Hantzh alone works, but Simplified and Traditional collide if both use it
cnzh-CNcn is a region, not a language
twzh-TWSame
zh-hkzh-HKRegion code must be uppercase (Google tolerates lowercase but other tools don’t)
en-usen-USSame
en-uken-GBUK isn’t an ISO country code; GB is
zh-CN-Hanszh-Hans-CNScript before region
jpjaJapanese is ja; jp is a country code

How to confirm: Grab all <link rel="alternate" hreflang="..."> tags from one page and check each value against the table above.

3. Missing x-default

x-default tells Google “if the user’s language isn’t in any of the listed versions, send them here.” Skip it → Google guesses and may send an English user to your Japanese page.

<link rel="alternate" hreflang="en" href="..." />
<link rel="alternate" hreflang="zh" href="..." />
<link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en/article/" />

Usually x-default points to your primary language (typically English).

4. hreflang URL targets 404 / redirect / noindex

<!-- on /zh/post -->
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/post/" />
<!-- but /en/post/ was deleted, or 301-redirects, or is noindexed -->

Google drops the entire pair.

How to confirm:

for u in /en/post /zh/post /ja/post; do
  echo -n "$u: "
  curl -sI -o /dev/null -w "%{http_code}\n" "https://yourdomain.com$u/"
done
# All should be 200, and none should have noindex in their head

5. Same language declared for two URLs

<!-- on /zh/post -->
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/post-v2/" />
<!-- declared the same lang twice -->

Each hreflang value (e.g., zh) can only appear once. Multiple entries → Google ignores all of them.

6. hreflang conflicts with canonical (the suicide pattern)

The most lethal mistake:

<!-- on /zh/post -->
<link rel="canonical" href="https://yourdomain.com/en/post/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/post/" />
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/post/" />

The canonical pointing to English means “don’t index the Chinese page,” but hreflang is trying to make the Chinese page the destination for zh traffic. Google kicks the Chinese page out of the index entirely; all Chinese search traffic lands on the English page.

Right: canonical must always point to the current page in the same language. Google’s docs are explicit: when you use hreflang, “specify a canonical page in the same language, or the best possible substitute language” — never a cross-locale canonical. So on /zh/post, <link rel="canonical"> must be https://yourdomain.com/zh/post/.

Shortest path to fix

Step 1: Generate hreflang from one helper (never hand-write)

// src/lib/hreflang.js
const BASE = "https://yourdomain.com";
const LANGS = ["en", "zh-Hans", "ja"];  // every locale you support

export function buildHreflang(currentLang, slug) {
  const canonical = `${BASE}/${currentLang}/${slug}/`;
  const alternates = LANGS.map((lang) => ({
    hreflang: lang,
    href: `${BASE}/${lang}/${slug}/`,
  }));
  alternates.push({
    hreflang: "x-default",
    href: `${BASE}/en/${slug}/`,
  });
  return { canonical, alternates };
}

In the layout:

---
import { buildHreflang } from "../lib/hreflang.js";
const { canonical, alternates } = buildHreflang(Astro.props.lang, Astro.props.slug);
---
<link rel="canonical" href={canonical} />
{alternates.map(({ hreflang, href }) => (
  <link rel="alternate" hreflang={hreflang} href={href} />
))}

This guarantees every page emits the same hreflang array and auto-closes the loop.

Step 2: Crawl the build output to verify reciprocity

// scripts/check-hreflang.mjs
import fg from "fast-glob";
import fs from "node:fs";

const files = fg.sync("dist/**/*.html");
const map = new Map();  // url -> Map of hreflang -> url

for (const f of files) {
  const html = fs.readFileSync(f, "utf8");
  const canonical = html.match(/<link\s+rel=["']canonical["']\s+href=["']([^"']+)["']/i)?.[1];
  if (!canonical) continue;
  const alts = [...html.matchAll(/<link\s+rel=["']alternate["']\s+hreflang=["']([^"']+)["']\s+href=["']([^"']+)["']/gi)];
  map.set(canonical, new Map(alts.map(m => [m[1], m[2]])));
}

const issues = [];
for (const [url, alts] of map) {
  for (const [lang, altUrl] of alts) {
    if (lang === "x-default") continue;
    const reverseAlts = map.get(altUrl);
    if (!reverseAlts) issues.push(`${url} → ${altUrl} (${lang}): target not crawled`);
    else if (!reverseAlts.has(getMyLang(url))) issues.push(`${url} ↔ ${altUrl}: missing return tag`);
  }
}

function getMyLang(u) {
  return new URL(u).pathname.split("/")[1];  // /zh/foo → zh
}

console.log(issues.length ? issues.join("\n") : "All hreflang pairs closed ✓");

Run after build for a reciprocity check.

Step 3: Cross-check with a third-party tool

Since Search Console no longer audits hreflang, a crawler is now the primary source of truth:

Step 4: Confirm Google has re-read the page

There is no hreflang report to watch any more, so use the URL Inspection tool in Search Console instead:

  1. Paste a locale URL into URL Inspection → “Test live URL.”
  2. In the rendered HTML / HTTP response, confirm the <link rel="alternate"> tags are present and the canonical is same-locale.
  3. Click “Request indexing” on the corrected URLs to nudge a recrawl. Reflection of any fix still takes roughly 1-3 weeks as Google recrawls the whole cluster — hreflang only takes effect once Google has re-fetched every page in the set.

Step 5: If it still isn’t working after a recrawl

  • Confirm the verified property covers all locale subdirectories (a domain property covers everything; a https://yourdomain.com/en/ URL-prefix property does not see /zh/).
  • Check robots.txt isn’t blocking any locale directory — a blocked target counts as a broken return tag.
  • Make sure HTTP and HTTPS (and www / non-www) aren’t both live; a mix means the hreflang target and the indexed URL differ, which reads as “no return tag.”
  • DevTools the page and confirm the <link> tags are inside <head> — tags injected into <body> (or by client-side JS after load) are ignored.

Prevention

  • All hreflang goes through one helper function — no hand-writing
  • Canonical always points to its own current locale; never cross-locale
  • When adding a new language, deploy return tags site-wide first; don’t “ship English now, add Chinese later”
  • CI runs the hreflang reciprocity check — one missing return tag fails the build
  • ISO codes everywhere: language ISO 639-1, region ISO 3166-1 alpha-2, script zh-Hans / zh-Hant

How to confirm it’s fixed

  1. Crawl the built site (Screaming Frog or the scripts/check-hreflang.mjs above) — the report must show zero missing return tags and zero non-200 hreflang targets.
  2. Spot-check one pair by hand: fetch /en/post/ and /zh/post/ and confirm the hreflang block is identical and each canonical is same-locale.
  3. URL-Inspect a corrected page in Search Console → “Test live URL” → confirm the alternate links render and request indexing.
  4. Give it 1-3 weeks. There is no error count to watch (the International Targeting report is gone), so the real signal is that the correct localized URL starts ranking for searches in that language.

FAQ

Where do I see hreflang errors in Search Console now? You don’t. The “International Targeting” report that listed “No return tags” was deprecated in August 2022 and removed. As of June 2026 Search Console has no hreflang error report. Use a crawler (Screaming Frog) plus the URL Inspection tool instead.

Does hreflang="zh" work, or do I need zh-Hans / zh-Hant? zh alone is valid and works if you only have one Chinese version. The moment you ship both Simplified and Traditional, give them zh-Hans and zh-Hant (script subtags) so they don’t both claim the same zh slot, which makes Google ignore both.

Do I need a self-referencing hreflang tag? Yes. Google requires each page to list itself plus every other locale. A common cause of “no return tags” is emitting alternates for the siblings but forgetting the self-reference, so the cluster never closes.

My pages point to each other but the warning persists — why? Usually a mismatch you can’t see at a glance: a trailing-slash difference, an http:// vs https:// or www vs non-www host, or a target that 301-redirects or is noindex. Any of these makes the “return” URL not match the indexed URL, which Google reads as a missing return tag. Curl each target and confirm a direct 200.

How long until the warning clears after I fix it? About 1-3 weeks. hreflang only takes effect once Google has recrawled every URL in the set, so the slowest-to-recrawl page sets the pace. “Request indexing” on the key URLs speeds it up.

Sitemap hreflang or <link> tags in the <head> — which should I use? Either works; don’t use both for the same URLs (conflicting signals). <head> tags are easiest for small sites. For thousands of URLs, Google recommends the XML-sitemap <xhtml:link> method because it centralizes the annotations.

Tags: #SEO #Google #Search Console #Indexing