Reading a page's <link rel="alternate" hreflang> tags
yourself is free and takes thirty seconds. It also cannot catch the two
failures that break hreflang most often: a region code that looks right
but was never assigned, and a reciprocal link that only fails to exist on
a page you did not open.
Checking by hand is fine for confirming a tag exists
and is roughly the right shape — you will spot a missing
href or an obviously mistyped attribute immediately. It
cannot validate a region code against the actual ISO 3166-1 list from
memory, and it cannot confirm reciprocity without opening every
alternate page named on the one you started with and checking each one
links back.
This tool validates every hreflang value's language
and region subtags against the real BCP 47 / ISO 3166-1 data, checks
that no two tags declare the same code for different URLs, flags a
missing self-reference or x-default, and — given the
alternate pages' content, either fetched by the CLI or pasted into the
browser page — confirms each alternate actually links back. It
runs free, with no account, and the browser page sends nothing to a
server.
| What checking by hand catches | What this tool catches too |
|---|---|
A tag with no href, or an obviously misspelled attribute. | Same, plus every hreflang value validated against the real language/region tables. |
| "en-uk" looks like a reasonable code for British English. | Flags it: "UK" was never assigned an ISO 3166-1 region code; the real one is "GB". |
| Whether the page you are reading lists itself among its own alternates — if you think to check. | Always checks self-reference, mechanically, on every run. |
| Whether every alternate page links back — only if you open all of them and compare by hand. | Confirms reciprocity for every alternate given its content, and reports "unverified" rather than guessing when content isn't available. |
| Two same-code tags pointing at different URLs, if you notice the duplicate on the same page. | Always flags conflicting or duplicate hreflang codes. |
Four languages, four tags, every one present and syntactically well-formed:
| hreflang | href | Looks right? |
|---|---|---|
en | /en/ | Yes |
en-uk | /en-uk/ | Yes, at a glance |
fr | /fr/ | Yes |
es | /es/ | Yes |
Running this exact set through this tool's own engine reports:
| Finding | Severity | Visible by reading the /en/ page alone? |
|---|---|---|
| "en-uk" is not a valid hreflang value — "UK" was never assigned an ISO 3166-1 region code (the real one is "GB"). | Danger | No — the tag is syntactically well-formed. |
| The /es/ page does not link back to /en/, so the annotation is not reciprocal. | Danger | No — requires opening /es/ and comparing its own tags. |
No x-default declared for 4 language variants. | Notice | Only if you happen to know to look for it. |
2 danger-level findings, 1 notice, from a set that reads as complete on the page you'd actually be looking at. Google's hreflang documentation states that a non-reciprocal annotation may be ignored entirely for both pages in the pair, so the /en/–/es/ relationship in this example gets no hreflang benefit at all despite four tags that all look fine. Figures taken directly from this tool's own buildReport(), not invented.
Neither approach is wrong for what it does. A quick read catches an obviously broken tag; it was never going to catch a plausible-looking invalid code or a link-back on a page you didn't open. This tool checks the parts that require comparing every value against a reference table and every alternate against every other alternate — the part manual review runs out of attention for first.
Because the two failures that matter most are invisible from reading one page's markup. An invalid region code like "en-uk" is syntactically well-formed and reads as correct — "UK" looks like the obvious abbreviation for the United Kingdom, but ISO 3166-1 never assigned it a country code; the real one is "GB". And reciprocity failures require opening every alternate page and checking that each one links back, which does not show up from reading the page you started on.
A human reviewer catches an obviously malformed tag — a missing href, a typo in the attribute name — because those look wrong on sight. What gets missed is exactly what looks right: a region code that reads like it should be valid, and a link-back on a page nobody opened. Reciprocity in particular requires checking N-1 other pages for every page in the set, which nobody does by hand past two or three languages.
In the worked example on this page, a 4-language hreflang set with every tag present and well-formed at a glance carries 2 danger-level findings once actually checked: an invalid "en-uk" code, and one alternate page that never links back, breaking reciprocity for that pair. Google's own documentation says a non-reciprocal annotation may be ignored entirely — meaning the page pair with the broken link-back gets no hreflang benefit at all, despite four tags that all looked fine.
For a two-page site where you personally maintain both pages and remember to update both at once, eyeballing them can work. It stops scaling the moment a third language, a redirect, or a second author enters the picture, because reciprocity is a claim about every other page in the set, not about the one you are looking at.
The Node CLI does, over real HTTP, which is why it can check reciprocity unattended in CI. The browser page cannot reliably fetch a different origin itself (cross-origin requests are usually blocked), so it checks reciprocity from HTML you paste in for each alternate — still mechanical, still exhaustive, just not automatic in the browser the way the CLI is.
Nothing here says or implies that checking hreflang tags by hand is a bad practice — it is the obvious first move, and it catches obviously broken tags immediately. This page describes the specific failures it structurally cannot catch, not a fault in anyone who does it.
Open hreflang Reciprocity Check and run it against your own pages.