Skip to content

About

Validate hreflang for multilingual sites: language codes, return links, x-default, canonical conflicts and html lang, from HTML, headers and sitemaps.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

hreflang-check

Validate the hreflang annotations of a multilingual site: language and region codes, return links, canonicals, redirects, <html lang>, and whether the HTML, the Link: headers and the sitemap agree with each other.

python -m hreflang_check https://example.com/en/ https://example.com/fr/
hreflang-check https://example.com/en/ --crawl --max-pages 20 --format json --output report.json

Why another checker:

  • Three sources, compared. hreflang can live in <link rel="alternate"> in the <head>, in HTTP Link: headers, or in sitemap xhtml:link entries (sitemap index and .gz included). All three are read and cross-checked, because a conflict between them is a real bug that single-source tools miss.
  • Checks the targets, not only the page. Every hreflang target is fetched: it must answer 200 without redirecting, and it must link back.
  • Polite by design. Same host only, robots.txt honoured with RFC 9309 matching (longest match wins, * and $; not urllib.robotparser, which applies the first match), at least 1 s between requests, timeouts, GET/HEAD only, hard cap of 30 pages, and it never submits a form.
  • No dependencies. Standard library only, Python 3.10+. The ISO 639-1 and ISO 3166-1 code lists are bundled.

Checks

Check Severity What it looks for
language-code error / warning Invalid ISO 639-1 language or ISO 3166-1 region, case-insensitive, x-default accepted. Explains common mistakes: en-UK (use en-GB), en_US (underscore), eng (ISO 639-2), region-only codes such as us, jp for ja, deprecated iw
return-link error If A lists B, B must list A
self-reference warning The page lists itself
x-default warning No x-default in the set
target-status error / info Target does not return 200, or redirects (the chain is printed). Targets blocked by robots.txt or on another host are reported as info and not fetched
canonical error / warning rel=canonical points elsewhere, so the page's hreflang is ignored (error); a target declares a different canonical (warning)
duplicate-language error / info Same code pointing to different URLs in one set
source-conflict error / warning HTML, header and sitemap disagree (different URL for a code: error; different language sets: warning)
html-lang warning <html lang> missing or its language differs from the hreflang the cluster assigns to that page (en-CA vs en-GB is fine)
relative-url warning hreflang URL is not absolute
head-placement warning hreflang <link> inside <body> (search engines ignore it)
no-annotations, redirect info Page has no hreflang at all; the requested URL redirected

Usage

python -m hreflang_check URL [URL ...] [--crawl] [--max-pages N] [--sitemap URL] [--no-sitemap]
                         [--format markdown|json] [--output FILE] [--fail-on error|warning]
                         [--delay SECONDS] [--timeout SECONDS] [-v]
  • Single or several URLs: each page plus its hreflang targets is checked.
  • --crawl: also follows same-host links. --max-pages defaults to 20 and is capped at 30 for the whole run (pages fetched, targets included).
  • Sitemaps: found through the Sitemap: lines of robots.txt, else /sitemap.xml, or pass --sitemap URL (up to 5 files are read).
  • --delay: seconds between requests, never below 1. robots.txt Crawl-delay is honoured (up to 10 s).
  • Exit codes: 0 no errors, 1 errors found (--fail-on warning also fails on warnings), 2 usage error or an input URL that could not be fetched (network failure, blocked by robots.txt).

As a library:

from hreflang_check import check
report = check(["https://example.com/en/"], delay=1.0)
report.counts()          # {'error': 0, 'warning': 1, 'info': 0}
report.findings[0]       # Finding(severity, check, page, message, target)

