Technical 12 min read

Multilingual Sitemaps: The Complete hreflang Implementation Guide

Multilingual Sitemaps: The Complete hreflang Implementation Guide

Running a multilingual website? Then you need to tell Google which language version to show to which users. That's where hreflang comes in.

The problem: Without hreflang, Google might:

  • Show Spanish content to English speakers
  • Show UK English to US visitors
  • Index multiple language versions as duplicate content
  • Confuse users with the wrong language variant

The solution: Implement hreflang annotations in your XML sitemap to tell Google exactly which language and region each page targets.

This guide will show you how to add hreflang to your sitemaps, avoid common mistakes, and ensure international visitors see content in their language.

What is hreflang?

hreflang is an HTML attribute (or XML element) that tells search engines:

  1. What language a page is written in
  2. What geographic region it targets
  3. Where to find alternate language versions

Format: hreflang="language-region"

Examples:

  • en-us = English for United States
  • en-gb = English for United Kingdom
  • es-mx = Spanish for Mexico
  • es-es = Spanish for Spain
  • fr = French (any region)
  • zh-cn = Chinese (Simplified) for China
  • zh-tw = Chinese (Traditional) for Taiwan

Why Use hreflang in Sitemaps?

You can implement hreflang in three ways:

  1. HTML <link> tags in page headers
  2. HTTP headers for non-HTML files
  3. XML sitemaps (recommended for large sites - see fundamentals)

Advantages of sitemap implementation:

  • Easier to manage for large sites
  • Centralized in one place
  • No need to edit every page
  • Works for all content types
  • Easier to audit and debug

When to use sitemaps:

  • You have 100+ multilingual pages
  • You frequently add new languages
  • You want centralized management
  • You have a complex language structure

When to use HTML tags:

  • Small site (under 100 pages)
  • Simple language structure
  • You want page-level control

hreflang Syntax in XML Sitemaps

Basic Structure

<?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/page</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/page" />
    <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
    <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/page" />
  </url>
</urlset>

Key components:

  • xmlns:xhtml namespace declaration
  • <xhtml:link> for each language version
  • rel="alternate" attribute
  • hreflang attribute with language code
  • href with the URL for that language

Every language version must link to ALL other versions, including itself.

Wrong (missing self-reference):

<url>
  <loc>https://example.com/page</loc>
  <!-- Missing self-reference! -->
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
  <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/page" />
</url>

Right (includes self-reference):

<url>
  <loc>https://example.com/page</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/page" />
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
  <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/page" />
</url>

Complete Example: Three Languages

<?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">

  <!-- English version -->
  <url>
    <loc>https://example.com/about</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/about" />
    <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/acerca-de" />
    <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />
  </url>

  <!-- Spanish version -->
  <url>
    <loc>https://example.com/es/acerca-de</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/about" />
    <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/acerca-de" />
    <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />
  </url>

  <!-- French version -->
  <url>
    <loc>https://example.com/fr/a-propos</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/about" />
    <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/acerca-de" />
    <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />
  </url>

</urlset>

Notice: Each URL entry has identical <xhtml:link> elements. This is required!

Language and Region Codes

Language-Only Codes

Use when targeting a language regardless of region:

Code Language
en English (any region)
es Spanish (any region)
fr French (any region)
de German (any region)
it Italian (any region)
pt Portuguese (any region)
ja Japanese
ko Korean
zh Chinese
ar Arabic

Language-Region Codes

Use when targeting specific regions:

Code Language & Region
en-us English - United States
en-gb English - United Kingdom
en-ca English - Canada
en-au English - Australia
es-es Spanish - Spain
es-mx Spanish - Mexico
es-ar Spanish - Argentina
pt-br Portuguese - Brazil
pt-pt Portuguese - Portugal
fr-fr French - France
fr-ca French - Canada
de-de German - Germany
de-at German - Austria
de-ch German - Switzerland
zh-cn Chinese (Simplified) - China
zh-tw Chinese (Traditional) - Taiwan

Format: Always lowercase, language first, then region (ISO 639-1 + ISO 3166-1).

The x-default Tag

Use x-default to specify a fallback page for users whose language isn't available:

<url>
  <loc>https://example.com/</loc>
  <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/" />
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/" />
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/" />
  <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/" />
</url>

When to use x-default:

  • You have a language selector page
  • You want to show English to unmatched users
  • You have a global homepage

Common pattern: Set x-default to your primary language (usually English).

How to Generate Multilingual Sitemaps

(For general sitemap creation, see our complete guide)

Method 1: Manual Creation (Small Sites)

For sites with few pages and languages:

<?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">

  <!-- Homepage -->
  <url>
    <loc>https://example.com/</loc>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/" />
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/" />
    <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/" />
  </url>

  <url>
    <loc>https://example.com/es/</loc>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/" />
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/" />
    <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/" />
  </url>

  <!-- Repeat for each page -->

</urlset>

Method 2: WordPress Plugins

WPML (WordPress Multilingual):

  • Automatically generates hreflang sitemaps
  • Integrates with Yoast SEO
  • Handles all language relationships

Polylang:

  • Works with Yoast SEO for sitemap generation
  • Automatically adds hreflang annotations
  • Supports unlimited languages

TranslatePress:

  • Generates hreflang automatically
  • Works with existing sitemap plugins
  • No manual configuration needed

Method 3: Python Script

For custom sites:

def generate_multilingual_sitemap(pages, languages):
    """
    Generate sitemap with hreflang annotations

    pages: dict mapping page slugs to language URLs
    languages: list of language codes
    """
    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 page_slug, urls in pages.items():
        # Create entry for each language version
        for lang in languages:
            if lang in urls:
                xml += '  <url>\n'
                xml += f'    <loc>{urls[lang]}</loc>\n'

                # Add x-default (use first language as default)
                default_lang = languages[0]
                xml += f'    <xhtml:link rel="alternate" hreflang="x-default" href="{urls[default_lang]}" />\n'

                # Add all language versions
                for alt_lang in languages:
                    if alt_lang in urls:
                        xml += f'    <xhtml:link rel="alternate" hreflang="{alt_lang}" href="{urls[alt_lang]}" />\n'

                xml += '  </url>\n'

    xml += '</urlset>'
    return xml

# Usage
pages = {
    'homepage': {
        'en': 'https://example.com/',
        'es': 'https://example.com/es/',
        'fr': 'https://example.com/fr/',
    },
    'about': {
        'en': 'https://example.com/about',
        'es': 'https://example.com/es/acerca-de',
        'fr': 'https://example.com/fr/a-propos',
    },
    'contact': {
        'en': 'https://example.com/contact',
        'es': 'https://example.com/es/contacto',
        'fr': 'https://example.com/fr/contact',
    },
}

languages = ['en', 'es', 'fr']
sitemap = generate_multilingual_sitemap(pages, languages)
print(sitemap)

For database-driven sites:

import sqlite3

def generate_sitemap_from_db():
    """Generate multilingual sitemap from database"""
    conn = sqlite3.connect('website.db')
    cursor = conn.cursor()

    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'

    # Get all pages with their translations
    cursor.execute('''
        SELECT p.slug, p.lang, p.url
        FROM pages p
        WHERE p.published = 1
        ORDER BY p.slug, p.lang
    ''')

    # Group by slug
    pages = {}
    for slug, lang, url in cursor.fetchall():
        if slug not in pages:
            pages[slug] = {}
        pages[slug][lang] = url

    # Generate sitemap
    for slug, translations in pages.items():
        for lang, url in translations.items():
            xml += '  <url>\n'
            xml += f'    <loc>{url}</loc>\n'

            # Add x-default (English)
            if 'en' in translations:
                xml += f'    <xhtml:link rel="alternate" hreflang="x-default" href="{translations["en"]}" />\n'

            # Add all translations
            for alt_lang, alt_url in translations.items():
                xml += f'    <xhtml:link rel="alternate" hreflang="{alt_lang}" href="{alt_url}" />\n'

            xml += '  </url>\n'

    xml += '</urlset>'

    conn.close()
    return xml

Common hreflang Mistakes

Mistake #1: Missing Self-Reference

Wrong:

<url>
  <loc>https://example.com/page</loc>
  <!-- Missing hreflang="en" for itself! -->
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
</url>

Right:

<url>
  <loc>https://example.com/page</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/page" />
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
</url>

Wrong (English page links to Spanish, but Spanish doesn't link back):

<!-- English page -->
<url>
  <loc>https://example.com/page</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/page" />
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
</url>

<!-- Spanish page - MISSING English link! -->
<url>
  <loc>https://example.com/es/page</loc>
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
</url>

Right (both pages have identical hreflang annotations):

<!-- English page -->
<url>
  <loc>https://example.com/page</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/page" />
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
</url>

<!-- Spanish page -->
<url>
  <loc>https://example.com/es/page</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/page" />
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />
</url>

Mistake #3: Wrong Language Codes

Wrong:

<xhtml:link rel="alternate" hreflang="EN-US" href="..." />   Uppercase
<xhtml:link rel="alternate" hreflang="en_us" href="..." />   Underscore
<xhtml:link rel="alternate" hreflang="english" href="..." />   Full word

Right:

<xhtml:link rel="alternate" hreflang="en-us" href="..." />   Lowercase, hyphen

Mistake #4: Pointing to Different Content

hreflang should link to equivalent content in different languages, not different pages.

Wrong:

<!-- English: Product page -->
<url>
  <loc>https://example.com/products/shoes</loc>
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/blog" />   Different content!
</url>

Right:

<!-- English: Product page -->
<url>
  <loc>https://example.com/products/shoes</loc>
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/productos/zapatos" />   Same content, different language
</url>

Mistake #5: Missing Namespace

Wrong:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <!-- Missing xmlns:xhtml! -->
  <xhtml:link rel="alternate" hreflang="es" href="..." />
</urlset>

Right:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:link rel="alternate" hreflang="es" href="..." />
</urlset>

Testing and Validation

Google Search Console

  1. Submit your sitemap
  2. Wait 24-48 hours
  3. Go to International TargetingLanguage
  4. Check for hreflang errors

Common errors:

  • "No return tags" (missing reciprocal links)
  • "Incorrect hreflang value" (wrong language code)
  • "Missing self-reference"

hreflang Testing Tools

Recommended tools:

Manual Testing

Check your sitemap:

curl https://example.com/sitemap.xml | grep hreflang

Verify reciprocal links:

  1. Find a URL in your sitemap
  2. Check all its hreflang links
  3. Visit each linked URL
  4. Verify it has the same hreflang annotations

Best Practices

1. Keep It Simple

Start with language-only codes if you don't need regional targeting:

<xhtml:link rel="alternate" hreflang="en" href="..." />
<xhtml:link rel="alternate" hreflang="es" href="..." />

Add regions only when necessary:

<xhtml:link rel="alternate" hreflang="en-us" href="..." />
<xhtml:link rel="alternate" hreflang="en-gb" href="..." />

2. Use Absolute URLs

Always:

<xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/page" />

Never:

<xhtml:link rel="alternate" hreflang="es" href="/es/page" />

3. Include x-default

Specify a default for unmatched languages:

<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/" />

4. Automate Generation

Don't manually maintain hreflang annotations. Use:

  • CMS plugins (WordPress, Drupal)
  • Build scripts (for static sites)
  • Database queries (for custom sites)

5. Monitor Regularly

Monthly checklist:

  • [ ] Check Search Console for hreflang errors
  • [ ] Verify new pages have hreflang annotations
  • [ ] Test a sample of URLs for reciprocal links
  • [ ] Audit for broken language URLs

Next Steps

Now that you understand hreflang in sitemaps:

  1. Audit your current setup - Do you have hreflang implemented?
  2. Plan your language structure - Which languages and regions do you target?
  3. Generate your sitemap - Add hreflang annotations
  4. Test thoroughly - Use validation tools
  5. Submit to Search Console - Monitor for errors
  6. Learn about organization - Read our sitemap index guide for managing multiple languages

Key Takeaways

  • hreflang tells Google which language version to show users
  • Implement in sitemaps for easier management on large sites
  • Every language version must link to ALL others, including itself
  • Use lowercase language codes (en-us, not EN-US)
  • Include x-default for a fallback language
  • Test with Google Search Console and validation tools

Bottom line: Proper hreflang implementation prevents duplicate content issues and ensures international visitors see content in their language.

Ready to audit your multilingual sitemap? Visualize your sitemap structure to see all your hreflang relationships at a glance.

Ready to audit your sitemap?

Visualize your site structure, spot errors, and improve your SEO with our free tool.

Launch Sitemap Explorer