7 Hugo Sitemap Checks for Multilingual Websites

Check Hugo multilingual sitemaps, language URLs, translation links and lastmod dates. Use a CSV-to-XML script to find missing or incorrect alternates.

Editorial policy

A multilingual Hugo sitemap needs three checks before you rely on it: the root file must point to the right language sitemaps, those files must contain the intended public URLs, and any alternate links they emit must connect real translations. A successful build does not establish all three.

These seven Hugo sitemap checks follow the path from language configuration and content relationships to the XML served by your public website. They include localized-slug examples and a script that compares language sitemap entries with an independent translation inventory.

The examples follow Hugo 0.169.0's embedded templates and current official documentation, checked on November 8, 2026. They are documentation-based examples, not results from a tested production deployment. Check your installed version and any theme overrides before adapting them.

1. Identify the index and the templates that own it

Open the root XML file and read its root element. In Hugo's default multilingual output, sitemap.xml is an index containing child sitemap addresses. Each language sitemap contains a urlset of page URLs.

For a single-host project that puts both English and French in language subdirectories, the output normally includes:

Plain text
public/
├── sitemap.xml
├── en/sitemap.xml
└── fr/sitemap.xml

The official sitemap template reference distinguishes two template names:

Template override Responsibility
layouts/sitemapindex.xml Generate the multilingual sitemap index
layouts/sitemap.xml Generate each language's page entries

The template named sitemapindex.xml does not mean the default public index URL is /sitemapindex.xml. Read the actual generated file rather than diagnosing a missing sitemap from an assumed filename.

Inspect project and theme/module overrides, language-specific template variants, the configured sitemap filename and disableKinds. An old custom template can continue to own the output after you upgrade Hugo.

Follow every child <loc> in the index. Do not construct those addresses by adding language prefixes yourself: default-language-at-root configurations, custom filenames and multihost deployments need their own verified mapping.

2. Verify each language's origin, path and locale

Compare the effective production configuration with the absolute URLs in the XML. A preview baseURL, an unintended subdirectory or a missing language can affect an entire sitemap.

This illustrative hugo.toml fragment defines a single-host site with English and French under /en/ and /fr/. Merge it only where it matches your intended routing; preserve unrelated project settings.

TOML
baseURL = "https://example.com/"
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true

[languages.en]
contentDir = "content/en"
locale = "en-US"
label = "English"
weight = 1

[languages.fr]
contentDir = "content/fr"
locale = "fr-FR"
label = "Français"
weight = 2

The language key fr controls the site's language identity and, in this arrangement, its route prefix. The locale fr-FR describes the intended language/region value. They need not be identical. Hugo 0.169.0's embedded sitemap uses .Language.Locale for hreflang.

Check each locale against Google's supported language and optional region codes. A valid Hugo language identifier, including a private-use identifier, is not automatically a valid Google hreflang value. Choose regional targeting because the page serves that audience, rather than adding a country merely to match an example.

The current language configuration reference uses locale and label; languageCode and languageName were deprecated in 0.158.0. Check older theme examples accordingly.

For multihost sites, Hugo accepts a distinct baseURL for every language and publishes separate site roots. Verify each host's deployed sitemap and document addresses. Do not treat a build directory named en as proof that the public URL must contain /en/.

Changing defaultContentLanguageInSubdir on an established site changes URL structure. Keep your intended URLs stable while correcting the sitemap, unless a separately planned migration requires otherwise.

3. Reconcile translation identity before checking XML

Hugo must know which pages are translations before it can emit their relationships. A navigation label or matching tag on two articles does not establish that relationship.

Hugo can associate translations through matching filenames with language suffixes, or matching relative paths in per-language content directories. When filenames differ, an explicit translationKey links the pages. The multilingual content documentation explains these approaches.

Using the directory configuration above, these two files share one content identity despite their different filenames and slugs.

content/en/guides/deployment.md:

YAML
---
title: "Deployment guide"
translationKey: "deployment-guide"
slug: "deployment"
---
English deployment instructions.

content/fr/guides/deploiement.md:

YAML
---
title: "Guide de déploiement"
translationKey: "deployment-guide"
slug: "deploiement"
---
Instructions de déploiement en français.

With the illustrated routing and no additional permalink overrides, their public addresses are https://example.com/en/guides/deployment/ and https://example.com/fr/guides/deploiement/. Verify the actual .Permalink values in your project rather than translating a URL string yourself.

Keep an independent inventory containing the content key, locale and final URL. Include every eligible page, not just articles: homepages, useful sections and included taxonomy pages also need a deliberate inventory decision.

If a page has no French translation, do not invent one or substitute the French homepage as an equivalent. A language switcher destination is not necessarily a translation of the current page.

