How VitePress Handles Multi-Language SEO with hreflang Tags and Dynamic Locale Configuration

VitePress generates comprehensive multi-language SEO metadata by defining a centralized localeMap in docs/.vitepress/config.mjs, dynamically building hreflang alternate links via the getSeoHead function, and synchronizing runtime locale detection through the useI18n composable, ensuring every page emits proper canonical URLs, Open Graph tags, and cross-language references for search engines.

The datawhalechina/easy-vibe documentation site demonstrates a production-grade approach to international SEO in static site generation. By combining compile-time configuration with dynamic head generation, the implementation ensures each localized page—including deeply nested routes—publishes correct hreflang annotations, canonical URLs, and social media metadata.

The Three-Pillar SEO Architecture

The multi-language SEO implementation rests on three coordinated components defined in docs/.vitepress/config.mjs: a static locale registry, a dynamic head generator, and a runtime locale binding system.

Centralized Locale Metadata

At lines 31–92 of config.mjs, the localeMap object serves as the single source of truth for all language-specific SEO attributes. This plain object maps locale keys (e.g., zh-cn, en, ja-jp) to configurations containing Open Graph locales, Twitter handles, HTML language attributes, and hreflang identifiers.

const localeMap = {
  'zh-cn': { ogLocale: 'zh_CN', twitterSite: '@datawhale', lang: 'zh-CN', hreflang: 'zh-CN' },
  en:     { ogLocale: 'en_US', twitterSite: '@datawhale', lang: 'en-US', hreflang: 'en' },
  // additional locales...
}

Dynamic Head Generation

The getSeoHead function (around lines 95–82 in config.mjs) constructs the complete <head> element for every page. It accepts a locale key, page title, description, and path, then returns a VitePress-compatible head array containing canonical links, Open Graph tags, Twitter Cards, and crucially, the full set of hreflang alternate links.

const getSeoHead = (locale, title, description, path = '') => {
  const seoConfig = localeMap[locale] || localeMap['zh-cn']
  const canonicalUrl = path ? `${siteUrl}${path}` : `${siteUrl}/${locale}/`
  const relativePath = getRelativePath(path, locale)   // strips leading "/en/"

  const head = [
    ['link', { rel: 'canonical', href: canonicalUrl }],
    // Open Graph and Twitter tags omitted for brevity...
  ]

  // Generate hreflang alternates for all languages
  Object.keys(localeMap).forEach(lang => {
    let alternateUrl = `${siteUrl}/${lang}/`
    if (relativePath) {
      alternateUrl = `${siteUrl}/${lang}/${relativePath}`
    }
    head.push(['link', { rel: 'alternate', hreflang: localeMap[lang].hreflang, href: alternateUrl }])
  })
  
  // x-default points to Chinese version
  head.push(['link', { rel: 'alternate', hreflang: 'x-default', href: `${siteUrl}/zh-cn/` }])

  return head
}

Runtime Locale Synchronization

To ensure UI components and SEO metadata reference the same language code, docs/.vitepress/theme/composables/useI18n.js (lines 22–30) exposes the active locale through a reactive composable. This pulls from localeMap to compute locale and provides a translation helper t, keeping navigation, breadcrumbs, and page content synchronized with the SEO head.

import { useI18n } from '../composables/useI18n'

export default {
  setup() {
    const { t, locale } = useI18n()
    return { t, locale }
  },
  template: `<h1>{{ t('site.title') }}</h1>`
}

How hreflang Tags Are Generated

VitePress does not automatically create hreflang tags for multi-language sites. The easy-vibe implementation handles this by computing alternate URLs for every page during the build process.

Handling Deep Page Paths

For nested routes like /en/stage-2/frontend/figma-mastergo/, the system cannot simply swap the locale prefix. The getRelativePath utility strips the current locale segment from the path, then the generator loops through Object.keys(localeMap) to reconstruct the full URL for each language variant:

  • Original: /en/stage-2/frontend/figma-mastergo/
  • Relative path: stage-2/frontend/figma-mastergo/
  • Chinese alternate: /zh-cn/stage-2/frontend/figma-mastergo/
  • Japanese alternate: /ja-jp/stage-2/frontend/figma-mastergo/

