First CH Apps / hreflang Return-Link Checker
Enter a URL and we extract its hreflang annotations (both <link rel="alternate"> and the HTTP Link header), then fetch each declared page to confirm it links back. Self-reference, x-default and the validity of every language/region code (en-UK, jp and friends) are checked in the same pass. Up to 10 destinations are fetched per run.
Grading canonical and title belongs to the Meta Tag & SEO Basics Check; counting redirect hops belongs to the Redirect Chain Tracer. This page answers one question only: do the language versions point at each other correctly?
Either language version works as the starting point — we check the cross-references from whichever page you enter.
Return links: hreflang has to be bidirectional. If A declares B but B never declares A, the annotation is ignored. We fetch each declared page and look for an annotation pointing back here.
URLs are compared exactly. http and https, with and without a trailing slash, with and without www are different URLs (when only those differ we report a mismatch rather than a pass).
Self-reference: every page must also declare its own hreflang. Without it the whole set can be dropped.
x-default: optional, but recommended — it names the page to show users who match none of your languages.
Language codes are validated against ISO 639-1 and regions against ISO 3166-1 alpha-2. en-UK (the UK is GB), jp (not a language) and en_US (hyphen, not underscore) are flagged for fixing; three-letter codes such as fil are flagged for review.
Up to 10 destinations are fetched per run (to stay inside the free-tier subrequest limit). Anything beyond that is labelled “not checked”.
Annotations declared in an XML sitemap (xhtml:link) are out of scope — we read the page's HTML and HTTP headers only.
Sent: only the URL you type. Our server (Cloudflare Workers) fetches it with a public GET, then fetches up to 10 of the hreflang destinations it finds the same way. No cookies and no credentials are sent.
Stored: nothing. The check lives entirely inside one request and writes neither the URL nor the result to any database (no KV, no D1). It leaves memory when the request ends.
Returned: the extracted hreflang values, the declared URLs and our verdicts — nothing else. We never re-serve a fetched page (this app cannot be used to browse other sites through us). Destination pages are read only as far as the first 64KB, and only to find their head.
Note: public GET only. URLs pointing at internal networks, localhost or private IPs are rejected. Annotations injected later by JavaScript are invisible to a server-side check.
POST /hreflang/api/check with JSON ({"url": "https://example.com/"}) or a form body and you get the same result back as JSON.