Check translated taxonomy terms separately. Two translated articles carrying equivalent tags do not by themselves prove that their term pages are linked translations. Give those pages their own content association when they are genuine equivalents.

4. Compare complete alternate clusters across child files

Hugo 0.169.0's embedded sitemap already loops through .AllTranslations for translated pages. That collection includes the current language, so the generated XML can include self-reference as well as the other versions. Check theme overrides before adding another implementation.

When you use sitemap hreflang, Google's localized-version rules call for absolute URLs, the page itself and the relevant alternate versions. Verify the same relationship from each member. A separate French sitemap does not, by itself, declare which English page is its equivalent.

Use one supported method consistently. Google supports HTML, HTTP headers and sitemaps; you do not need all three. If your site emits more than one, investigate conflicting destinations rather than assuming repetition improves targeting.

Save an independent inventory as expected-translations.csv. This sample covers the two guide pages above; replace it with the complete eligible inventory before auditing a real build. Give an untranslated page its own one-row group.

CSV
group,lang,url
deployment-guide,en-US,https://example.com/en/guides/deployment/
deployment-guide,fr-FR,https://example.com/fr/guides/deploiement/

Save this standard-library Python 3 script as check-hugo-alternates.py:

Python
import csv
import sys
from pathlib import Path
from urllib.parse import urlsplit
import xml.etree.ElementTree as ET

groups, expected = {}, {}
with open(sys.argv[1], encoding="utf-8", newline="") as stream:
    for row in csv.DictReader(stream):
        if not all(row.get(key) for key in ("group", "lang", "url")):
            raise SystemExit("Incomplete expected-inventory row.")
        key, lang, url = row["group"], row["lang"].lower(), row["url"]
        parsed = urlsplit(url)
        if parsed.scheme not in ("https", "http") or not parsed.netloc:
            raise SystemExit(f"Expected an absolute URL: {url}")
        cluster = groups.setdefault(key, {})
        if lang in cluster or url in expected:
            raise SystemExit("Duplicate expected locale or page URL.")
        cluster[lang] = url
        expected[url] = key

if not expected or len(sys.argv) < 3:
    raise SystemExit("Supply a nonempty inventory and all child XML files.")

ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9",
      "x": "http://www.w3.org/1999/xhtml"}
actual = {}
for filename in sys.argv[2:]:
    root = ET.fromstring(Path(filename).read_text(encoding="utf-8-sig"))
    if root.tag != "{" + ns["s"] + "}urlset":
        raise SystemExit(f"Supply a child URL sitemap, not an index: {filename}")
    for entry in root.findall("s:url", ns):
        url = entry.findtext("s:loc", default="", namespaces=ns).strip()
        if not url or url in actual:
            raise SystemExit("Missing or duplicate sitemap loc.")
        alternates = {}
        for link in entry.findall("x:link", ns):
            if link.get("rel") != "alternate":
                continue
            lang, target = link.get("hreflang", "").lower(), link.get("href", "")
            if lang == "x-default":
                continue
            if not lang or not target or lang in alternates:
                raise SystemExit(f"Invalid or repeated alternate label: {url}")
            alternates[lang] = target
        actual[url] = alternates

missing = sorted(set(expected) - set(actual))
unexpected = sorted(set(actual) - set(expected))
wrong = []
for url in set(expected) & set(actual):
    wanted = groups[expected[url]]
    allowed = [wanted] if len(wanted) > 1 else [{}, wanted]
    if actual[url] not in allowed:
        wrong.append(url)

for label, values in [
    ("Missing URLs", missing), ("Unexpected URLs", unexpected),
    ("Wrong alternate sets", sorted(wrong))
]:
    print(f"{label}: {len(values)}")
    for value in values[:10]:
        print(" ", value)
print(f"Checked {len(actual)} URLs in {len(sys.argv) - 2} child file(s).")
raise SystemExit(1 if missing or unexpected or wrong else 0)

Supply every child URL sitemap referenced by the index, not the index itself:

Shell
python check-hugo-alternates.py expected-translations.csv public/en/sitemap.xml public/fr/sitemap.xml

The script compares full URL sets and each page's expected language-to-URL mapping. It catches missing self-references, missing return relationships and wrong translated slugs when they differ from the inventory. Duplicate page URLs or repeated locale labels also fail.

This checker assumes every expected alternate also has a sitemap URL entry. If you intentionally omit a valid alternate target from the sitemap inventory, audit URL membership and alternate mappings separately or adapt the script. That arrangement is not inherently invalid.

For an untranslated page, it accepts no alternates or a self-only set. It deliberately ignores optional x-default entries; inspect their real fallback destination separately. It does not validate Google's language-code list, page responses, canonical tags or translation quality.

Keep the inventory independent of the XML being checked. A CSV generated by copying the sitemap would preserve the same missing translations.

