How to Implement Hreflang (Step by Step)
Working code for all three hreflang methods — HTML head tags, HTTP Link headers, and XML sitemap annotations — plus the syntax rules, at-scale CMS automation, and the validation workflow to confirm it actually shipped correctly.
1 evidence signal on this page
- Related live toolreturntag - hreflang checker
There are exactly three ways to add hreflang — HTML link tags in the head, HTTP Link response headers (for non-HTML files like PDFs), or xhtml:link entries in an XML sitemap (best at scale). Pick one; Google treats all three as equivalent and there's no benefit to combining them. Every method obeys the same two non-negotiable rules: self-reference (each page lists itself) and reciprocity (if A points to B, B must point back to A, or Google ignores the pair). Codes are ISO 639-1 language plus optional ISO 3166-1 alpha-2 region; x-default is the fallback. Generate it all from a single source of truth, then validate the rendered head, not just view-source — in my study of 374,756 domains, over 67% of hreflang setups had at least one issue.
TL;DR — There are three ways to add hreflang: small
<link>tags in your page’s<head>, aLink:header your server sends, or entries in your XML sitemap. You only pick one. Whichever you pick, two rules never change: every page lists itself, and every page you link to has to link back — or Google ignores the whole set. Then you check that the tags actually show up in the page the way you meant them to.
Start here: you’ve already decided you need hreflang
This is the how-to. If you’re still not sure whether you need hreflang or what it does, read the hreflang overview first — this page assumes you know you have multiple language or country versions of a page and you just want to build the thing correctly.
The three ways to add it (pick one)
- HTML
<link>tags in the<head>. You add a few lines to the top of each page’s HTML. Easiest to understand, easiest to see, good for smaller sites. - HTTP
Link:headers. The same information, sent by your server in the response instead of in the HTML. This is the only option for files that aren’t HTML — like a PDF, which has no<head>to put tags in. - XML sitemap. You list all the language versions inside your sitemap file instead of on each page. Best for big sites, because you don’t have to touch every page — the whole map lives in one place.
Google treats all three the same. There’s no “faster” or “stronger” one, and there is no benefit to using more than one at a time — that just gives you more places for things to fall out of sync.
Evidence for this claim Google supports equivalent HTML, HTTP-header, and sitemap methods for declaring localized versions; HTTP headers can be used for non-HTML files such as PDFs. Scope: Google Search hreflang implementation methods. Confidence: high · Verified: Google: Localized versionsWhat the HTML version looks like
Say you have a US English page, a UK English page, a German page, and a global homepage that lets people choose. On the US English page you’d put:
<link rel="alternate" hreflang="en-us" href="https://example.com/us/" />
<link rel="alternate" hreflang="en-gb" href="https://example.com/uk/" />
<link rel="alternate" hreflang="de" href="https://example.com/de/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />Read each line as: “there’s an alternate version of this page, it’s for this
language/region, and it lives at this address.” The x-default line is your
fallback for anyone whose language doesn’t match the others.
Two things to notice, because they’re the two rules that make hreflang work:
- The US page lists itself (that first
en-usline points at the US page you’re already on). - Every other page in the set has to carry the same block pointing back. The UK page, the German page, and the homepage all need their own version of these four lines.
The two rules, in plain terms
- Every page points back. If your US page links to your German page, the German page has to link to the US page. Miss that return link and Google throws the pair out. This is the rule people break most.
- Every page points to itself. Each version lists its own URL in the set. Google calls this optional but good practice — do it anyway, it keeps everything consistent.
Use real, complete web addresses
Always use the full address, starting with https://. Not /us/, not
//example.com/us/ — the whole thing, https://example.com/us/. And it has to
match the exact address Google actually indexes: same protocol, same www (or
not), same trailing slash, same capitalization.
Get the codes right
The code is a two-letter language, optionally followed by a dash and a two-letter
country: en, en-us, de, es-mx. The classic mistakes are en-uk (it’s
en-gb — uk actually means Ukrainian) and jp for Japanese (it’s ja).
Then check that it worked
The most important thing beginners miss: after you ship, actually look at the
rendered page to confirm the tags are there and in the <head>. If your tags are
added by JavaScript, they might not show up when you “View Page Source” but will
show up in the live page. Google Search Console’s URL Inspection can show you what
Google actually rendered.
Want full code for the header and sitemap methods, how to automate this across a whole CMS, the exact validation steps, and the mistakes that quietly break clusters? Switch to the Advanced tab.
TL;DR — Three methods, pick one: HTML
<link>tags in the<head>, HTTPLink:headers (the only option for non-HTML files like PDFs), or<xhtml:link>entries in an XML sitemap (best at scale — one machine-generated file, easiest to QA). Google says the three are equivalent and there’s no benefit to combining them. Every method obeys self-reference + reciprocity, uses absolute URLs, and uses ISO 639-1 language + optional ISO 3166-1 alpha-2 region codes. Generate the whole thing from one source of truth — hand-maintained hreflang rots. Then validate the rendered head, crawl the whole cluster for reciprocity, and keep re-checking after every URL change. Do not auto-redirect crawlers by geo.
Before you write a line of code: three decisions
1. Which method. Google is explicit that the choice is about convenience, not performance — “The three methods are equivalent from Google’s perspective and you can choose the method that’s the most convenient for your site.” My rough rule:
Evidence for this claim Google supports equivalent HTML, HTTP-header, and sitemap methods for declaring localized versions; HTTP headers can be used for non-HTML files such as PDFs. Scope: Google Search hreflang implementation methods. Confidence: high · Verified: Google: Localized versions- A handful of URLs, a static or simple site, few locales → HTML
<link>tags. - Non-HTML assets (PDFs, docs) → HTTP
Link:headers (you have no other option — there’s no<head>in a PDF). - Many locales, an existing sitemap pipeline, or a headless/JAMstack setup → XML sitemap, generated programmatically.
The Decision Trees lens walks this as an actual flowchart.
2. One source of truth. Whatever method you pick, the annotations should be generated from one place — a locale/translation table in your database, a CMS relationship field, or (for small sites) a single spreadsheet. The single worst hreflang anti-pattern is hand-maintaining tags per page. The moment one locale’s template drifts, return links go missing and pairs get dropped.
3. Do not combine methods. You can run all three; Google says there’s no benefit, and every extra method is another surface for the three copies to disagree with each other.
Evidence for this claim Google requires fully qualified alternate URLs and reciprocal links, and recommends including each page itself in its alternate set. Scope: Google Search hreflang rules shared by all delivery methods. Confidence: high · Verified: Google: Hreflang guidelinesMethod 1 — HTML <link> tags in the <head>
The syntax, verbatim from Google, is:
<link rel="alternate" hreflang="lang_code" href="url_of_page" />A full working set for a multi-language, multi-region cluster with a fallback:
<link rel="alternate" hreflang="en-us" href="https://example.com/us/" />
<link rel="alternate" hreflang="en-gb" href="https://example.com/uk/" />
<link rel="alternate" hreflang="de" href="https://example.com/de/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />That exact block goes on every page in the cluster (the US page, the UK page, the German page, and the homepage) — each one includes its own self-referencing line. That’s what satisfies reciprocity.
Placement is a hard rule. Google: “The <link> tags must be inside a
well-formed <head> section of the HTML.” And their own troubleshooting advice:
“If in doubt, paste code from your rendered page into an HTML validator to ensure
that the links are inside the <head> element.”
This matters more than it sounds, because of a failure mode most written guides
skip: hreflang tags can be forced out of the <head> and into the <body>,
where they’re invalid. In my Pubcon Vegas 2019 deck
I flagged this directly — tags can’t legitimately live in the body (that would let
one site hijack another’s alternates), but a malformed or injected <p> tag, or an
iframe, can prematurely close the <head> so that everything after it — including
your hreflang — renders inside the <body>. The tag is technically present in
source but silently invalid. The fastest way to debug it is browser DOM
breakpoints: watch where the tags actually land in the rendered DOM, then walk
back to the markup that broke the <head>.
When HTML tags make sense: small-to-medium sites with a manageable number of
locales, where the per-page <link> block doesn’t balloon. On a site with dozens
of locales, every page carries a large block of markup — which is one reason big
sites lean on the sitemap method.
Don’t mix hreflang with other alternate attributes on the same
<link> element. Google’s guidance is explicit that a <link rel="alternate">
carrying hreflang shouldn’t also carry an unrelated alternate attribute like
media in the same tag — if you need both a language/region alternate and a
media-query alternate for the same URL, that’s two separate <link> elements,
not one combined one. Mixing them is an easy way to end up with a tag Google
can’t parse as either.
Method 2 — HTTP Link: headers
Same information, sent in the HTTP response instead of the HTML. The syntax:
Link: <https://example.com/file.pdf>; rel="alternate"; hreflang="en",
<https://de-ch.example.com/file.pdf>; rel="alternate"; hreflang="de-ch"Note the angle brackets around each URL, the semicolon-separated attributes, and the comma separating each alternate. Every URL in the header is comma-appended.
When you need this: non-HTML resources. A PDF, a .doc, an image served
directly — none of them have a <head> to hold <link> tags, so the header is
your only way to attach hreflang to them.
Configuration considerations: you set these headers at the server or CDN layer
(an Apache Header directive, an Nginx add_header, a Cloudflare/Fastly/CloudFront
response-header rule, or your application’s response). Because the header is set by
infrastructure rather than per-document markup, the reciprocity rule is easy to
break here — the English PDF’s header and the German PDF’s header are usually
configured separately, so it’s on you to make sure each one lists the whole set,
itself included. Keep the header generation driven by the same locale table as your
HTML/sitemap approach.
The invariant to hold onto: every alternate response carries the identical complete set, every time. It’s not enough for the English PDF’s header to list the German alternate — the German PDF’s response has to carry the exact same set back (itself plus every other alternate), on every single response, not just the first one a crawler happens to hit. Treat the header as generated output, not a one-off you set and forget.
Method 3 — XML sitemap <xhtml:link> annotations
At scale, this is usually the right call: the whole cluster lives in one (or a few)
machine-generated files, nothing bloats each page’s <head>, and — because the
entire relationship graph is in one place — it’s by far the easiest method to QA.
The syntax:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://www.example.com/english/page.html</loc>
<xhtml:link rel="alternate" hreflang="de"
href="https://www.example.de/deutsch/page.html"/>
<xhtml:link rel="alternate" hreflang="en"
href="https://www.example.com/english/page.html"/>
</url>
<url>
<loc>https://www.example.de/deutsch/page.html</loc>
<xhtml:link rel="alternate" hreflang="de"
href="https://www.example.de/deutsch/page.html"/>
<xhtml:link rel="alternate" hreflang="en"
href="https://www.example.com/english/page.html"/>
</url>
</urlset>Two things that trip people up in the sitemap method:
- The namespace declaration is mandatory. That
xmlns:xhtml="http://www.w3.org/1999/xhtml"on the<urlset>element is not optional decoration — omit it and every<xhtml:link>in the file is invalid. - Every
<url>block must be self-complete. Notice that both<url>blocks above list both alternates, including their own<loc>. Each<url>entry carries the full set (self-reference + all alternates). Reciprocity in a sitemap is just “every URL’s block lists every URL in the cluster, itself included.”
Two behaviors worth knowing so you don’t over-engineer the generator: the order
of the <xhtml:link> children inside a <url> block doesn’t matter to Google —
don’t waste time sorting them — and those <xhtml:link> annotations don’t count
against your sitemap’s 50,000-URL-per-file limit, since they’re children of a
<url> entry, not separate <url> entries themselves.
Generating it programmatically. The whole point of the sitemap method is that
it’s a byproduct of your CMS’s own translation data. If your CMS already knows
that /english/page.html and /deutsch/page.html are translations of each other,
your sitemap generator should walk that relationship table and emit the
<xhtml:link> blocks at build time (or at sitemap-request time). Do that and
hreflang can’t drift out of sync with reality — it’s regenerated from the source of
truth every time, and adding a locale is a data change, not a hand-edit across
hundreds of pages.
For teams without dev resources, I built a lightweight Google Sheets template in my Ahrefs hreflang guide: a Setup tab (pick a default language plus up to four variants), a URLs tab (paste each language’s URLs into columns), and a Results tab that auto-generates the sitemap XML block for you. It’s the “single source of truth” idea made concrete for sites too small to justify a real CMS integration.
The rules that apply no matter which method you pick
These are method-agnostic and non-negotiable.
- Self-reference. Every page (or every
<url>block, or every header) lists itself. Mueller frames this as optional-but-good-practice, but skipping it was present in 18.0% of the domains in my study — automate it and it costs nothing. - Reciprocity. “Each language version must list itself as well as all other language versions.” And the enforcement: “If two pages don’t both point to each other, the tags will be ignored. This is so that someone on another site can’t arbitrarily create a tag naming itself as an alternative version of one of your pages.” One missing return link drops the pair.
- Absolute, fully-qualified URLs. “Alternate URLs must be fully-qualified,
including the transport method (http/https)” — so
https://example.com/foo, never//example.com/fooor/foo. And the URL must match the exact form Google indexes: protocol,www, trailing slash, and case all have to line up, or the return-link match fails.
Getting the codes right
“The first code of the hreflang attribute is the language code (in ISO 639-1
format) followed by an optional second code that represents the region code (in ISO
3166-1 Alpha 2 format).” Combine them with a dash: en-US. You can target a
language on its own (es = Spanish everywhere), but you cannot target a region
on its own — there’s always a language first.
The mistakes I see most often (and saw across the study dataset):
en-UKinstead ofen-GB—ukis Ukrainian, the UK’s region code isgb.jpinstead ofjafor Japanese,cninstead ofzhfor Chinese.- Three-letter codes (
ger,eng) where two-letter ISO 639-1 is required. - Using
EU,UN, orUKas region codes — none are valid ISO 3166-1 alpha-2 targets.
x-default is the reserved value for the fallback — a language selector or an
auto-redirecting home page that serves users who match none of your explicit
locales:
<link rel="alternate" href="https://example.com/" hreflang="x-default" />It’s not mandatory. It was the single most-missing element in my study (56.3% of
domains omitted it), but a missing x-default doesn’t break the cluster the way a
missing reciprocal tag does — Google just falls back to its own language/region
detection for unmatched users. Add it anyway; it’s a checklist item, not a
cluster-breaker. (There’s a dedicated x-default subtopic in this cluster.)
Implementing at scale — CMS and platform approaches
WordPress. Yoast SEO does not generate hreflang on its own. To actually
output tags you need a multilingual plugin — WPML or Polylang. WPML
auto-generates hreflang for every page that has translations, adds an x-default
pointing at your default-language version, and by default injects the annotations
into the XML sitemap; there’s a setting under WPML → Languages → SEO Options
(“Display alternative languages in the HEAD section”) if you want head-tag output
instead of, or in addition to, the sitemap. (See WPML’s docs on using it with
Yoast.)
Shopify. Shopify Markets automatically generates mutually-linked hreflang
tags once you’ve configured markets/languages and the content is published and
linked in navigation — which removes the most common failure mode (missing
reciprocal links) by design. The catch: Markets’ automatic tags are often
language-only (fr, de) rather than language-region (fr-FR, fr-CA), which
under-serves brands that need regional specificity — French for France vs. Canada
vs. Belgium. For that you drop to manual <link> tags in theme.liquid (Shopify’s
own theme hreflang docs
show the pattern) or an app — and be careful, because mixing automatic Markets
tags with manual/app tags is a documented source of conflicts. Pick one.
Custom / headless / enterprise. Generate the annotations from your translation-relationship table at build or serve time, as described in the sitemap section above. This is the same “single source of truth” principle — hreflang becomes a computed output of data your CMS already holds, not a hand-maintained artifact that can rot.
Implementation mistakes that break the cluster
Auto-redirecting by geo/IP. This is a separate failure from syntax errors and it’s worse. If you redirect visitors — and especially crawlers — to a version based on perceived location, you can de-index entire regional clusters, because Googlebot mostly crawls from the US. As I put it in that Pubcon deck: a geo-redirect “would redirect search engines to where they crawl from. Google for instance mostly crawls from the US so we would effectively de-index all geo pages.” There’s a regulatory angle too — potential exposure under EU anti-geoblocking rules. The right pattern is redirect users, never crawlers: detect and (optionally) redirect human visitors, but always let bots reach every URL variant directly, with hreflang doing the actual routing signal. Google’s own multi-regional guidance recommends a non-intrusive suggestion banner over automatic redirection for exactly this reason.
Pointing hreflang at non-canonical, redirected, or noindexed URLs. If a locale’s
URL changes and the redirect goes in but the hreflang still points at the old URL,
your cluster now references a 301 or a 404 (16.9% of domains in my study
referenced broken/redirected pages). Similarly, each variant should canonicalize to
itself; pointing hreflang at a URL that’s canonicalized away, or at a noindexed
page, breaks the return link (8.0% pointed at non-canonical URLs).
Inconsistent URL formats. The hreflang URL and the indexed URL have to be
byte-identical in form — trailing slash, www, protocol, and case all included. A
mismatch here is a silent reciprocity failure.
Validating your implementation after launch
View rendered source, not just raw source. If your hreflang is injected by
client-side JavaScript, curl or Ctrl+U (“View Page Source”) will show nothing —
but the tags exist in the rendered DOM. Confirm with GSC URL Inspection → “Test Live
URL” → “View Tested Page,” or a rendering-capable crawler. This is the single most
common reason someone says “my tags are missing” when they’re actually fine (or the
reverse — present in source but broken because they render in the <body>).
Crawl the whole cluster, don’t spot-check one URL. Reciprocity is a relationship between pages, so checking one page tells you almost nothing. Run a crawler across the whole cluster — Screaming Frog’s hreflang audit or Ahrefs Site Audit — to catch missing return tags, non-canonical targets, and broken references at scale. Ahrefs Site Audit’s Hreflangs tab draws the cluster as a graph with broken links in red, which is far easier to reason about than a CSV.
Verify real SERPs with &hl= and &gl=. Append the host-language (&hl=) and
geolocation (&gl=) parameters to a Google search URL to preview what a given
locale’s results actually look like, rather than guessing from your own location.
Validation is not a one-time step. New locales, URL/redirect changes, and dev/staging config bleeding into production are the recurring causes of post-launch breakage. Build hreflang checks into regression tests and recurring crawls, not just a launch-day QA pass. My framing from the talks: “any number of things can break from masking to things carrying over from dev/test/staging environments.”
Myths worth retiring
- “Sitemaps process faster than HTML tags.” False. Both are resolved at crawl time — I’ve debunked this directly. The sitemap’s advantage is maintainability and QA, not speed.
- “Yandex doesn’t support hreflang in sitemaps.” False — Yandex’s own documentation confirms sitemap hreflang support.
- “Use all three methods for extra signal.” No benefit per Google, and it just multiplies inconsistency risk.
- “A missing
x-defaultbreaks the cluster.” False — it’s optional; Google falls back to its own detection. - “Wrong hreflang gets you penalized.” False — it’s a hint, not a directive. Broken hreflang is ignored, not penalized.
Where to go next
This article is the how-to depth under the hreflang hub — the hub covers what hreflang is, why reciprocity matters, and what Bing does instead. The x-default subtopic goes deep on the fallback value. For the strategy this implements, see the International SEO pillar — hreflang is the technical layer, not a substitute for genuine localization.
AI summary
A condensed take on the Advanced version:
- Three methods, pick exactly one (Google treats them as equivalent; no benefit
to combining):
- HTML
<link>tags in the<head>— small/medium sites; must be in a well-formed<head>, never the<body>; don’t mixhreflangwith another alternate attribute likemediaon the same<link>element. - HTTP
Link:headers — the only option for non-HTML files (PDFs); set at server/CDN; every alternate response must carry the identical complete set, every time. - XML sitemap
<xhtml:link>— best at scale; needs thexmlns:xhtml="http://www.w3.org/1999/xhtml"namespace on<urlset>; easiest to QA because the whole graph is in one file; child order doesn’t matter and these children don’t count toward the 50,000-URL sitemap limit.
- HTML
- Two universal rules: self-reference (each page lists itself) and reciprocity (A→B requires B→A, or the pair is ignored). Use absolute, fully-qualified URLs that match the exact indexed form.
- Codes: ISO 639-1 language + optional ISO 3166-1 alpha-2 region (
en-GB, noten-UK;ja, notjp).x-defaultis the reserved fallback — optional but the most-missed element (56.3% in my study). - Automate from one source of truth. WordPress needs WPML/Polylang (Yoast alone does nothing); Shopify Markets auto-generates reciprocal tags but often language-only; custom/headless should emit hreflang from the translation table.
- Don’t auto-redirect crawlers by geo — Googlebot crawls mostly from the US, so it can de-index regional clusters; redirect users, never bots.
- Validate the rendered head, not just view-source (JS-injected tags won’t show
in source); crawl the whole cluster for reciprocity; check locales with
&hl=/&gl=; re-check after every URL change.
Official documentation
Primary-source documentation for implementing hreflang.
- Localized versions of your pages — the primary implementation doc: all three methods with exact syntax, the reciprocity requirement, valid codes, the absolute-URL rule, and
x-default. - Managing multi-regional and multilingual sites — URL-structure options and the auto-redirect / cloaking warning (why you must not geo-redirect crawlers).
- Tell Google about localized versions (x-default blog, 2013) — the original introduction of
x-default. - International Targeting report deprecation (Sept 2022) — the old report is gone; hreflang tags still work.
Bing / Microsoft
- Bing Webmaster Guidelines — Bing’s guidance; note Bing leans on
content-languageand<html lang>over hreflang. - Bingbot Series: Maximizing Crawl Efficiency — context on how Bing handles international/multilingual sites.
CMS / platform
- WPML — Using WordPress SEO (Yoast) with WPML — how WPML outputs hreflang (head vs. sitemap setting).
- Shopify — Add hreflang tags in your theme — the manual
theme.liquid<link>pattern.
Quotes from the source
On-the-record statements relevant to implementation. Each Google-docs link is a deep link that jumps to the quoted passage on the live page.
Google — the three methods
- “The three methods are equivalent from Google’s perspective and you can choose the method that’s the most convenient for your site.” — Google Search Central docs. Jump to quote
Google — placement
- “The
<link>tags must be inside a well-formed<head>section of the HTML.” — Google Search Central docs. Jump to quote
Google — the two rules
- “Each language version must list itself as well as all other language versions.” — Google Search Central docs. Jump to quote
- “If two pages don’t both point to each other, the tags will be ignored.” — Google Search Central docs. Jump to quote
- “Alternate URLs must be fully-qualified, including the transport method (http/https).” — Google Search Central docs. Jump to quote
Google — codes
- “The first code of the hreflang attribute is the language code (in ISO 639-1 format) followed by an optional second code that represents the region code (in ISO 3166-1 Alpha 2 format).” — Google Search Central docs. Jump to quote
John Mueller, Google — it’s a hint, not a directive
- On self-referential tags: hreflang self-references are reported as optional but good practice. — John Mueller, Google, relayed via my Ahrefs hreflang guide.
- On correctness not guaranteeing outcome (May 2025, Bluesky): Mueller noted that hreflang doesn’t guarantee indexing, so a variant may simply not be indexed, and that same-language variants (like
fr-frandfr-be) are commonly consolidated. — relayed via Search Engine Journal coverage.
Which implementation method should I use?
Work top to bottom and stop at the first match.
1. Are the pages non-HTML files (PDFs, docs, images served directly)?
→ Yes → HTTP Link: headers. They have no <head>, so this is your only
option. Set the header at the server/CDN, listing the whole set on each file.
→ No → continue.
2. Do you have more than a handful of locales, an existing sitemap pipeline, or a
headless/JAMstack build?
→ Yes → XML sitemap <xhtml:link>. Generate it programmatically from your
translation table. One machine-generated file, no per-page markup, easiest to QA.
→ No → continue.
3. Small/simple site, few locales, and you’d rather keep everything visible on the
page?
→ HTML <link> tags in the <head>. Just make sure the block is generated from
one source of truth and lands inside a well-formed <head>.
Whatever you pick: use it alone. Google says there’s no benefit to combining methods, and every extra copy is a place for the three to disagree.
Do I need x-default?
Do you have a page that serves users who match none of your explicit locales — a
country/language selector, or a global homepage?
→ Yes → add x-default pointing at that fallback page.
→ No → you can skip it. It’s optional; Google falls back to its own detection. It
won’t break the cluster (unlike a missing reciprocal tag). Adding it is still good
practice — it was the single most-missed element in my study (56.3%).
Should I auto-redirect users by location?
Are you thinking about redirecting based on IP/geo? → Redirect human visitors only, and prefer a non-intrusive banner over a hard redirect. → Never redirect crawlers by geo. Googlebot mostly crawls from the US, so a crawler geo-redirect can de-index your other regional versions — and it risks cloaking classification and EU anti-geoblocking exposure. Let bots reach every URL variant directly; hreflang does the routing signal.
Which CMS path applies to me?
- WordPress? → Yoast alone does nothing. Install WPML or Polylang; they emit hreflang (choose head vs. sitemap output in WPML’s SEO settings).
- Shopify? → Markets auto-generates reciprocal tags — but often language-only.
Need
fr-FRvsfr-CA? Add manualtheme.liquidtags or an app, and don’t mix the two (conflict risk). - Custom / headless? → Emit
<xhtml:link>(or head tags) from your translation-relationship table at build/serve time.
SOP: Ship a hreflang cluster from scratch
A repeatable procedure for adding a new hreflang cluster (or a new locale to an existing one). Run it top to bottom.
1. Build the locale matrix (single source of truth).
List every URL and its language-region code in one place — a database table, a CMS
translation field, or a spreadsheet. Confirm every URL is the canonical, indexed
form (right protocol, www, trailing slash, case). This table is the input to
everything below; nothing gets hand-typed downstream.
2. Pick one method using the Decision Trees lens. Do not combine methods.
3. Generate the annotations from the matrix.
- HTML: emit the
<link>block into each page’s<head>from the matrix. - Sitemap: emit each
<url>block with thexmlns:xhtmlnamespace on<urlset>. - Headers: emit the
Link:header from the matrix at the server/CDN.
4. Verify the two rules programmatically before publishing. Every entry must (a) include a self-reference and (b) list every other entry in the cluster. Confirm reciprocity: for each A→B, there’s a matching B→A.
5. Add x-default if you have a selector or global fallback page.
6. Publish, then validate the rendered output (see the Playbooks lens for the
detailed validation runbook): rendered head (not view-source), a full-cluster crawl
for reciprocity, and a &hl=/&gl= SERP spot-check.
7. Wire it into ongoing checks. Add a recurring crawl / regression test so a future URL change or new locale can’t silently rot a return tag. Validation is not a one-time task.
When adding a locale later: you only edit the matrix (step 1) and re-run steps 3–6. If you’re hand-editing individual pages to add a locale, your source of truth is wrong — fix that first.
Playbook: “My hreflang isn’t working” — validation runbook
Run these in order. Each step either finds the break or clears a suspect.
Step 1 — Confirm the tags actually render. Open the page and view the rendered DOM (GSC URL Inspection → Test Live URL → View Tested Page, or a rendering crawler). Do not rely on Ctrl+U “View Page Source” — if hreflang is JS-injected it won’t appear there even though it’s live.
- Tags present in rendered head → go to Step 3.
- Tags missing from rendered head → Step 2.
Step 2 — Are the tags in the <head> or the <body>?
If the tags exist in source but sit inside <body>, they’re invalid. A malformed or
injected <p> tag or an iframe likely closed the <head> early. Use browser DOM
breakpoints to find where the <head> breaks, fix that markup, re-check.
Step 3 — Check the URLs are absolute and canonical.
Every href must be fully-qualified (https://…) and byte-identical to the indexed
form (protocol, www, trailing slash, case). Confirm none point at a 301, 404,
noindexed, or canonicalized-away URL.
Step 4 — Check reciprocity across the whole cluster. Crawl the entire cluster (Screaming Frog / Ahrefs Site Audit). For every A→B, verify a matching B→A exists, and that each page self-references. This is where most breaks live — a single missing return tag drops the pair.
Step 5 — Validate the codes.
Confirm ISO 639-1 language + ISO 3166-1 alpha-2 region. Look specifically for
en-UK (→ en-GB), jp (→ ja), three-letter codes, and region-only codes.
Step 6 — Rule out geo-redirects. Confirm crawlers aren’t being geo-redirected. If Googlebot (crawling from the US) gets bounced to your US version, your other regional URLs may never be reached.
Step 7 — Reset expectations if the cluster is technically correct.
If everything above checks out and same-language variants (e.g. fr-fr / fr-be)
are still consolidated in reporting, that’s expected — Google can consolidate
near-identical same-language variants regardless of implementation. It’s a hint, not
a directive; wrong or ignored hreflang is not a penalty. Stop chasing it as a bug.
Implementation anti-patterns
Concrete mistakes, why they’re wrong, and what to do instead.
1. Hand-maintaining hreflang per page. Why it’s wrong: the moment one template or locale drifts, return tags go missing and pairs get dropped — reciprocity rot at scale. Do instead: generate every annotation from one source of truth (translation table, CMS field, or the Sheets template for small sites), so the whole cluster is regenerated, not edited.
2. Using all three methods for “extra signal.” Why it’s wrong: Google says there’s no benefit, and three copies of the truth means three chances for them to disagree. Do instead: pick one method and use it alone.
3. Auto-redirecting crawlers by geo/IP. Why it’s wrong: Googlebot crawls mostly from the US, so a crawler geo-redirect can de-index your other regional clusters entirely — plus cloaking and EU anti-geoblocking exposure. Do instead: redirect (or better, banner-prompt) users only; never crawlers. Let bots reach every URL directly and let hreflang carry the signal.
4. Pointing hreflang at non-canonical, redirected, or noindexed URLs.
Why it’s wrong: the return link resolves to a 301/404/noindexed page, so the
pair breaks (16.9% referenced broken/redirected pages, 8.0% non-canonical in my
study).
Do instead: each variant canonicalizes to itself; hreflang points only at live,
canonical, indexable URLs — regenerated whenever URLs change.
5. Relative or protocol-relative URLs.
Why it’s wrong: Google requires fully-qualified URLs; /foo and //example.com/foo
are invalid, and even absolute-but-mismatched forms (wrong slash/www/case) fail the
reciprocity match.
Do instead: always https://example.com/foo, matching the exact indexed form.
6. Trusting “View Page Source” as validation.
Why it’s wrong: JS-injected hreflang doesn’t appear in raw source, so you either
think it’s missing (it isn’t) or miss that it renders in the <body> (invalid).
Do instead: validate the rendered DOM via GSC URL Inspection or a rendering
crawler.
7. Wrong or invented locale codes.
Why it’s wrong: en-UK, jp, ger, and region-only codes are invalid and
ignored.
Do instead: ISO 639-1 language + optional ISO 3166-1 alpha-2 region — en-GB,
ja, de, es-MX.
Before / after
1. Missing reciprocal tag (the classic). Before: the US page lists US + UK + DE, but the German page’s template only lists DE + US — it forgot the UK return link. Google drops the DE↔UK pair. After: the German page’s block lists DE + US + UK (all self-referencing and reciprocal). Regenerated from the locale table so it can’t drift again.
2. Relative URLs.
Before: <link rel="alternate" hreflang="de" href="/de/" /> — relative, so it’s
invalid and ignored.
After: <link rel="alternate" hreflang="de" href="https://example.com/de/" /> —
fully-qualified and matching the indexed form.
3. Wrong UK code.
Before: <link rel="alternate" hreflang="en-uk" href="https://example.com/uk/" />
— uk is Ukrainian; the annotation is invalid.
After: <link rel="alternate" hreflang="en-gb" href="https://example.com/uk/" />.
4. Sitemap missing the namespace.
Before: a sitemap using <xhtml:link> entries but with a <urlset> that only
declares the base sitemaps namespace — every <xhtml:link> is invalid.
After: <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> — the xhtml namespace is declared, so
the annotations validate.
Ready-to-copy AI prompts
Adapt the placeholders, then paste into your assistant of choice. Always validate the output against a real crawl — an LLM can generate plausible-but-wrong codes or miss a return link.
Generate an HTML <head> block from a locale matrix
I have these language/region page variants:
- en-US: https://example.com/us/
- en-GB: https://example.com/uk/
- de: https://example.com/de/
- global fallback / selector: https://example.com/
For EACH page above, output the complete hreflang <link> block that belongs in its
<head>. Every block must (a) self-reference, (b) list all other variants, and
(c) include an x-default pointing at the fallback. Use fully-qualified https URLs
exactly as given. Validate the codes as ISO 639-1 language + ISO 3166-1 alpha-2
region and flag any that look wrong.Convert the same matrix into an XML sitemap block
Using the same variant list, output an XML sitemap that uses <xhtml:link> hreflang
annotations. Requirements: declare xmlns:xhtml="http://www.w3.org/1999/xhtml" on
<urlset>; give every <url> block a full self-referencing + all-alternates set; use
the exact URLs provided. Do not add any URL not in my list.Audit a pasted cluster for reciprocity + code errors
Here are the hreflang tags from each page in my cluster: [paste each page's URL and
its hreflang tags]. Check for: missing self-reference, missing reciprocal (A->B
without B->A), invalid ISO codes, relative/protocol-relative URLs, and any URL that
appears with inconsistent formatting (trailing slash / www / case). List each issue
with the exact page and tag it's on. Do not assume tags I didn't paste. Extraction, console, and generation snippets
Practical one-liners for building and checking hreflang. Adjust URLs before running.
Chrome DevTools Console — list the hreflang tags on the current page Paste into the Console (F12 → Console) on any page to see what the browser actually rendered — this reflects JS-injected tags that “View Source” misses:
[...document.querySelectorAll('link[rel="alternate"][hreflang]')]
.map(l => ({ hreflang: l.hreflang, href: l.href,
inHead: !!l.closest('head') }));The inHead flag is the tell for the <head>-break bug: any tag showing
inHead: false is rendering in the <body> and is invalid.
Bookmarklet — same check, one click Save as a bookmark with this URL, then click it on any page:
javascript:(()=>{const t=[...document.querySelectorAll('link[rel="alternate"][hreflang]')].map(l=>`${l.hreflang} ${l.href} ${l.closest('head')?'(head)':'(BODY - INVALID)'}`);alert(t.length?t.join('\n'):'No hreflang tags found');})();XPath — select hreflang links inside the head (for a crawler / browser inspector)
//head/link[@rel='alternate' and @hreflang]If a crawler finds link[@hreflang] matches under //body instead, that’s the
head-break bug.
Regex — pull hreflang code + URL from raw HTML (quick grep, not a real parser)
<link[^>]*rel=["']alternate["'][^>]*hreflang=["']([^"']+)["'][^>]*href=["']([^"']+)["']Capture group 1 is the code, group 2 is the URL. Use only for a fast sanity grep — parse real HTML with a DOM library, not regex.
curl — check for hreflang in HTTP Link: response headers (the PDF/non-HTML case)
curl -sI https://example.com/file.pdf | grep -i '^link:'Python — generate a self-consistent hreflang <head> block from a matrix
variants = {
"en-us": "https://example.com/us/",
"en-gb": "https://example.com/uk/",
"de": "https://example.com/de/",
"x-default": "https://example.com/",
}
# Every page gets the SAME full block (self-reference + all alternates),
# which is exactly what satisfies reciprocity.
block = "\n".join(
f'<link rel="alternate" hreflang="{code}" href="{url}" />'
for code, url in variants.items()
)
print(block) Test yourself: Implementing hreflang
Five quick questions on building a hreflang cluster correctly. Pick an answer for each, then check.
Resources worth your time
My related writing
- Hreflang: The Easy Guide for Beginners — my Ahrefs guide with all three methods, the nine common implementation issues and fixes, and the Google Sheets template for semi-automated hreflang generation at scale.
- Over 67% of Domains Using Hreflang Have Issues — my study of 374,756 domains, the largest ever run, and the source of the error-rate breakdown (missing x-default 56.3%, missing self-reference 18.0%, broken/redirected targets 16.9%, missing reciprocal 15.3%, non-canonical 8.0%, bad codes 4.6%).
My speaking
- International SEO: The Weird Technical Parts — Pubcon Vegas 2019 — the richest implementation source: the
<head>-break bug (tags forced into<body>by iframes/malformed markup) and DOM-breakpoint debugging, why “sitemaps are faster” is a myth, the geo-redirect de-indexing risk, and the&hl=/&gl=SERP-check technique. - Hreflang Study and Interesting Issues — Brighton SEO 2023 — the deck behind the study, plus Google’s most-specific matching order (language+country → language → x-default) and the most common code mistakes across the dataset.
- You’re Going To Screw Up International SEO — Pubcon Vegas 2017 — the ecosystem of implementation chaos: tools reporting wrong info, content served from URLs that differ from what’s indexed, duplicate-page traps.
From around the industry
- Google’s Localized versions of your pages — the primary implementation doc; the exact syntax for all three methods, the reciprocity/self-reference rules, valid codes, and the absolute-URL requirement. Read it in full before you build.
- Google’s Managing multi-regional and multilingual sites — URL-structure options and the auto-redirect / cloaking warning behind “redirect users, not crawlers.”
- WPML — Using WordPress SEO (Yoast) with WPML — how WordPress actually outputs hreflang (Yoast alone doesn’t), and the head-vs-sitemap output setting.
- Shopify — Add hreflang tags in your theme — the manual
theme.liquid<link>pattern for when Shopify Markets’ language-only tags aren’t specific enough. - Screaming Frog — How To Audit & Test Hreflang — the crawl-based workflow for confirming reciprocity across a whole cluster rather than spot-checking one URL.
- Google Reminds That Hreflang Tags Are Hints, Not Directives — Search Engine Journal, May 2025, on Mueller’s same-language-consolidation clarification (why a technically perfect cluster can still be consolidated).
- r/TechSEO — the community for debugging broken hreflang clusters.
Prove the hreflang cluster actually shipped
Hreflang fails silently: the tags can be present, well-formed, and still ignored if the return leg is missing. “It’s in the code” is not the test — reciprocity across the whole cluster is. Run these after you deploy a language/region set.
Test 1 — Every pair returns the tag (reciprocity)
- Test to run — Crawl the whole cluster with returntag (it checks that every page A points to actually points back at A), or run Screaming Frog’s hreflang report across the set.
- Expected result — 0 non-reciprocal pairs and every URL self-references. Zero errors, not “a few.”
- Failure interpretation — A one-way pair (A → B but B doesn’t → A) means Google drops that pair — the most common real-world failure, and invisible if you only spot-check one URL’s view-source.
- Monitoring window — Immediate for the rendered tags; the checker reads what’s live now.
- Rollback trigger — Any non-reciprocal pair, or a tag pointing at a URL that 301s or 404s — fix the source of truth and redeploy before waiting on Google.
Test 2 — Google is processing it cleanly
- Test to run — Google retired the old International Targeting report in Search Console in September 2022, so don’t rely on it — check Page Indexing for each locale in the cluster, and confirm via URL Inspection on a sample of pages that the “Google-selected canonical” and indexing status line up with what you expect for that locale.
- Expected result — Each locale’s intended pages are indexed (not “Duplicate, Google chose different canonical” in a way that collapses distinct language versions into one), and URL Inspection shows the alternate you expect.
- Failure interpretation — Pages showing up as duplicates of a different locale’s canonical, or clustered/consolidated indexing, usually traces back to a broken return leg (re-check Test 1) rather than hreflang itself — hreflang is a hint, not a directive, so a technically perfect cluster can still be consolidated if Google judges the content near-duplicate.
- Monitoring window — 2–4 weeks — Search Console re-crawls and reports the cluster over time, not instantly.
- Rollback trigger — A locale’s pages consistently indexing under the wrong canonical, or the wrong regional URL surfacing for a query — re-audit reciprocity first; don’t assume the tags “aren’t working” when the return leg is the actual gap.
Build-time retrieval analysis plus live signals for this exact article. The automatic chunk report includes a deterministic readiness score and is ready without a model download.
Search Console
sampleGA4 traffic (28d)
sampleCloudflare traffic (7d)
sampledCrUX field data (28d, phone)
sampleGoogle NLP entities
localChangelog
Updated Jul 25, 2026.
Editorial summary and recorded change details.Summary
Aligned legacy tool display names with returntag and Scout Site Audit Free.
Change details
-
Updated the linked tool names to match their current public labels.
Full comparison unavailable — no prior snapshot was archived for this revision.
Updated Jul 18, 2026.
Editorial summary and recorded change details.Summary
Corrected the validation workflow's dependence on Search Console's retired International Targeting report and filled three verified gaps against Google's own hreflang documentation: the HTML attribute-mixing rule, the HTTP header's every-response full-set invariant, and sitemap xhtml:link order/URL-limit behavior.
Change details
-
Test 2 in the Validation Tests lens no longer points readers at the removed Legacy tools & reports → International Targeting report; it now uses Page Indexing plus URL Inspection to confirm each locale indexes under its own canonical.
-
Method 1 (HTML link tags) now states Google's rule against mixing hreflang with other alternate attributes like media on the same link element.
-
Method 2 (HTTP headers) now states the every-response full-set invariant explicitly.
-
Method 3 (XML sitemap) now notes that xhtml:link child order doesn't matter and those children don't count toward the sitemap's 50,000-URL limit.
Full comparison unavailable — no prior snapshot was archived for this revision.