Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 50 additions & 31 deletions docs/content/docs/02.guide/06.seo.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ Here are the specific optimizations and features that it enables:

## Requirements

To leverage the SEO benefits, you must configure the `locales` option as an array of objects, where each object has an `language` option set to the locale language tags:
To leverage the SEO benefits, you must configure the `locales` option as an array of objects, where each object has an `language` option set to the locale language tags.

**NOTE**: while `code` is used to create prefixes for URLs and to set locales and can be any arbitrary value, `language` goes to `<html lang="...">` and must be [BCP 47](https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag). In most cases though both can share the same value like `en` or `es`.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
i18n: {
locales: [
{
code: 'en',
language: 'en-US'
language: 'en'
},
{
code: 'es',
Expand Down Expand Up @@ -61,7 +63,7 @@ The `useLocaleHead()`{lang="ts"} is a composable function, Calling that composab

To enable SEO metadata, declare a `setup` function in one of the places specified above and make it return the result of a `useLocaleHead()`{lang="ts"} function call.

To avoid duplicating the code, it's recommended to set globally with [Meta Components](https://nuxt.com/docs/getting-started/seo-meta#components) in [layout components](https://nuxt.com/docs/guide/directory-structure/layouts) and override some values per-page Vue component like [`definePageMeta()`{lang="ts"}](https://nuxt.com/docs/guide/directory-structure/pages#page-metadata), if necessary.
To avoid duplicating the code, it's recommended to set globally with [Meta Components](https://nuxt.com/docs/getting-started/seo-meta#components) in `app.vue` or [layout components](https://nuxt.com/docs/guide/directory-structure/layouts) and override some values per-page Vue component like [`useSeoMeta()`{lang="ts"}](https://nuxt.com/docs/api/composables/use-seo-meta), if necessary.

::code-group

Expand All @@ -71,44 +73,60 @@ To avoid duplicating the code, it's recommended to set globally with [Meta Compo
<NuxtPage />
</NuxtLayout>
</template>

<script setup>
</script>
```

```vue [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'))
);

// Global i18n head setup
const localeHead = useLocaleHead()

useHead({
...localeHead.value,
// Add something if needed
})

Comment on lines +88 to +92
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix reactivity loss when passing localeHead to useHead

Spreading localeHead.value into a plain object makes the head static and it won’t update on locale changes. Wrap in a function (or pass the computed directly) to keep it reactive.

-useHead({
-  ...localeHead.value,
-  // Add something if needed
-})
+useHead(() => ({
+  ...localeHead.value,
+  // Add something if needed
+}))

Alternative:

-useHead({
-  ...localeHead.value
-})
+useHead(localeHead)
+useHead({ /* your extra static entries */ })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useHead({
...localeHead.value,
// Add something if needed
})
useHead(() => ({
...localeHead.value,
// Add something if needed
}))
🤖 Prompt for AI Agents
In docs/content/docs/02.guide/06.seo.md around lines 88 to 92, spreading
localeHead.value into a plain object makes the head static and loses reactivity;
instead pass a function that returns the reactive value or pass the computed
directly to useHead so it updates on locale changes — replace useHead({
...localeHead.value, /* extras */ }) with useHead(() => ({ ...localeHead.value,
/* extras */ })) or simply useHead(localeHead) as appropriate.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem needed for SEO (as generally there is no need to be reactive for non-title tags). So I'd ignore this comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@okainov, understood! You're right that for SEO metadata like hreflang links, canonical URLs, and og:locale tags, reactivity typically isn't crucial since these values are generally static per page/route. I appreciate the clarification about your specific use case requirements.

  (\   /)
 ( ._.)
o_(")(")  *nods thoughtfully*

// Global SEO defaults
useSeoMeta({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} - ${t('nav.brand')}` : t('nav.brand');
},
title: computed(() => t('site.name')), // Fallback when no page title
description: computed(() => t('site.description')),
ogType: 'website',
ogTitle: 'Wonderful OG title',
ogSiteName: computed(() => t('nav.brand'))
})
</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.key">
<Link :id="link.key" :rel="link.rel" :href="link.href" :hreflang="link.hreflang" />
</template>
<template v-for="meta in head.meta" :key="meta.key">
<Meta :id="meta.key" :property="meta.property" :content="meta.content" />
</template>
</Head>
<Body>
<slot />
</Body>
</Html>
<Navbar />

<!-- Main content -->
<main>
<slot />
</main>

<Footer />
</div>
</template>
```

```vue [pages/index.vue]
<script setup>
definePageMeta({
title: 'pages.title.top' // set resource key
const { locale, locales, t } = useI18n()

useSeoMeta({
// Only override page-specific details
title: computed(() => `${t('index.title')} ${locale.value}`),
description: computed(() => t('site.index.description')),
})

const { locale, locales, t } = useI18n()
const switchLocalePath = useSwitchLocalePath()

const availableLocales = computed(() => {
Expand Down Expand Up @@ -138,18 +156,19 @@ Check out the options you can pass to the `useLocaleHead()`{lang="ts"} in the [c

That's it!

If you also want to add your own metadata, you have to call `useHead()`{lang="ts"}. When you call `useHead()`{lang="ts"} with the additional metadata, `useHead()`{lang="ts"} will merge it global metadata that has already defined.
**NOTE**: while you might be tempted to use [`definePageMeta`{lang="ts"}](https://nuxt.com/docs/4.x/api/utils/define-page-meta) to set page title (and it works!), `definePageMeta`{lang="ts"} is not meant for it. Its main purprose is to set page-specific metadata, such as [layout](https://nuxt.com/docs/guide/directory-structure/layouts) or `middleware`{lang="ts"} and not to manipulate SEO and/or i18n-specific metadata. Default choice should be [`useSeoMeta()`{lang="ts"}](https://nuxt.com/docs/api/composables/use-seo-meta) for fields that it supports and `useHead` for everything else.

If you also want to add your own metadata, you have to call `useSeoMeta()`{lang="ts"}. When you call it with the additional metadata, `useSeoMeta()`{lang="ts"} will merge it global metadata that has already defined.

```vue [pages/about/index.vue]
<script setup>
// define page meta for layouts/default.vue
definePageMeta({
title: 'pages.title.about'
})
// add/override metadata coming from layouts/default.vue

useHead({
meta: [{ property: 'og:title', content: 'this is og title for about page' }]
useSeoMeta({
title: 'pages.title.about',
ogTitle: 'this is og title for about page',
})

</script>

<template>
Expand Down