Index of every page on this site: llms.txt

Hreflang Tags in Vue

Map corresponding translated pages to hreflang URLs in Vue. Verify self links, return links, canonical choices, and reactive Unhead output.
Harlan Wilton12 mins read Published Updated
What you'll learn
  • Hreflang identifies corresponding language or regional pages. It does not guarantee which version Google shows
  • Every page in a hreflang cluster needs a self-referencing tag and must link back to every alternate version it references
  • An optional x-default identifies a fallback for unmatched languages. Hreflang does not redirect visitors

Hreflang identifies corresponding language or regional versions of a page. Google can discover these versions without annotations. Fully translated main content is not a duplicate merely because it describes the same topic. Hreflang does not replace translated content or guarantee a particular search result.

Emit this complete set on both corresponding English and French landing pages:

<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:

  • Same content in different languages
  • Regional variations (en-US vs en-GB)
  • Partial translations mixed with original language

Hreflang is a signal, not a directive. Search engines can ignore it if they think a different version better matches user intent.

Quick Reference

Before calling useHead() in component setup, install Unhead and its Vue plugin. This excerpt covers the same two published landing pages:

Unhead
import { useHead } from '@unhead/vue'

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' }
  ]
})

Hreflang Format

Hreflang values combine an ISO 639-1 language code with an optional ISO 3166-1 Alpha-2 region code:

FormatExampleUse Case
Language onlyenEnglish content for all regions
Language + regionen-USEnglish for United States
Language + regionen-GBEnglish for United Kingdom
Special fallbackx-defaultDefault for unmatched languages/regions

Google treats hreflang values as case-insensitive. Use conventional lowercase language codes and uppercase regions, separated by a hyphen: en-US.

Common Examples

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)

Region Codes Are Tricky

UK isn't a valid ISO 3166-1 region code: use GB for the United Kingdom instead. For Chinese, Google's own hreflang examples use zh-TW (the script is inferred as Traditional) or the explicit script codes zh-Hant and zh-Hans, since ISO 639-1 only defines one zh language code.

Implementation in Vue

With Unhead

Avoid prefixing the current localized path with another locale. Slugs can differ between translations, such as English /en/about and French /fr/a-propos. Store the actual corresponding URLs by page identity.

The composable below emits that mapping. Install Unhead's plugin first. Supply the current page identity from your router or data layer, and include only published counterparts.

With vue-i18n

Vue I18n's Composition API provides translations and locale state. Follow its installation guide and configure createI18n({ legacy: false, ... }) before using useI18n() in component setup.

An available message locale does not prove that a translated URL exists. Keep the URL mapping separate from the message catalog. When the route changes, update the translation locale and supply the matching page identity to the head composable.

Creating a Composable

Save this as useHreflang.ts. This two-language example has a published translation for each page. Add or remove entries to reflect your actual content:

import { useHead } from '@unhead/vue'

export type PageId = 'home' | 'about'
export type Locale = 'en' | 'fr'

export const translatedPages = {
  home: { en: 'https://example.com/en', fr: 'https://example.com/fr' },
  about: {
    en: 'https://example.com/en/about',
    fr: 'https://example.com/fr/a-propos'
  }
} satisfies Record<PageId, Record<Locale, string>>

export function useHreflang(page: () => PageId) {
  useHead(() => {
    const urls = translatedPages[page()]
    return {
      link: [
        { rel: 'alternate', hreflang: 'en', href: urls.en },
        { rel: 'alternate', hreflang: 'fr', href: urls.fr },
        { rel: 'alternate', hreflang: 'x-default', href: urls.en }
      ]
    }
  })
}

The getter reads the current page identity when it changes. Emit the same set on each corresponding translation. The English fallback points to the equivalent English page, rather than sending every unmatched-language result to the home page.

X-Default Tag

The optional x-default identifies a fallback when no language or region matches. Google recommends considering a fallback, particularly for language selectors. It does not perform a browser redirect.

<!-- Fallback annotation for unmatched languages -->
<link rel="alternate" hreflang="x-default" href="https://example.com/en" />

Set x-default to:

  • Your primary language version
  • A language selector page
  • A page suitable for unmatched-language visitors
import { useHead } from '@unhead/vue'

// 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' }
  ]
})

Hreflang Rules

Every page referenced in hreflang must link back: if page A links to page B, page B must link to page A. Missing return links is one of the mistakes Google calls out directly.

<!-- 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" />

3. Use Canonical URLs Only

For the counterparts you want Google to process, use URLs that:

  • Return HTTP 200 status
  • Be indexable (no noindex)
  • Not be blocked by robots.txt
  • Point to canonical URLs (not redirects)

Use reachable, indexable final URLs that represent the corresponding content. A blocked or excluded URL can prevent Google from processing that counterpart; it does not prove every other pair fails.