5. Apply inclusion rules to URLs and alternates

Decide which published pages belong in search discovery, then compare that policy with both <loc> entries and alternate links. Check drafts, future publications, disabled languages, preview pages, redirects, noindex pages and canonicals pointing elsewhere.

Hugo's sitemap configuration supports a page-level exclusion in front matter:

YAML
sitemap:
  disable: true

This excludes that page's entry from the embedded sitemap. It does not add noindex, prevent crawling or remove the page from the website. Separately, disableKinds = ["sitemap"] disables sitemap generation.

There is an important difference in the 0.169.0 embedded template: the outer page loop filters Sitemap.Disable, while the alternate loop ranges through .AllTranslations without applying that filter again. A page omitted as a <loc> can therefore still appear as another page's alternate.

That is not automatically an error. A published, indexable translation can remain a valid alternate even when you deliberately leave it out of the sitemap inventory. Investigate the target's actual eligibility.

If your project explicitly uses sitemap.disable to mean “exclude from both sitemap entries and alternate relationships,” apply that policy consistently in your custom sitemap template. Inside each included page's <url>, replace the existing alternate block with:

GO-HTML-TEMPLATE
{{ $alternates := where .AllTranslations "Sitemap.Disable" "ne" true }}
{{ if gt (len $alternates) 1 }}
  {{ range $alternates }}
    <xhtml:link rel="alternate"
                hreflang="{{ .Language.Locale }}"
                href="{{ .Permalink }}" />
  {{ end }}
{{ end }}

This is a targeted replacement for an existing per-page loop, not a complete sitemap template. Preserve the namespace, parent filtering, URL and date serialization around it. Recheck every affected language file after the change.

If a translation is unpublished or intentionally non-indexable, remove it from the expected search cluster too. Do not hide the contradiction by changing only the audit CSV while the XML still points to the unsuitable page.

6. Validate dates, encoding and file limits

Inspect <lastmod> on an English page and its translation separately. A French article published later, or substantially revised independently, should not inherit a misleading timestamp merely because both files share a translation key.

Hugo's Lastmod documentation describes explicit front matter values and configurable date sources, including Git history and fallback values. Check the effective date configuration before assuming that the XML reflects editorial history.

For a page whose main content actually changed at the illustrated instant, front matter could contain:

YAML
lastmod: 2026-10-20T09:30:00Z

Replace that sample with the real significant-change date. A build time, copied source-language date or automated save does not establish when the rendered translation changed. If no reliable date is available, review the date-source configuration and omit an untrustworthy value rather than fabricating freshness.

Google's sitemap guidance says lastmod should reflect significant changes. Google ignores priority and changefreq, so tuning those fields cannot repair missing language URLs or inaccurate dates.

Parse the XML itself, especially after overriding templates. Check UTF-8 encoding, proper escaping of values such as ampersands, the sitemap namespace and the XHTML namespace for alternate links. A file extension or HTTP content type is not proof that its body is valid XML.

Each sitemap is limited to 50,000 URL entries and 50MB uncompressed. Alternate child links do not count toward that URL limit. Language separation alone does not ensure a large language sitemap stays within the limits; inspect the actual output and arrange a supported split when necessary.

7. Check the public release and discovery references

Compare the built XML with what the public site actually serves. For a known translation change, follow the content revision through the Hugo build, deployment output and any cache serving the sitemap.

Fetch the public root index, then every referenced language sitemap. Inspect a recently added translation, a changed localized slug and a removed version when those changes exist in the release. Confirm that both the <loc> entries and alternate targets reflect the intended state.

For representative page destinations, verify:

Public evidence What should agree
Final HTTP response The intended page loads without an unintended redirect or error
Canonical URL It identifies the intended language page
Indexing directives HTML and response headers do not contradict the page's inclusion policy
Alternate relationships Maintained HTML/header/XML annotations name the same real equivalents
Visible content The requested language and relevant update are actually present

An English canonical on every translated page can contradict a plan to publish independently indexable language versions. Diagnose the underlying canonical policy rather than rewriting sitemap URLs to conceal it.

Check the site's actual robots.txt sitemap declaration. On a multihost deployment, check each host's relevant files. Verify useful internal links to translated pages as well; XML is only one discovery path.

Keep a short evidence record containing the release identity, checked index and child URLs, expected translation group, observed alternates and any mismatch. If local XML is correct but the public response is stale, investigate deployment and caching before changing the translation key again.

Our XML sitemap guide covers broader inclusion and discovery diagnosis. The international SEO guide provides context for language and regional URL choices.

Repeat the affected checks after a publishing or template change. Agreement between the content inventory, generated XML and public pages establishes what you shipped; it does not guarantee that Google will crawl, index or rank every translation.

Related posts

Keep reading practical SEO guides from the Screpy blog.

View all posts