Fixing Sitemap and HTML Hreflang Mismatches
Google accepts hreflang in the page head and in the XML sitemap, and many sites end up with both: a plugin writes link tags into the HTML while a sitemap generator adds xhtml:link entries. Each source is built separately, so sooner or later they disagree.
One lists a new locale the other has never heard of, or they use different addresses for the same page. The Hreflang Checker reports this as a sitemap mismatch. This guide shows how to compare the two sources properly, decide which one to keep, and produce a sitemap that is valid, complete and generated from the same data as the site.
Why two sources drift apart
Google treats the three declaration methods, HTML link tags, HTTP headers and sitemaps, as equivalent. Using more than one is allowed. The trouble is that they are rarely produced by the same code. A typical WordPress site might have WPML printing link tags and an SEO plugin building the sitemap; a custom site might have hreflang in a template and a nightly job writing the sitemap from the database. Each reads the site at a different time, from a different place, with different rules for building addresses.
When the two agree, the duplication is harmless but pointless. When they disagree, you are sending Google two versions of the same cluster, and there is no documented rule for which one wins. Google may combine them, prefer one, or distrust the relationship. You lose control of which page is shown where.
The kinds of mismatch
| Mismatch | Example | Usual cause |
|---|---|---|
| Different locale lists | HTML has en, fr, de; sitemap
has en, fr | New language added to the plugin but not to the sitemap job |
| Different addresses | HTML says /de/kontakt/; sitemap says /de/contact/ | Sitemap builds localised slugs from the source language |
| Different codes | HTML says en-GB; sitemap says en | Each tool maps languages to codes its own way |
| x-default only in one | HTML has x-default; sitemap has none | x-default added later to one source |
| Stale entries | Sitemap still lists a locale that was removed | Sitemap cached or generated from an old export |
| Different hosts or slashes | www. in one, no www. in the other | Separate base URL settings |
| Sitemap lists non-canonical pages | Parameter or paginated addresses in loc | Generator reads every URL in the database, not only canonical ones |
Comparing the two sources properly
Eyeballing a sitemap next to page source works for one page, not for a site. A short script can pull the cluster for every address from both sources and report the differences. This example reads one sitemap and fetches each page listed in it:
import re, requests, xml.etree.ElementTree as ET
NS = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9",
"x": "http://www.w3.org/1999/xhtml"}
tree = ET.fromstring(requests.get("https://example.com/sitemap-pages.xml").content)
for url in tree.findall("s:url", NS):
loc = url.find("s:loc", NS).text
in_map = {(l.get("hreflang").lower(), l.get("href"))
for l in url.findall("x:link", NS)}
page = requests.get(loc).text
in_html = {(c.lower(), h) for c, h in re.findall(
r'hreflang="([^"]+)"\s+href="([^"]+)"', page)}
if in_map != in_html:
print(loc)
print(" only in sitemap:", sorted(in_map - in_html))
print(" only in HTML: ", sorted(in_html - in_map))
The regular expression assumes hreflang comes before href in each tag; adjust
it if your templates write them the other way round. Run it on a sample first, and be gentle with request
rates on large sites.
Which source to keep
There is no ranking benefit to either method. Choose the one your team can keep correct with the least effort.
| Keep the HTML head if | Keep the sitemap if |
|---|---|
| The site has a handful of locales | The site has many locales, so head blocks get long |
| Your CMS or framework already outputs tags correctly | Pages are rendered by several systems that are hard to change |
| Editors and developers want to check a page with View Source | A central data team already builds sitemaps from a product or page database |
| Pages are added often and the sitemap lags | You also need hreflang for non-HTML files such as PDFs |
If you keep the HTML, the sitemap still lists your pages; it just drops the xhtml:link
entries. If you keep the sitemap, remove the link tags from the templates. Either way, the rule is one
source of hreflang, built from the same locale list the site uses for its menus and switcher.
A valid hreflang sitemap, line by line
If the sitemap is the source you keep, it has to be right in every detail. This example covers a contact
page in three languages plus a fallback. Every address in the cluster gets its own url entry,
and every entry repeats the full set of alternates, including itself:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://example.com/contact/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/contact/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/kontakt/"/>
<xhtml:link rel="alternate" hreflang="nl" href="https://example.com/nl/contact/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/contact/"/>
</url>
<url>
<loc>https://example.com/de/kontakt/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/contact/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/kontakt/"/>
<xhtml:link rel="alternate" hreflang="nl" href="https://example.com/nl/contact/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/contact/"/>
</url>
<!-- and a third url entry for /nl/contact/ with the same four links -->
</urlset>
- The namespace. Without
xmlns:xhtml="http://www.w3.org/1999/xhtml"on theurlsetelement, the link entries are not understood. - Full addresses. Every
hrefis absolute, with protocol and host, matching the page's canonical exactly. - Canonical pages only. Every
locshould be a page that is its own canonical, returns 200 and can be indexed. - Escaping. An ampersand in an address must be written as
&in XML, or the file will not parse. - Size limits. A sitemap can hold up to 50,000 addresses and 50 MB uncompressed. The alternate links count towards the file size, so a site with many locales reaches the size limit well before the address limit. Split into several files under a sitemap index.
Alternate pages can sit in different sitemap files, and even on different hosts if you have verified ownership of each in Search Console, but keeping each cluster together in one file makes it far easier to check.
Generating the sitemap from one locale map
The durable fix is to generate hreflang from the same data the site uses. Here is a minimal generator that takes a mapping of page keys to their address in each locale:
from xml.sax.saxutils import escape
PAGES = {
"contact": {"en": "https://example.com/contact/",
"de": "https://example.com/de/kontakt/",
"nl": "https://example.com/nl/contact/"},
}
DEFAULT = "en"
out = ['<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" '
'xmlns:xhtml="http://www.w3.org/1999/xhtml">']
for versions in PAGES.values():
links = [(code, url) for code, url in sorted(versions.items())]
links.append(("x-default", versions[DEFAULT]))
for url in versions.values():
out.append(" <url><loc>%s</loc>" % escape(url))
for code, href in links:
out.append(' <xhtml:link rel="alternate" hreflang="%s" href="%s"/>'
% (code, escape(href)))
out.append(" </url>")
out.append("</urlset>")
open("sitemap-pages.xml", "w", encoding="utf-8").write("\n".join(out))
In a real site, PAGES comes from your CMS, database or routing configuration, and pages
whose translation is not published are simply absent from their locale's entry. The same mapping should
feed the language switcher, so a page cannot appear in one and not the other.
For general sitemap problems unrelated to hreflang, such as entries that return errors, see the guide to sitemap mismatches and oversized sitemaps.
The fix, step by step
Step 1
Extract hreflang from both sources
Run
the Hreflang Checker to see which clusters it flags for sitemap inconsistency, then gather the full picture:
read every sitemap listed in robots.txt or your sitemap index, extract each loc with its
xhtml:link entries, and fetch the same pages to extract their head link tags. Normalise
language codes to lower case for comparison, but keep addresses exactly as written, because a slash or
host difference is part of the problem.
Step 2
Classify each difference
Sort every mismatch into a type: locale present in only one source, different address for the same locale, different code for the same page, x-default in only one source, stale entries for removed pages, or non-canonical addresses in the sitemap. Count each type. Most sites find that one or two types account for nearly all differences, and each type traces back to a specific setting, plugin or generation job that you can fix once.
Step 3
Choose the source you will maintain
Decide between the HTML head and the sitemap based on how many locales you have, which system already produces correct output, and who will maintain it. Pick the one that reads from the same locale and address data as your language switcher. Record the decision where developers will see it, such as the repository readme or the plugin settings notes, so that a future tool does not quietly add the other method back.
Step 4
Remove or regenerate the other source
First make the chosen source complete and correct for every cluster. Then remove hreflang from the other: switch off the sitemap generator's alternate links, or remove link tags from the templates or translation plugin. If you keep the sitemap, regenerate it from your locale map so it contains only canonical, indexable pages, with every alternate including itself and the x-default. Purge any sitemap cache or CDN copy afterwards.
Step 5
Validate the sitemap structure
Open
the sitemap in a browser to confirm it parses, and check that the urlset element declares
the xhtml namespace, that every address is absolute, that ampersands are escaped, and that no single
file exceeds 50,000 addresses or 50 MB uncompressed. Spot-check a few clusters: each address should appear
once as a loc, and every entry in the cluster should carry the identical set of alternate
links.
Step 6
Resubmit and recheck the clusters
Submit the sitemap or sitemap index in Search Console's Sitemaps report and confirm it is read without errors, and make sure robots.txt lists it. Run the Hreflang Checker across each locale again; the mismatch errors should be gone. Any remaining return tag, code or canonical errors were present in the source you kept, so fix those next. Repeat the comparison script after the next content release to confirm nothing drifts.
1. Is it wrong to have hreflang in both the sitemap and the HTML?
It is allowed, and Google treats the methods as equivalent. It only becomes a problem when the two disagree, which tends to happen because they are generated by different tools at different times. If they are produced from the same data and always match, having both does no harm, but it doubles what you have to check. Most sites are better off choosing one source and removing hreflang from the other, so there is only one thing to maintain.
2. Which does Google use if the sitemap and HTML disagree?
Google has not published a rule for which source wins when they conflict. It may combine them, prefer one, or treat the relationship as unreliable, and the result can differ from page to page. That uncertainty is the reason to fix the mismatch rather than wait: while two versions of the cluster exist, you cannot predict which version of your page will be shown in each market. Make them identical, or better, keep only one.
3. Do I need the xhtml namespace in my sitemap?
Yes. The xhtml:link entries only mean something to a sitemap reader when the urlset element declares the namespace with xmlns:xhtml set to http://www.w3.org/1999/xhtml. Without it the alternate links are not recognised, and in some parsers the file fails altogether. It is a common omission in hand-built sitemaps and in generators where hreflang support was added later. Check the opening urlset element of every sitemap file that carries alternates, including files produced by plugins.
4. Does every URL in a hreflang sitemap need all the alternates?
Yes. Each loc entry must list every version in its cluster, including itself and the x-default if you use one. The sitemap equivalent of a page's head block is the set of links under that page's entry, and the same rules apply: self-reference, reciprocity and valid codes. A sitemap that lists the alternates only under the English entry, and leaves the German and French entries bare, has missing return links for every translated page.
5. Will the Search Console Sitemaps report show my hreflang errors?
No. The Sitemaps report shows whether Google could fetch and read the file and how many pages it found, not whether the hreflang inside it is valid or consistent with your pages. Search Console's International Targeting report, which once listed hreflang errors, has been retired. To find mismatches between the sitemap and the HTML, or errors within the sitemap's clusters, you need a crawler that reads both, such as the Hreflang Checker, or your own comparison script.
6. Can alternates in the sitemap point to another domain?
Yes. A cluster can span country domains such as example.co.uk and example.de, and a sitemap on one domain can list alternates on another. For Google to accept a sitemap that includes addresses on other hosts, you need to have verified ownership of those hosts in Search Console. Many multi-domain sites avoid the question by giving each domain its own sitemap containing its own addresses as loc entries, each with the full set of cross-domain alternates.
7. Our sitemap is getting huge because of all the alternate links. What can we do?
Split it. A single sitemap file can hold up to 50,000 addresses and 50 MB uncompressed, and alternate links count towards the size, so a site with many locales reaches the size limit sooner. Break the sitemap into several files, for example one per section or per locale, and list them in a sitemap index. You can also gzip the files. Alternatively, if the size is unmanageable, moving hreflang to the HTML head is a valid option.
The Hreflang Checker follows each cluster of locale pages and validates return tags, language codes, x-default, sitemap consistency, target reachability and canonical alignment, so differences between your sitemap and page annotations are reported cluster by cluster.
Open Hreflang Fixes โ