This setup excerpt describes the fully translated English product page. Emit the same alternate set, with the French canonical, on its French counterpart:

import { useHead } from '@unhead/vue'

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' }
  ]
})

If an old URL redirects, reference its final counterpart instead.

4. Canonical vs Hreflang

Canonical tags identify a preferred representative of duplicate or near-identical content. Hreflang identifies localized counterparts. For fully translated pages, use a canonical for that language version. If the main content remains untranslated, assess the appropriate canonical separately.

<!-- ✅ 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 asks Google to prefer the English URL instead -->

Delivering Hreflang

Google supports HTML tags, HTTP headers, and sitemaps. SSR is not required for all these methods. Static HTML or a sitemap can describe counterparts without runtime rendering.

If you use SSR or generation, emit head tags during setup so they appear in the response. An onMounted() callback does not run on the server. Inspect both initial and rendered HTML when the app depends on JavaScript. See the prerendering guide for build-time output.

Implementation Methods

Google considers HTML tags, HTTP headers, and XML sitemaps equivalent methods. Choose the method your site can maintain. Using several methods gives no additional Search benefit; keep them consistent if you do.

This component-setup excerpt describes corresponding US and UK landing pages. Emit the same set on both pages:

<script setup lang="ts">
import { useHead } from '@unhead/vue'

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>

HTTP Headers

HTTP Link headers can describe non-HTML resources such as PDFs. Set this complete header on both corresponding English and French PDF responses through your host or server:

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"

The file-serving handler and response configuration belong to your host. Verify the actual PDF response headers, rather than assuming a Vue component can set them.

XML Sitemap

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>

Common Mistakes

<!-- ❌ 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! -->

Non-Canonical URLs

<!-- ❌ 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" />

Noindex or Blocked Pages

<!-- ❌ Hreflang on noindex page -->
<meta name="robots" content="noindex" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />

All pages in a hreflang cluster must be indexable and crawlable. Remove noindex, unblock the URL in robots.txt, or remove the page from hreflang annotations altogether.

Mixing with Canonical

<!-- Fully translated French page: prefer its French URL -->
<!-- French page -->
<link rel="canonical" href="https://example.com/en" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<!-- This canonical asks Google to prefer EN instead of the French URL -->

Testing Hreflang

Manual Inspection

View page source and verify:

  • All alternate links are present
  • Self-referential link exists
  • If used, x-default points to the intended fallback
  • All URLs return 200
  • All URLs are canonical (not redirects)

Google Search Console

The International Targeting report is deprecated. Google still supports hreflang. Inspect the actual tags and counterpart URLs directly. URL Inspection can help check a page's fetch and indexing state, but it is not a replacement hreflang-cluster report.

Third-Party Tools

Worked Example: Translated Pages

Save this component as TranslatedPage.vue beside useHreflang.ts. It demonstrates reactive annotations and canonicals. Replace its illustrative heading with your translated page content:

<script setup lang="ts">
import type { Locale, PageId } from './useHreflang'
import { useHead } from '@unhead/vue'
import { translatedPages, useHreflang } from './useHreflang'

const props = defineProps<{ page: PageId, locale: Locale }>()
useHreflang(() => props.page)
useHead({
  link: [{ rel: 'canonical', href: () => translatedPages[props.page][props.locale] }]
})
</script>

<template>
  <main>
    <h1>{{ props.page }}</h1>
  </main>
</template>

In an installed Vue Router app, this route excerpt supplies page identity and locale as props:

import TranslatedPage from './TranslatedPage.vue'

const routes = [
  { path: '/en', component: TranslatedPage, props: { page: 'home', locale: 'en' } },
  { path: '/fr', component: TranslatedPage, props: { page: 'home', locale: 'fr' } },
  { path: '/en/about', component: TranslatedPage, props: { page: 'about', locale: 'en' } },
  { path: '/fr/a-propos', component: TranslatedPage, props: { page: 'about', locale: 'fr' } }
]

Create the router and install it, along with the Unhead plugin, using the Vue setup guide. For SSR, create fresh app, router, and head instances per request. The Vite SSR guide shows the server integration. These excerpts do not install Vue I18n, fetch translations, serve routes, or transfer initial state.

Checklist

Checklist
  • Every page includes a self-referencing hreflang tag plus links to all alternate versions
  • Every alternate page links back (return links) to close the cluster
  • If used, x-default identifies a suitable fallback for unmatched languages
  • Hreflang URLs return 200, are indexable, and aren't blocked by robots.txt
  • Canonical choices reflect the content; fully translated pages use a preferred URL for their own language
  • The chosen HTML, header, or sitemap method contains the intended annotations; inspect actual output

Using Nuxt? @nuxtjs/i18n provides locale-aware head helpers. Its SEO setup requires configuration, including locale language codes and an appropriate base URL. Learn more in Nuxt →

Was this page helpful?