DEV Community

Peter Panda
Peter Panda

Posted on

A 40-Line Python Script to Check hreflang Reciprocity on a Small Site

Multilingual sites usually break in a boring way. Someone adds a new language page, links it from the English version, and forgets that the new page must link back. Search engines treat hreflang annotations as valid only when they are reciprocal, so a missing return link quietly cancels the signal. You can catch most of these errors with a short script that fetches your pages and compares the annotations. This post walks through a minimal version you can adapt.

What the script checks

For every URL in a list, it will:

  1. Fetch the page and read all link rel="alternate" hreflang tags in the head.
  2. Record each language and the URL it points to.
  3. Fetch each alternate and confirm that it points back to the original with the correct language code.
  4. Report missing return links, missing self-references and duplicate language codes.

It does not replace a full crawler, but it is fast to run and easy to understand.

The script

import re, sys, requests
from urllib.parse import urljoin

TAG = re.compile(r'<link[^>]+rel=["\']alternate["\'][^>]*>', re.I)

def annotations(url):
    html = requests.get(url, timeout=20, headers={"User-Agent": "hreflang-check"}).text
    out = {}
    for tag in TAG.findall(html):
        lang = re.search(r'hreflang=["\']([^"\']+)["\']', tag, re.I)
        href = re.search(r'href=["\']([^"\']+)["\']', tag, re.I)
        if lang and href:
            lang_code = lang.group(1).lower()
            if lang_code in out:
                print(f"DUPLICATE {lang_code} on {url}")
            out[lang_code] = urljoin(url, href.group(1))
    return out

def check(start):
    pages = {start: annotations(start)}
    if start not in pages[start].values():
        print(f"NO SELF-REFERENCE on {start}")
    for lang, alt in list(pages[start].items()):
        if alt == start:
            continue
        pages[alt] = annotations(alt)
        if start not in pages[alt].values():
            print(f"MISSING RETURN LINK: {alt} ({lang}) does not point back to {start}")

for u in sys.argv[1:]:
    check(u)
Enter fullscreen mode Exit fullscreen mode

Run it with one URL per language group, for example python check.py https://example.com/en/pricing/. The script reads the English page, follows each alternate and tells you which ones fail to link back.

Reading the output

  • MISSING RETURN LINK means page B is listed by page A, but page B does not list page A. Fix the template or plugin that outputs the tags for page B.
  • NO SELF-REFERENCE means a page does not list itself. Each page in a group should include its own language and URL.
  • DUPLICATE means the same language code is declared twice, which is usually a plugin conflict.

Practical extensions

You can feed the script from your sitemap, add a short delay between requests, and write results to a CSV. If your site declares hreflang in sitemaps or HTTP headers instead of the HTML head, extend the function to read those sources. Also check that every alternate returns a normal 200 status and is not redirected or blocked from crawling, since annotations pointing at redirected URLs are ignored in practice.

Process matters more than the script

Run the check whenever you publish a new language version, and put it on a monthly schedule for the whole site. Keep the language code format consistent, such as en, zh-hans or zh-hk, and decide once how regional variants are handled.

For the non-code side of the topic, including URL structures, translation workflow and what to fix first, see PandaWM's hreflang and multilingual SEO guide. A small script plus a monthly habit catches the problems that are otherwise discovered months later in a traffic report.

Published by PandaWM, an overseas marketing team based in Hong Kong.

Top comments (0)