Nuxt i18n SEO Guide
Configure¶
mkdir -p i18n/locales
touch i18n/i18n.config.ts i18n/locales/en.json i18n/locales/zh-Hans.json
i18n/i18n.config.ts
import en from './locales/en.json'
import et from './locales/et.json'
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
messages: {
en,
et,
}
}))
nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
strategy: 'prefix_except_default',
defaultLocale: 'en',
detectBrowserLanguage: false,
baseUrl: 'https://ex.ex',
lazy: true,
locales: [
{
code: 'en',
language: 'en',
name: 'English',
file: 'en.json',
},
{
code: 'et',
language: 'et',
name: 'Eesti',
file: 'et.json'
},
],
vueI18n: 'i18n.config.ts',
}
})
Configuring the vueI18n option can help resolve potential warnings, such as WARN [Vue warn]: Invalid watch source: en. A watch source must be a getter/effect function, a ref, a reactive object, or an array of these types
. This warning may occur when using certain packages. For example, I encountered it after installing @nuxtjs/sitemap version 7.2.9.
SEO Documentation examples¶
Layout Components Approach¶
layouts/default.vue
<script setup>
const route = useRoute()
const { t } = useI18n()
const head = useLocaleHead()
const title = computed(() => t(route.meta.title ?? 'TBD', t('layouts.title'))
);
</script>
<template>
<div>
<Html :lang="head.htmlAttrs.lang" :dir="head.htmlAttrs.dir">
<Head>
<Title>{{ title }}</Title>
<template v-for="link in head.link" :key="link.hid">
<Link :id="link.hid" :rel="link.rel" :href="link.href" :hreflang="link.hreflang" />
</template>
<template v-for="meta in head.meta" :key="meta.hid">
<Meta :id="meta.hid" :property="meta.property" :content="meta.content" />
</template>
</Head>
<Body>
<slot />
</Body>
</Html>
</div>
</template>
Access the baseUrl
result:
<html lang="en" dir="ltr">
<link id="i18n-alt-en" rel="alternate" href="<i18n.baseUrl>" hreflang="en">
<link id="i18n-alt-es" rel="alternate" href="<i18n.baseUrl>/es" hreflang="es">
<link id="i18n-alt-de" rel="alternate" href="<i18n.baseUrl>/de" hreflang="de">
<link id="i18n-alt-fr" rel="alternate" href="<i18n.baseUrl>/fr" hreflang="fr">
<link id="i18n-alt-ja" rel="alternate" href="<i18n.baseUrl>/ja" hreflang="ja">
<link id="i18n-alt-zh" rel="alternate" href="<i18n.baseUrl>/zh-hans" hreflang="zh">
<link id="i18n-alt-zh-Hans" rel="alternate" href="<i18n.baseUrl>/zh-hans" hreflang="zh-Hans">
<link id="i18n-xd" rel="alternate" href="<i18n.baseUrl>" hreflang="x-default">
<link id="i18n-can" rel="canonical" href="<i18n.baseUrl>">
<meta id="i18n-og-url" content="<i18n.baseUrl>" property="og:url">
<meta id="i18n-og" content="en" property="og:locale">
<meta id="i18n-og-alt-es" content="es" property="og:locale:alternate">
<meta id="i18n-og-alt-de" content="de" property="og:locale:alternate">
<meta id="i18n-og-alt-fr" content="fr" property="og:locale:alternate">
<meta id="i18n-og-alt-ja" content="ja" property="og:locale:alternate">
<meta id="i18n-og-alt-zh-Hans" content="zh_Hans" property="og:locale:alternate">
Composition API Approach¶
<script setup>
const i18nHead = useLocaleHead({ seo: { canonicalQueries: ['foo'] } })
useHead(() => ({
htmlAttrs: {
lang: i18nHead.value.htmlAttrs!.lang
},
link: [...(i18nHead.value.link || [])],
meta: [...(i18nHead.value.meta || [])]
}))
</script>
Access the baseUrl
result:
<html lang="en">
<link rel="alternate" href="<i18n.baseUrl>" hreflang="en" data-hid="7fcee50">
<link rel="alternate" href="<i18n.baseUrl>/es" hreflang="es" data-hid="52a6c69">
<link rel="alternate" href="<i18n.baseUrl>/de" hreflang="de" data-hid="5d718b2">
<link rel="alternate" href="<i18n.baseUrl>/fr" hreflang="fr" data-hid="484de76">
<link rel="alternate" href="<i18n.baseUrl>/ja" hreflang="ja" data-hid="396afb4">
<link rel="alternate" href="<i18n.baseUrl>/zh-hans" hreflang="zh" data-hid="1a9396e">
<link rel="alternate" href="<i18n.baseUrl>/zh-hans" hreflang="zh-Hans" data-hid="70511e8">
<link rel="alternate" href="<i18n.baseUrl>" hreflang="x-default" data-hid="6527e7a">
<link rel="canonical" href="<i18n.baseUrl>">
<meta property="og:url" content="<i18n.baseUrl>">
<meta property="og:locale" content="en">
<meta property="og:locale:alternate" content="es">
<meta property="og:locale:alternate" content="de">
<meta property="og:locale:alternate" content="fr">
<meta property="og:locale:alternate" content="ja">
<meta property="og:locale:alternate" content="zh_Hans">
Enhanced Composition API Approach¶
<script setup lang="ts">
const i18nHead = useLocaleHead({
seo: true,
key: 'id'
});
useHead({
htmlAttrs: {
lang: i18nHead.value.htmlAttrs!.lang,
dir: i18nHead.value.htmlAttrs?.dir
},
link: [
...(i18nHead.value.link || [])
],
meta: [
...(i18nHead.value.meta || [])
]
})
</script>
Access the baseUrl
result:
<html lang="en" dir="ltr">
<link id="i18n-alt-en" rel="alternate" href="<i18n.baseUrl>" hreflang="en">
<link id="i18n-alt-es" rel="alternate" href="<i18n.baseUrl>/es" hreflang="es">
<link id="i18n-alt-de" rel="alternate" href="<i18n.baseUrl>/de" hreflang="de">
<link id="i18n-alt-fr" rel="alternate" href="<i18n.baseUrl>/fr" hreflang="fr">
<link id="i18n-alt-ja" rel="alternate" href="<i18n.baseUrl>/ja" hreflang="ja">
<link id="i18n-alt-zh" rel="alternate" href="<i18n.baseUrl>/zh-hans" hreflang="zh">
<link id="i18n-alt-zh-Hans" rel="alternate" href="<i18n.baseUrl>/zh-hans" hreflang="zh-Hans">
<link id="i18n-xd" rel="alternate" href="<i18n.baseUrl>" hreflang="x-default">
<link id="i18n-can" rel="canonical" href="<i18n.baseUrl>">
<meta id="i18n-og-url" property="og:url" content="<i18n.baseUrl>">
<meta id="i18n-og" property="og:locale" content="en">
<meta id="i18n-og-alt-es" property="og:locale:alternate" content="es">
<meta id="i18n-og-alt-de" property="og:locale:alternate" content="de">
<meta id="i18n-og-alt-fr" property="og:locale:alternate" content="fr">
<meta id="i18n-og-alt-ja" property="og:locale:alternate" content="ja">
<meta id="i18n-og-alt-zh-Hans" property="og:locale:alternate" content="zh_Hans">
Page Component Usage Examples¶
Component Interpolation¶
Sometimes, we need to localize with a locale message that was included in a HTML tag or component. For example:
<p>Powered by <a href="https://1024.dev">1024</a></p>
en.json
{
"powered_by": "Powered by {name}"
}
<i18n-t keypath="powered_by" tag="p">
<template #name>
<a href="https://1024.dev">
1024
</a>
</template>
</i18n-t>
Output:
<p>Powered by <a href="https://1024.dev">1024</a></p>
However, You sometimes meet the warning message on your browser console the following:
[intlify] Not found parent scope. use the global scope.
A workaround to suppress this warning is to specify global as the scope property of Translation component.
<i18n-t keypath="powered_by" scope="global" tag="p">
<template #name>
<a href="https://1024.dev">
1024
</a>
</template>
</i18n-t>