/ Hreflang Fixes / Sitemap Mismatch

How to Fix Hreflang Sitemap vs HTML Mismatch

Hreflang can be declared in HTML <head> tags OR in sitemap XML. Both methods work. The problem starts when a site declares it in both AND the declarations disagree. Google sees one set of clusters in the HTML and another in the sitemap, and silently picks one or ignores both. The fix is to commit to a single source of truth, generate from that source, and remove the other declaration entirely.

1. The two methods

HTML head method

<head>
  <link rel="canonical" href="https://example.com/en/about" />
  <link rel="alternate" hreflang="en" href="https://example.com/en/about" />
  <link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />
  <link rel="alternate" hreflang="de" href="https://example.com/de/uber-uns" />
  <link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
</head>

Sitemap method

<?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/en/about</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/uber-uns"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
  <url>
    <loc>https://example.com/fr/a-propos</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/uber-uns"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
  <!-- ... -->
</urlset>

2. Which method to choose

Both are SEO-equivalent. Choose based on operational factors:

Use HTML head if:

Use sitemap if:

3. Audit current state

Step 1
Check HTML head
curl -s https://example.com/en/about | grep -E "hreflang|alternate"
# Lists every hreflang annotation in the HTML head
Step 2
Check sitemap
curl -s https://example.com/sitemap.xml | grep -E "xhtml:link"
# If empty, sitemap doesn't declare hreflang — you're using HTML head only
# If present, sitemap declares hreflang — check whether HTML head also does
Step 3
Compare clusters
For one representative page, extract the cluster from HTML head and from sitemap. Compare. If they differ in any URL, alt text, or hreflang value, you have a mismatch.

4. Commit to one source

Choose one. Remove the other. The choice doesn't affect SEO — operational simplicity matters more.

If picking HTML head as canonical

Remove xhtml:link entries from sitemap.xml. Sitemap stays valid as plain loc-only entries.

<!-- Sitemap reduced to bare loc entries -->
<url>
  <loc>https://example.com/en/about</loc>
  <lastmod>2024-01-15</lastmod>
</url>
<!-- No xhtml:link tags -->

If picking sitemap as canonical

Remove hreflang from HTML head. Keep self-canonical only.

<head>
  <link rel="canonical" href="https://example.com/en/about" />
  <!-- No hreflang here; sitemap handles it -->
</head>

5. Generate from a single config

Both methods should pull from the same config. Source of truth lives in code or database, not in two places that drift independently.

// locales-config.js (single source of truth)
export const clusters = {
  about: {
    'en': 'https://example.com/en/about',
    'fr': 'https://example.com/fr/a-propos',
    'de': 'https://example.com/de/uber-uns',
    'x-default': 'https://example.com/en/about'
  },
  contact: {
    'en': 'https://example.com/en/contact',
    'fr': 'https://example.com/fr/contact',
    // ...
  }
};

// If HTML head method:
// Each page template imports clusters[currentPage] and renders <link> tags

// If sitemap method:
// Build script imports all clusters and renders sitemap.xml with xhtml:link entries

6. Sitemap-method generation example

// scripts/generate-hreflang-sitemap.js
const { clusters } = require('./locales-config');
const fs = require('fs');

function buildSitemap(clusters) {
  let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
  xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n`;
  xml += `        xmlns:xhtml="http://www.w3.org/1999/xhtml">\n`;
  
  for (const [pageId, cluster] of Object.entries(clusters)) {
    for (const [lang, url] of Object.entries(cluster)) {
      if (lang === 'x-default') continue;
      xml += `  <url>\n`;
      xml += `    <loc>${url}</loc>\n`;
      
      // Each URL declares its full cluster as alternates
      for (const [altLang, altUrl] of Object.entries(cluster)) {
        xml += `    <xhtml:link rel="alternate" hreflang="${altLang}" href="${altUrl}"/>\n`;
      }
      
      xml += `  </url>\n`;
    }
  }
  
  xml += `</urlset>\n`;
  return xml;
}

fs.writeFileSync('./public/sitemap.xml', buildSitemap(clusters));
console.log('Sitemap regenerated');

7. Common mismatches and what causes them

Mismatch 1: Old plugin still adding HTML tags

Site migrated to sitemap method but old SEO plugin still emits HTML head tags. Disable the plugin's hreflang output or uninstall.

Mismatch 2: Different locale lists

HTML head includes en/fr/de/es. Sitemap only includes en/fr/de. Spanish either gets ignored or gets partial signals. Single source of truth prevents this.

Mismatch 3: Different URLs

HTML head uses /fr/about. Sitemap uses /fr/a-propos/. URL paths differ. Google treats them as different clusters or rejects both. Always source URLs from the same generator.

8. Verify after fix

Step 1
Confirm single source
Either HTML head OR sitemap declares hreflang — never both. Verify by curl + grep on representative pages and on sitemap.
Step 2
Re-run Hreflang Checker
Sitemap mismatch findings clear. Clusters validate against a single declared source.
💡 Sitemap approach scales better but HTML approach debugs easier. Pick based on your site size and team workflow, not because one is "more SEO-friendly". Google handles both equally. The mistake is running both at once and letting them drift.

🌍 Re-run the Hreflang Checker

Verify single source of truth for hreflang.

Run Hreflang Checker →
Related Guides: Hreflang Fixes  ·  Fix Return Tags  ·  Fix Sitemap Declaration  ·  Hreflang Guide
💬 Got a problem?