The x-default Fallback

Following Google’s guidelines for multilingual sites, the implementation adds an x-default hreflang tag pointing to the Chinese (zh-cn) version at lines configured within getSeoHead. This signals to search engines which version to display when no other language matches the user's browser preferences.

Integrating with VitePress Configuration

At lines 47–66 of config.mjs, each locale registers in the locales field of the VitePress configuration object. Each entry calls getSeoHead with its specific parameters, injecting the generated head array into themeConfig.head:

locales: {
  'en': {
    label: 'English',
    lang: 'en-US',
    link: '/en/',
    title: 'Easy-Vibe Tutorial',
    description: 'Learn Vibe Coding with AI from scratch.',
    head: getSeoHead('en', 'Easy-Vibe Tutorial', 'Learn Vibe Coding...', '/en/'),
    themeConfig: { /* ... */ }
  },
  // other locales...
}

During static site generation, VitePress merges these head arrays into the final HTML, ensuring every page contains the complete set of SEO metadata.

Extending to New Languages

Adding support for an additional language requires three steps:

  1. Extend localeMap with the new locale’s SEO attributes:
'pt-br': {
  ogLocale: 'pt_BR',
  twitterSite: '@datawhale',
  lang: 'pt-BR',
  hreflang: 'pt-BR'
}
  1. Register the locale in the locales configuration block, calling getSeoHead with the new locale key, title, and description.

  2. Create the content directory at docs/pt-br/ mirroring the structure of existing locales. The hreflang generation logic automatically includes the new language in the alternate link loop without further configuration changes.

Summary

  • Centralized configuration: The localeMap object in config.mjs defines all SEO-relevant locale metadata in one location.
  • Dynamic generation: The getSeoHead function builds canonical URLs, Open Graph tags, and a complete set of hreflang alternate links for every page, including deep paths.
  • Path manipulation: By stripping locale prefixes and rebuilding URLs, the system ensures cross-language links point to equivalent content across all languages.
  • Runtime sync: The useI18n composable binds the runtime locale to the SEO configuration, preventing mismatches between page content and metadata.
  • x-default support: The implementation includes the recommended x-default hreflang tag pointing to the primary Chinese language version.

Frequently Asked Questions

How does VitePress know which hreflang tags to generate for a specific page?

VitePress relies on the custom getSeoHead function defined in docs/.vitepress/config.mjs. This function receives the current page path, strips the locale prefix using getRelativePath, then iterates over all keys in localeMap to construct alternate URLs. It emits a <link rel="alternate" hreflang="..."> tag for every supported language plus an x-default entry, ensuring search engines discover all language variants regardless of which version they crawl initially.

What happens if I add a page in one language but not another?

The hreflang tags are generated based on the locale configuration and path structure, not content availability. If you create /en/new-page/ but lack a corresponding /zh-cn/new-page/, the hreflang link for the Chinese version will still be generated (pointing to /zh-cn/new-page/), resulting in a 404 unless you create the content. To maintain SEO integrity, ensure content parity across locales or implement conditional logic in getSeoHead to check file existence before emitting alternate links.

Why is the x-default hreflang set to zh-cn instead of auto-detecting the user's language?

According to the implementation in config.mjs, the x-default tag hardcodes the URL to the Chinese (zh-cn) version using href: ${siteUrl}/zh-cn/. This follows the pattern where x-default should point to a language selector page or the primary market version. In the easy-vibe setup, Chinese serves as the primary content language, making it the appropriate fallback when a user's browser preferences do not match any of the specific declared languages.

How do I verify that hreflang tags are rendered correctly on my VitePress site?

Build the site using vitepress build and inspect the generated HTML in the dist directory (or your configured output directory). Look for <link rel="alternate"> tags in the <head> section of any page. Each should contain hreflang attributes matching your defined locales, with URLs correctly reflecting the page's path structure. You can also use SEO auditing tools or Google Search Console to validate the hreflang implementation across your domain.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →