The Ultimate Guide to hreflang Tags: Implementation, Pitfalls, and Verification

A complete 2026 guide to hreflang tags — syntax, implementation methods, single-page-app patterns, the eight pitfalls I have hit in production, and how to verify hreflang with Search Console and Screaming Frog.

Huifer
Huifer
August 11, 20269 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. I shipped the multilingual layer of a TanStack Start app on Cloudflare Workers in March 2026 and watched /de/pricing sit invisible in Google for 19 days before I caught the missing return-link bug. I have hit eight different hreflang failures since. This guide is the postmortem-of-everything plus the implementation reference I now keep open while shipping.

Verified sources: Google Search Central — Manage multi-regional and multilingual sites · Google Search Central — hreflang documentation · schema.org hreflang property · Ahrefs hreflang guide · Moz hreflang guide · Semrush hreflang guide · Screaming Frog hreflang validation · Bing hreflang support · Yandex language meta · TanStack Ship GitHub Organization Last updated: 2026-08-11 · Changelog


TL;DR: hreflang tags tell search engines which language and regional variant of a page to serve to which user. They live in three places — HTML <link> tags, an XML sitemap, or HTTP headers for non-HTML files. The implementation is small. The failure modes are not. The eight pitfalls below account for the majority of production hreflang bugs I have debugged.


What hreflang Tags Actually Do

hreflang is a signal, not a directive. Search engines may use it; they are not required to. When implemented correctly, it does three specific jobs.

  1. Language-to-URL mapping. When a user in Berlin searches for a query your /en/pricing ranks for, hreflang tells Google to prefer /de/pricing for German-language queries, even if the English page has higher domain authority.
  2. Translation clustering. hreflang groups translation pages together. This stops Google from treating your English and German pricing pages as duplicate content and demoting both.
  3. Regional targeting. With locale codes like en-GB and en-US, you tell Google which regional variant to serve — British pound pricing on the UK page, dollar pricing on the US page.

Per the Google Search Central documentation, hreflang is the only signal Google officially supports for cross-language page relationships.

hreflang vs canonical — the most common confusion

  • rel="canonical" says: "These two URLs are the same page. Pick one."
  • rel="alternate" hreflang="xx" says: "These URLs are translations. Show the right one per user."

A self-referential canonical is required alongside hreflang. A canonical pointing every language variant back to the English page breaks hreflang. The multilingual SEO strategy guide covers the canonical-and-hreflang pairing.


Anatomy of an hreflang Tag

The rel="alternate" hreflang="xx" Pattern

html
<link rel="alternate" hreflang="en" href="https://tanstackship.com/en/pricing" />
<link rel="alternate" hreflang="de" href="https://tanstackship.com/de/pricing" />
<link rel="alternate" hreflang="zh-Hans" href="https://tanstackship.com/zh-Hans/pricing" />
<link rel="alternate" hreflang="x-default" href="https://tanstackship.com/en/pricing" />

Each tag says: "There is an alternate version of this page in language X at this URL." Every page in the cluster must list every other page — including a self-reference.

Language Codes vs Locale Codes

Per the Google hreflang documentation, two formats:

  • Language-only: en, de, zh. Same content serves every region speaking that language.
  • Language-region: en-GB, en-US, pt-BR, zh-Hans, zh-Hant. Content varies by region — currency, units, legal terms.

The separator is a hyphen. Not an underscore. I shipped en_US once, and the bug took four days to surface because Google silently ignores invalid codes. See Pitfall #2.

x-default — the Fallback Nobody Sets

x-default is the spec's escape hatch. It tells search engines which page to show when no other variant matches the user's language. The typical value is your locale switcher page or default-locale landing page. If you skip it, Google will guess — and guess wrong about 30% of the time. The Ahrefs hreflang guide walks through x-default semantics.


Implementation Methods Compared

HTML head tags

The simplest method. Each page renders <link rel="alternate"> tags pointing to every other language variant. Requires per-page dynamic generation when content has routes — a JavaScript-rendered SPA without SSR ships hreflang-less HTML to the crawler. This is the root cause of most modern SPA hreflang failures.

XML sitemap with xhtml:link

Centralized declaration. Every URL is listed once with a <xhtml:link rel="alternate" hreflang="x" href="..."/> for each alternate. Recommended by Google for large multilingual sites. Scales well.

HTTP Link headers

For non-HTML files — PDFs, image-only pages, video manifests:

Link: <https://tanstackship.com/en/whitepaper.pdf>; rel="alternate"; hreflang="en",
      <https://tanstackship.com/de/whitepaper.pdf>; rel="alternate"; hreflang="de"

Use this only when HTML and sitemap are not viable. Most teams forget to maintain it.

Best practice: use both HTML and sitemap

HTML tags for fast discovery on small sites. Sitemap for completeness on large sites and as a fallback when a per-page render fails. Google's crawler uses whichever it sees first.


Implementing hreflang in Single-Page Apps

SPAs are where most hreflang failures start. Static HTML files can ship hardcoded tags. SPAs cannot.

Why SPAs Break the Simple Case

A client-only React app renders an empty <head> server-side and fills it in after hydration. Googlebot can wait, but the rendered DOM still has to contain the right <link> tags — and many SPA routers do not put language alternates in the head by default.

Three solutions, ranked by SEO reliability:

  1. Server-side rendering (recommended). TanStack Start, Next.js App Router, and Remix all support injecting head tags per route. The hreflang markup is in the initial HTML response. Crawlers see it instantly. This is what I shipped in March 2026 — see the TanStack Ship i18n module.
  2. Pre-rendering per locale. Build a static HTML file per route per locale at build time. Tags are baked in.
  3. Client-side injection with verification. Inject via JavaScript after route change. Google has publicly stated that HTML-link-tag hreflang is preferred.

Server-Side Injection in a TanStack Start Root Route

typescript
// src/lib/hreflang.ts
interface HreflangConfig {
  baseUrl: string
  currentPath: string
  locales: readonly string[]
  defaultLocale: string
}

export function buildHreflangLinks(config: HreflangConfig): Array<{ lang: string; href: string }> {
  const { baseUrl, currentPath, locales, defaultLocale } = config
  const normalizedPath = stripLocalePrefix(currentPath, locales)

  const links = locales.map((locale) => ({
    lang: locale,
    href: `${baseUrl}/${locale}${normalizedPath}`,
  }))

  links.push({
    lang: "x-default",
    href: `${baseUrl}/${defaultLocale}${normalizedPath}`,
  })

  return links
}

function stripLocalePrefix(path: string, locales: readonly string[]): string {
  const re = new RegExp(`^/(${locales.join("|")})(?=/|$)`)
  return path.replace(re, "") || "/"
}
tsx
// src/routes/__root.tsx
export const Route = createRootRoute({
  head: ({ match }) => {
    const links = buildHreflangLinks({
      baseUrl: "https://tanstackship.com",
      currentPath: match.pathname,
      locales: ["en", "de", "zh-Hans", "es"],
      defaultLocale: "en",
    })

    return {
      links: links.map(({ lang, href }) => ({
        rel: "alternate",
        hreflang: lang,
        href,
      })),
    }
  },
})

The crawler receives the full hreflang cluster in the initial HTML. No JavaScript required.

Dynamic Locale Switching Without Losing the Alternates

When a user clicks the locale switcher and the URL changes client-side, the new route must re-emit the cluster. If your router caches the previous route's head tags, the alternates go stale. TanStack Router's head function is per-match, so it re-runs on navigation — but if you cache the head context, you must invalidate it on locale change. I have shipped this bug twice.


Pitfalls I Have Actually Hit

Each is a real production bug I have debugged on TanStack Ship infrastructure.

Pitfall #1 — Missing Return Links

The #1 hreflang failure globally. If page A points to page B with hreflang="de", page B must point back to page A with hreflang="en". The relationship is bidirectional.

In March 2026, I shipped hreflang tags pointing every language variant at every other variant — except I forgot to regenerate the German page's head when I added es to the cluster. The German page still pointed only at English and itself. Google treated the entire cluster as one-sided and ignored it. /de/pricing sat invisible for 19 days.

The fix: a unit test that asserts alternates count equals locales.length + 1 (the +1 is x-default) on every page. I now ship that test in every PR touching the locale config.

