Build hreflang tags for every language version of a page as HTML, an HTTP header or sitemap XML, and catch invalid codes like en-UK before they ship.
Hreflang fails quietly. There is no error in Search Console when a country code is wrong, no warning when a row is duplicated; the annotation is simply ignored and visitors keep landing on the wrong language version. The Hreflang Tag Generator builds the markup for every locale you list and audits the set at the same time, so the mistakes surface here rather than six months into a rollout.
The single most common error is en-UK. It reads correctly to an English speaker and it is not a country code: the United Kingdom is GB in ISO 3166-1, and UK belongs to nobody. Worth knowing if you plan to check this yourself, the browser’s own Intl API will not catch it either, because CLDR carries UK as a deprecated alias of GB and happily reports “United Kingdom”. This tool checks the two ISO registries directly instead.
Output comes in the three forms Google accepts, and only valid rows reach it. Nothing is fetched and nothing is uploaded, which does leave one rule the tool cannot verify for you; it is named at the bottom of the page rather than glossed over.
Press Load Sample and a five-row set arrives, starting en-US|https://www.northharbor.example/us/. It is broken on purpose, in the two ways real sets usually are. The second row reads en-UK, and the audit answers with “Line 2: ‘UK’ is not a country code — the United Kingdom is ‘GB’ in ISO 3166-1. Write ‘en-GB’.” Separately, a warning points out that no x-default row exists, so a visitor whose language matches nothing in the set gets whichever version Google picks. Fix the country code, add an x-default, and the panel turns green. The detail people miss: the invalid row is excluded from the generated output rather than passed through, so copying the block before fixing it gives you four tags instead of five.
iw reports that Hebrew is now he, and in that Indonesian is id.GR and UA look like typos and are not: Greece and Ukraine. They pass. EU does not, because an hreflang region is a country and never a continent or a union.zh-Hans-CN and zh-Hant-TW are both accepted and kept distinct, since simplified and traditional readers need different pages even inside one language.EN-gb and the tool rewrites it en-GB and says so, because Google is case-insensitive here but the BCP 47 convention is not arbitrary.de-DE make Google discard the entire set, not just the duplicate, which is the failure mode most worth catching early. A relative path such as /fr/ is refused for the same reason: hreflang needs the absolute URL.<head>. This is the usual choice and the easiest to verify by viewing source.<head> to put it in, which in practice means PDFs.<url> block per page, each repeating the complete alternate set including a link to itself. That repetition looks redundant and is exactly what the specification requires.Line 2: "UK" is not a country code — the United Kingdom is "GB" in ISO 3166-1. Write "en-GB".
No x-default row. Google uses it for visitors whose language matches nothing in the set, and without one they land on whichever version it guesses.
en-UShttps://www.northharbor.example/us/en-UKhttps://www.northharbor.example/uk/fr-FRhttps://www.northharbor.example/fr/de-DEhttps://www.northharbor.example/de/ja-JPhttps://www.northharbor.example/jp/<link rel="alternate" hreflang="en-US" href="https://www.northharbor.example/us/" /> <link rel="alternate" hreflang="fr-FR" href="https://www.northharbor.example/fr/" /> <link rel="alternate" hreflang="de-DE" href="https://www.northharbor.example/de/" /> <link rel="alternate" hreflang="ja-JP" href="https://www.northharbor.example/jp/" />
1 invalid row was left out of this output. Fix the errors above and they will appear.
hreflang has to be reciprocal: every page in the set needs the same block, including a row pointing at itself. If the French page lists the German one but the German page does not list the French one back, Google drops the pair. That return link is the one thing this tool cannot check for you, because it would have to fetch the other pages, and nothing here leaves your browser.