examples/sample-local-report.json shows the JSON format (from a local test site); examples/*.md are the reports below.

Measured result

Run on 2026-09-29 against my own sites: a DeMark Studio local page that exists in English and French, and my portfolio (English + /es/). GET only, at least 1 s apart, 8 requests per run.

$ python -m hreflang_check https://demarkstudio.ca/en/web-design/miramichi https://marvin.demarkstudio.ca/
# hreflang-check report

- Pages checked: 4 on demarkstudio.ca, marvin.demarkstudio.ca
- HTTP requests made: 8 (including robots.txt and sitemaps)
- Result: 0 error(s), 2 warning(s), 0 info
- Sitemaps read: https://demarkstudio.ca/sitemap.xml, https://marvin.demarkstudio.ca/sitemap.xml

## Pages

| Page | HTTP | html lang | hreflang entries (html / header / sitemap) |
|---|---|---|---|
| https://demarkstudio.ca/en/web-design/miramichi | 200 | en | 3 / 0 / 3 |
| https://marvin.demarkstudio.ca/ | 200 | en | 3 / 0 / 2 |
| https://demarkstudio.ca/fr/conception-web/miramichi | 200 | fr | 3 / 0 / 3 |
| https://marvin.demarkstudio.ca/es/ | 200 | es | 3 / 0 / 2 |

## Findings

### https://marvin.demarkstudio.ca/

- **WARNING** `source-conflict`: HTML <link> and sitemap define different language sets (only in HTML <link>: x-default)

### https://marvin.demarkstudio.ca/es/

- **WARNING** `source-conflict`: HTML <link> and sitemap define different language sets (only in HTML <link>: x-default)

## Notes

- https://demarkstudio.ca: robots.txt read
- https://marvin.demarkstudio.ca: robots.txt read

The two warnings were real: the portfolio's HTML declared x-default, but its sitemap did not, so the two sources disagreed. I added the x-default alternate to the sitemap generator, redeployed, and ran the same command again:

# hreflang-check report

- Pages checked: 4 on demarkstudio.ca, marvin.demarkstudio.ca
- HTTP requests made: 8 (including robots.txt and sitemaps)
- Result: 0 error(s), 0 warning(s), 0 info
- Sitemaps read: https://demarkstudio.ca/sitemap.xml, https://marvin.demarkstudio.ca/sitemap.xml

## Pages

| Page | HTTP | html lang | hreflang entries (html / header / sitemap) |
|---|---|---|---|
| https://demarkstudio.ca/en/web-design/miramichi | 200 | en | 3 / 0 / 3 |
| https://marvin.demarkstudio.ca/ | 200 | en | 3 / 0 / 3 |
| https://demarkstudio.ca/fr/conception-web/miramichi | 200 | fr | 3 / 0 / 3 |
| https://marvin.demarkstudio.ca/es/ | 200 | es | 3 / 0 / 3 |

## Findings

No issues found.

## Notes

- https://demarkstudio.ca: robots.txt read
- https://marvin.demarkstudio.ca: robots.txt read

The DeMark pair was clean both times: en, fr and x-default in the HTML and the sitemap, reciprocal, self-referencing, <html lang> matching, canonical pointing to itself. (https://demarkstudio.ca/fr/ itself 301-redirects to /en/; the tool reports that as info when you pass it, since only the local pages have French versions.)

$ python -m pytest -q -p no:cacheprovider --import-mode=importlib hreflang-check
66 passed in 22.83s

Limitations

  • Only same-host targets are fetched. Alternates on another domain (example.fr) are reported as info and their return links cannot be checked.
  • Return links are checked against pages that were fetched in the same run; targets of targets are not followed (use --crawl for that, within the 30-page cap).
  • The page is read as served to a plain HTTP client: hreflang injected by JavaScript is not seen, and no Accept-Language or cookies are sent.
  • Script subtags (zh-Hans) are accepted; numeric regions (es-419) are flagged because hreflang wants ISO 3166-1 alpha-2.
  • Unlike search engines, a missing self-reference is only a warning, since return links can still make the set work.
  • --crawl is a small link follower for checking, not a general crawler: it skips images, scripts and other non-page files.

Author

Marvin Palencia, founder of DeMark Studio, Miramichi, New Brunswick, Canada. Portfolio: marvin.demarkstudio.ca

MIT License.

About

Validate hreflang for multilingual sites: language codes, return links, x-default, canonical conflicts and html lang, from HTML, headers and sitemaps.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages