x-default as fallback for users whose language isn't availableHreflang tags tell search engines which language version of your page to show users. Without them, Google might show your French content to English speakers or rank the wrong regional version.
<head>
<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en" />
</head>
Google recommends hreflang when you have:
Hreflang is a signal, not a directive. Search engines can ignore it if they think a different version better matches user intent.
useHead({
link: [
{ rel: 'alternate', hreflang: 'en', href: 'https://example.com/en' },
{ rel: 'alternate', hreflang: 'fr', href: 'https://example.com/fr' },
{ rel: 'alternate', hreflang: 'x-default', href: 'https://example.com/en' }
]
})
useSeoMeta({
alternateLanguages: {
'en': 'https://example.com/en',
'fr': 'https://example.com/fr',
'x-default': 'https://example.com/en'
}
})
Hreflang values use ISO 639-1 language codes and optional ISO 3166-1 Alpha-2 region codes:
| Format | Example | Use Case |
|---|---|---|
| Language only | en | English content for all regions |
| Language + region | en-US | English for United States |
| Language + region | en-GB | English for United Kingdom |
| Special fallback | x-default | Default for unmatched languages/regions |
Language codes must be lowercase. Region codes must be uppercase. Separate with hyphen: en-US.
en English (all regions)
fr French (all regions)
es Spanish (all regions)
en-US English for USA
en-GB English for UK
fr-CA French for Canada
es-MX Spanish for Mexico
zh-CN Simplified Chinese (China)
zh-TW Traditional Chinese (Taiwan)
UK is Ukraine, not United Kingdom. Use GB for Great Britain. Chinese uses zh-CN/zh-TW or zh-Hans/zh-Hant for simplified/traditional variants since ISO 639-1 only defines one zh code.
Use useHead() to set hreflang tags that work in SSR:
<script setup lang="ts">
const route = useRoute()
// Build alternate URLs based on current route
const alternates = [
{ lang: 'en', url: `https://example.com/en${route.path}` },
{ lang: 'fr', url: `https://example.com/fr${route.path}` },
{ lang: 'de', url: `https://example.com/de${route.path}` }
]
useHead({
link: [
...alternates.map(alt => ({
rel: 'alternate',
hreflang: alt.lang,
href: alt.url
})),
// x-default fallback
{ rel: 'alternate', hreflang: 'x-default', href: 'https://example.com/en' }
]
})
</script>
If you're using vue-i18n for translations, generate hreflang from available locales:
import { useI18n } from 'vue-i18n'
const { locale, availableLocales } = useI18n()
const route = useRoute()
// Generate alternate links for all locales
const alternateLinks = availableLocales.map(loc => ({
rel: 'alternate',
hreflang: loc,
href: `https://example.com/${loc}${route.path}`
}))
// Add self-referential link for current locale
alternateLinks.push({
rel: 'alternate',
hreflang: locale.value,
href: `https://example.com/${locale.value}${route.path}`
})
useHead({ link: alternateLinks })
Extract hreflang logic into a reusable composable:
// composables/useHreflang.ts
export function useHreflang(locales: string[]) {
const route = useRoute()
const links = computed(() => {
const baseUrl = 'https://example.com'
return [
...locales.map(locale => ({
rel: 'alternate',
hreflang: locale,
href: `${baseUrl}/${locale}${route.path}`
})),
// x-default to primary locale
{
rel: 'alternate',
hreflang: 'x-default',
href: `${baseUrl}/${locales[0]}${route.path}`
}
]
})
useHead({ link: links })
}
Use it in components:
<script setup lang="ts">
useHreflang(['en', 'fr', 'de', 'es'])
</script>
The x-default hreflang provides a fallback URL when no language matches the user's preferences. Google recommends x-default for all hreflang clusters.
<!-- User with no matching language sees this -->
<link rel="alternate" hreflang="x-default" href="https://example.com/en" />
Set x-default to:
// Point x-default to language selector
useHead({
link: [
{ rel: 'alternate', hreflang: 'en-US', href: 'https://example.com/en-us' },
{ rel: 'alternate', hreflang: 'en-GB', href: 'https://example.com/en-gb' },
{ rel: 'alternate', hreflang: 'fr-FR', href: 'https://example.com/fr-fr' },
{ rel: 'alternate', hreflang: 'x-default', href: 'https://example.com/choose-language' }
]
})
According to Victorious, x-default improves user experience by routing unmatched users to an appropriate fallback instead of a random language version.
Every page referenced in hreflang must link back. If page A links to page B, page B must link to page A. This is the most common hreflang error.
<!-- en page must reference fr page -->
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<!-- fr page must reference en page -->
<link rel="alternate" hreflang="en" href="https://example.com/en" />
Each page must include a hreflang tag pointing to itself:
<!-- On the English page -->
<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<!-- On the French page -->
<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
All hreflang URLs must:
noindex)According to I Love SEO, using non-200, non-indexable, or non-canonical URLs is the root cause of most hreflang mistakes.
<script setup lang="ts">
// ✅ Good - points to canonical URL
useHead({
link: [
{ rel: 'canonical', href: 'https://example.com/en/products' },
{ rel: 'alternate', hreflang: 'en', href: 'https://example.com/en/products' },
{ rel: 'alternate', hreflang: 'fr', href: 'https://example.com/fr/produits' }
]
})
// ❌ Bad - points to redirect
useHead({
link: [
{ rel: 'alternate', hreflang: 'en', href: 'https://example.com/old-url' } // redirects to /en/products
]
})
</script>
Canonical tags and hreflang serve different purposes. Don't use canonical to point between language versions—that signals duplicate content, not translations.
<!-- ✅ Correct: each language version is canonical to itself -->
<!-- English page -->
<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" />
<!-- French page -->
<link rel="canonical" href="https://example.com/fr/a-propos" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />
<!-- ❌ Wrong: canonical pointing between languages -->
<!-- French page -->
<link rel="canonical" href="https://example.com/en/about" />
<!-- This tells Google the French page is duplicate content -->
Hreflang tags must exist in the initial HTML response. SPAs that render hreflang client-side won't work reliably:
<script setup lang="ts">
// ❌ Runs after SSR - search engines may miss this
onMounted(() => {
useHead({
link: [
{ rel: 'alternate', hreflang: 'en', href: 'https://example.com/en' }
]
})
})
// ✅ Runs during SSR and client-side
useHead({
link: [
{ rel: 'alternate', hreflang: 'en', href: 'https://example.com/en' }
]
})
</script>
If you're building a SPA, see the prerendering guide or SSR frameworks comparison.
You can implement hreflang using HTML <link> tags, HTTP headers, or XML sitemaps. Best practice is to choose one method and stick to it. Mixing methods creates conflicting signals.
Most flexible approach. Set per-page using Unhead:
<script setup lang="ts">
useHead({
link: [
{ rel: 'alternate', hreflang: 'en-US', href: 'https://example.com/us' },
{ rel: 'alternate', hreflang: 'en-GB', href: 'https://example.com/uk' },
{ rel: 'alternate', hreflang: 'x-default', href: 'https://example.com/us' }
]
})
</script>
Useful for non-HTML resources (PDFs, etc.). Configure in your server:
app.get('/document.pdf', (req, res) => {
res.set('Link', [
'<https://example.com/en/document.pdf>; rel="alternate"; hreflang="en"',
'<https://example.com/fr/document.pdf>; rel="alternate"; hreflang="fr"',
'<https://example.com/en/document.pdf>; rel="alternate"; hreflang="x-default"'
].join(', '))
res.sendFile('/path/to/document.pdf')
})
export default defineEventHandler((event) => {
setHeader(event, 'Link', [
'<https://example.com/en/document.pdf>; rel="alternate"; hreflang="en"',
'<https://example.com/fr/document.pdf>; rel="alternate"; hreflang="fr"',
'<https://example.com/en/document.pdf>; rel="alternate"; hreflang="x-default"'
].join(', '))
return sendStream(event, createReadStream('/path/to/document.pdf'))
})
// server.ts
server.use((req, res, next) => {
if (req.url.endsWith('.pdf')) {
res.setHeader('Link', [
'<https://example.com/en/document.pdf>; rel="alternate"; hreflang="en"',
'<https://example.com/fr/document.pdf>; rel="alternate"; hreflang="fr"',
'<https://example.com/en/document.pdf>; rel="alternate"; hreflang="x-default"'
].join(', '))
}
next()
})
Add hreflang to your sitemap if you can't modify HTML/headers:
<?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</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en" />
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en" />
</url>
<url>
<loc>https://example.com/fr</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en" />
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en" />
</url>
</urlset>
<!-- ❌ English page links to French, but French doesn't link back -->
<!-- en page -->
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<!-- fr page -->
<!-- Missing hreflang links! -->
<!-- ❌ Linking to URL that redirects -->
<link rel="alternate" hreflang="en" href="https://example.com/en-us" />
<!-- But /en-us redirects to /us -->
<!-- ✅ Use final destination -->
<link rel="alternate" hreflang="en" href="https://example.com/us" />
<!-- ❌ Hreflang on noindex page -->
<meta name="robots" content="noindex" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
All pages in hreflang cluster must be indexable. Remove noindex or remove the page from hreflang annotations.
If a URL is blocked in robots.txt, crawlers can't access it to validate hreflang links. Ensure all alternate URLs are crawlable.
<!-- ❌ Don't point canonical between languages -->
<!-- French page -->
<link rel="canonical" href="https://example.com/en" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<!-- Conflicting signals: canonical says "I'm a duplicate of EN"
but hreflang says "I'm the French version" -->
View page source and verify:
After implementing hreflang, check Google Search Console > International Targeting for errors:
Here's a complete implementation with vue-i18n and Vue Router:
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const locales = ['en', 'fr', 'de', 'es']
const router = createRouter({
history: createWebHistory(),
routes: locales.flatMap(locale => [
{
path: `/${locale}`,
component: () => import('@/pages/Home.vue'),
meta: { locale }
},
{
path: `/${locale}/products`,
component: () => import('@/pages/Products.vue'),
meta: { locale }
}
])
})
export default router
// composables/useHreflang.ts
export function useHreflang() {
const route = useRoute()
const { locale } = useI18n()
const locales = ['en', 'fr', 'de', 'es']
const baseUrl = 'https://example.com'
// Remove locale prefix to get path
const pathWithoutLocale = route.path.replace(`/${locale.value}`, '')
const links = computed(() => [
// All locale variants
...locales.map(loc => ({
rel: 'alternate',
hreflang: loc,
href: `${baseUrl}/${loc}${pathWithoutLocale}`
})),
// Current page (self-referential)
{
rel: 'alternate',
hreflang: locale.value,
href: `${baseUrl}${route.path}`
},
// x-default fallback
{
rel: 'alternate',
hreflang: 'x-default',
href: `${baseUrl}/en${pathWithoutLocale}`
}
])
useHead({ link: links })
}
<!-- pages/Home.vue -->
<script setup lang="ts">
useHreflang()
useSeoMeta({
title: 'Home',
description: 'Welcome to our site'
})
</script>
<template>
<main>
<h1>{{ $t('home.title') }}</h1>
</main>
</template>
Nuxt's @nuxtjs/i18n module automatically generates hreflang tags for all configured locales with zero configuration. It handles return links, self-referential tags, and x-default automatically.
Query Parameters
Query parameters create duplicate content and waste crawl budget. Here's how to handle filters, sorting, and tracking params in Vue Router.
404 Pages
404 errors don't hurt SEO, but soft 404s do. Learn proper HTTP status codes, custom 404 design, and crawl budget optimization for Vue applications.