The cluster is the unit, not the line, in hreflang generation
3 conditions have to hold before any of this counts:
- Every page in the set references every other page and itself. The self-reference is not optional and it is the single most common omission.
- Every
hrefis an absolute URL, protocol included./fr/is ignored.//example.com/fr/is ignored. - The block is identical on every URL in the set. Same order, same codes, same URLs.
That last point is why the generator emits one block rather than a per-page variant. You paste the same output on all of them. If a page in the set is missing the block, that page drops out, and so does every pairing that pointed at it.
What goes in the locale list for generated hreflang tags
The locales field takes one pair per line. Left of the pipe is the hreflang value, right of it the absolute URL:
en-us | https://example.com/
en-gb | https://example.com/uk/
fr-fr | https://example.com/fr/
de | https://example.com/de/
Language comes first: ISO 639-1, 2 letters, lowercase by convention. The region is optional and is ISO 3166-1 alpha-2, uppercase by convention. The region narrows the language, it never replaces it. de-CH means German as written for Switzerland. There is no valid hreflang value that is a country on its own.
A language-only code is often the better choice. es | https://example.com/es/ serves every Spanish speaker anywhere. Reach for es-MX and es-ES only when the pages genuinely differ by market, in currency, shipping or legal copy. Splitting one page into 2 country variants that say the same thing gains nothing and doubles the surface area for mistakes.
Blank lines and lines starting with # are skipped. Pipes are the documented separator, but a tab, a comma or plain whitespace also parse, so a paste straight out of a spreadsheet works.
Why en-uk quietly voids a line
The ISO 3166-1 alpha-2 code for the United Kingdom is GB. en-uk is not a near miss that Google rounds off. It is an invalid region, and the annotation is discarded. The audit panel catches that one plus the rest of the family:
- Country code where a language belongs.
jpis not Japanese,jais.cnis not Chinese,zhis. Same forkr(useko) anddk(useda). - Deprecated codes.
iwwas Hebrew decades ago. It ishenow. Old CMS templates still ship the old value. - UN M.49 regions.
es-419is a real code for Latin America and Google does not read it. Split it into the countries you actually sell to. - EU as a region. The EU is not a country. Use per-country codes, or drop to a language-only code.
- Underscores.
en_UScomes from application locale files. hreflang wants a hyphen. - Codes that are valid but mean something else.
ukis Ukrainian, not the United Kingdom.seis Northern Sami, not Sweden, which issv. Both pass a syntax check and both send your annotation somewhere you did not intend, so they surface as a soft warning rather than an error.
Warnings never block the output. You still get the tags, with a list of what to fix first.
Choosing an x-default worth having
The x-default field is the fallback for every visitor whose language and country you do not cover. A Portuguese speaker in Angola hitting a set of English, French and German pages has to land somewhere, and x-default is where you decide that.
2 sensible values: a language selector page, or your primary-market page. Point it at the URL a stranger should see. It is optional in the spec and recommended on every set, and the generator flags its absence because leaving it out means the choice gets made for you.
3 places hreflang output can live
The output format field switches the artifact, not the content.
<link> tags are the default and the right answer for ordinary HTML pages. They go in the <head> of every URL in the set.
A Link: response header is one comma-separated header carrying the same information. This is the only option for files with no <head>, which in practice means PDFs. Apache sends it with Header add Link "…", nginx with add_header Link "…";.
An XML sitemap with <xhtml:link> children annotates the entire set from one file, which is useful when your templating layer cannot vary the head per page. One trap: the urlset element has to declare the xhtml namespace or the file is invalid XML and gets rejected whole.
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://example.com/uk/</loc>
<xhtml:link rel="alternate" hreflang="en-US" href="https://example.com/" />
<xhtml:link rel="alternate" hreflang="en-GB" href="https://example.com/uk/" />
<xhtml:link rel="alternate" hreflang="fr-FR" href="https://example.com/fr/" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/" />
</url>
</urlset>
Pick one method. Annotating the same page in the head and in a sitemap gives Google 2 sources that will eventually disagree, usually right after someone edits only one of them.
Checks that catch the failures worth catching
Fetch the raw HTML, not the DevTools DOM. curl -s https://example.com/uk/ | grep hreflang tells you what a crawler received. The DOM inspector shows you what JavaScript built afterward, which is a different question.
Then confirm the reciprocity by hand on one pair. Load the French page, find the tag pointing at the English page, load the English page, find the tag pointing back. If both exist, the templating is right and the rest of the set almost certainly follows.
2 more things worth verifying: the annotated URLs are the canonical URLs (a page canonicalized somewhere else exports its hreflang to nowhere), and each locale has its own URL. 2 locales sharing one URL collapses the cluster to a single page.