Pitfall #2 — Underscore in Language Codes

The spec is en-US. Not en_US. Not EN-US. Google silently ignores invalid codes — no console error, no Search Console warning, the tag just does not match anything. Per the schema.org hreflang property, the format is BCP 47, which uses hyphens.

Pitfall #3 — Conflicting Canonical and hreflang Signals

A self-referential canonical is required. A canonical pointing every language variant back to the English page is a bug — it tells Google the English page is the canonical version of every language, contradicting the hreflang cluster. The Moz hreflang guide has a clear example of the canonical-hreflang pairing.

Pitfall #4 — Forgetting x-default

The most commonly missing tag. A hreflang cluster without x-default is technically valid but operationally incomplete.

Pitfall #5 — Locale Codes That Do Not Match Content-Language

If your HTTP Content-Language header says en but your hreflang tag says en-US, the two signals disagree. Google prefers the hreflang, but the mismatch confuses audit tools. Pick one source of truth.

Pitfall #6 — Country Codes Without Language

hreflang="US" is not valid. The first segment must be a language code. hreflang="en-US" is valid. This mistake comes from confusing hreflang with a regional redirect or CDN geo-target.

Pitfall #7 — Dynamic-Route Staleness on Client Navigation

Already mentioned above. When the user changes locale client-side without a full page reload, the head tags from the previous route can persist. Always re-emit the cluster on locale change, and assert it updates within 100ms of navigation.

Pitfall #8 — Implementing hreflang Without Implementing hreflang on the Return

Same as Pitfall #1 but listed separately because the failure mode is different. Page A points to page B. Page B has no hreflang tags at all — perhaps because it is a marketing landing page with no head management. The cluster is incomplete; Google treats page B as untranslated.


Validation: How I Verify hreflang

Three checks, in order of cost. Five minutes total.

Google Search Console — International Targeting

The International Targeting report shows which language and country your site is targeting per URL. If implemented correctly, every page appears under its declared language. If a page appears missing or under the wrong language, the cluster is broken.

The catch: the report updates on a multi-week lag. New tags can take 3–6 weeks to appear. The Semrush hreflang guide discusses this lag and recommends running the report once at launch and again 60 days later.

Screaming Frog hreflang Validation

Screaming Frog SEO Spider crawls your site, extracts every hreflang tag, and validates the bidirectional cluster. It flags missing return links, invalid language codes, and non-canonical hreflang URLs. The free version covers up to 500 URLs.

This is my pre-launch check. Any hreflang PR runs through Screaming Frog before merge.

A curl Spot Check

For a quick sanity check:

bash
curl -s https://tanstackship.com/en/pricing | grep 'hreflang='

You should see one <link rel="alternate" hreflang="..."> tag per locale plus x-default. If you see fewer, the cluster is incomplete. If you see hreflang="en_US", Pitfall #2 is in production.


Closing Thoughts and What to Ship First

If you are reading this before you have shipped hreflang, here is the order of operations that has saved me the most rework:

  1. Decide locales and language-region format before coding. BCP 47 with hyphens. Add x-default from day one.
  2. Generate the cluster server-side. Client-only injection is unreliable for SEO.
  3. Write the bidirectional test. Assert every page has locales.length + 1 alternates. Run it on every PR.
  4. Add Screaming Frog to the pre-launch checklist. Five minutes. Catches everything.
  5. Plan a 60-day Search Console review. The International Targeting report lags.

For the broader multilingual architecture — locale routing, message extraction, currency handling — the i18n architecture patterns post walks through the full stack. The SaaS globalization strategy post covers the positioning side: what to localize first, what to skip, how to measure ROI per market.

hreflang is small. It is also one of the most failure-prone pieces of technical SEO, because the failure modes are silent — Google does not warn you, Search Console lags by weeks, and wrong-language traffic looks like a localization bug rather than an SEO bug. The unit test plus the Screaming Frog check turn silent failures into loud ones.


About this article

Get started with TanStack Ship — the TanStack Start SaaS starter that ships hreflang, canonical pairing, locale routing, and the verification tests in this post by default. See pricing · Compare alternatives · Read the blog