SEO 与 Meta 标签

Nuxt 底层的 head 管理由 Unhead 提供。你可以得到响应式的 composable、类型安全的 SEO 工具,以及内置组件 —— 按团队习惯任选风格。

nuxt.config.ts —— 全局默认值

放一些不会随页面变化的标签:

export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s · Nuxt 小册',
      htmlAttrs: { lang: 'zh-CN' },
      link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    },
  },
})

Nuxt 还会自动注入:

  • <meta charset="utf-8">
  • <meta name="viewport" content="width=device-width, initial-scale=1">

useHead —— 响应式 composable

<script setup lang="ts">
const title = ref('Hello World')

useHead({
  title,
  meta: [{ name: 'description', content: '我的超棒网站。' }],
  bodyAttrs: { class: 'app' },
  script: [{ innerHTML: 'console.log("loaded")' }],
})
</script>

useHead 支持响应式值,多处调用也会自动去重。

useSeoMeta —— 类型安全的 SEO

useSeoMeta 知道 nameproperty 的区别,拼错、写错一眼就被类型系统发现:

<script setup lang="ts">
useSeoMeta({
  title: '我的超棒网站',
  description: '这个网站做了啥,来了解一下。',
  ogTitle: '我的超棒网站',
  ogDescription: '这个网站做了啥,来了解一下。',
  ogImage: 'https://example.com/og.png',
  twitterCard: 'summary_large_image',
})
</script>

做 SEO 时基本就用它。

组件式写法

喜欢声明式模板也可以:

<script setup lang="ts">
const title = ref('Hello World')
</script>

<template>
  <Head>
    <Title>{{ title }}</Title>
    <Meta name="description" :content="title" />
  </Head>
</template>

支持的标签:<Title><Meta><Link><Script><Style><Base><NoScript><Html><Body>

Title template

全站标题一般是 “页面 — 站点名” 的模式:

<!-- app.vue -->
<script setup lang="ts">
useHead({
  titleTemplate: (title) => title ? `${title} · 我的站点` : '我的站点',
})
</script>

之后任何页面调用 useHead({ title: 'Blog' }) 就会显示成 Blog · 我的站点

Template params

需要多个占位符?

<script setup lang="ts">
useHead({
  titleTemplate: (title) => title ? `${title} %separator %siteName` : '%siteName',
  templateParams: { siteName: '我的站点', separator: '·' },
})
</script>

响应式

所有属性都支持 computed、ref 或 getter,可以直接跟着路由或接口数据走:

<script setup lang="ts">
const route = useRoute()
const post = await $fetch(`/api/posts/${route.params.id}`)

useSeoMeta({
  title: () => post.title,
  description: () => post.excerpt,
  ogImage: () => post.image,
})
</script>

definePageMeta 联动

有时希望页面级静态信息被其他层读到,放到 definePageMeta 里,再在布局里读:

<script setup lang="ts">
definePageMeta({ title: '某个页面' })
</script>
<!-- 默认布局 -->
<script setup lang="ts">
const route = useRoute()
useHead({
  meta: [{ property: 'og:title', content: `我的站点 · ${route.meta.title}` }],
})
</script>

怎么选?

你想做的事 用哪个
全站固定标签 app.head
响应式、任意 head 标签 useHead
类型安全的 SEO 标签 useSeoMeta
偏爱模板写法 <Head> / <Title>

Body 尾部标签

有些脚本要放在 <body> 末尾,用 tagPosition

<script setup lang="ts">
useHead({
  script: [
    {
      src: 'https://third-party-script.com',
      tagPosition: 'bodyClose',
    },
  ],
})
</script>

useHeadSafe

当你要把用户输入合并进 head,优先用 useHeadSafe。它会过滤掉危险属性(如 innerHTML、事件处理器),防止 XSS。

把元信息当作代码认真维护,这是 Nuxt 里最廉价也最划算的优化之一。

SEO 與 Meta 標籤

Nuxt 底層的 head 管理由 Unhead 提供。你可獲得響應式 composable、型別安全的 SEO 工具及內建元件 —— 依團隊習慣任選風格。

nuxt.config.ts —— 全域預設

擺放不隨頁面變化的標籤:

export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s · Nuxt 小冊',
      htmlAttrs: { lang: 'zh-Hant' },
      link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    },
  },
})

Nuxt 亦會自動注入:

  • <meta charset="utf-8">
  • <meta name="viewport" content="width=device-width, initial-scale=1">

useHead —— 響應式 composable

<script setup lang="ts">
const title = ref('Hello World')

useHead({
  title,
  meta: [{ name: 'description', content: '我的超棒網站。' }],
  bodyAttrs: { class: 'app' },
  script: [{ innerHTML: 'console.log("loaded")' }],
})
</script>

useHead 支援響應式值,多處呼叫亦會自動去重。

useSeoMeta —— 型別安全的 SEO

useSeoMeta 知道 nameproperty 的差異,拼錯或寫錯會即時被型別系統攔截:

<script setup lang="ts">
useSeoMeta({
  title: '我的超棒網站',
  description: '此站做了啥,來了解一下。',
  ogTitle: '我的超棒網站',
  ogDescription: '此站做了啥,來了解一下。',
  ogImage: 'https://example.com/og.png',
  twitterCard: 'summary_large_image',
})
</script>

做 SEO 時基本就用它。

元件式寫法

偏好宣告式模板者亦可:

<script setup lang="ts">
const title = ref('Hello World')
</script>

<template>
  <Head>
    <Title>{{ title }}</Title>
    <Meta name="description" :content="title" />
  </Head>
</template>

支援標籤:<Title><Meta><Link><Script><Style><Base><NoScript><Html><Body>

Title template

全站標題通常為「頁面 — 站點名」模式:

<!-- app.vue -->
<script setup lang="ts">
useHead({
  titleTemplate: (title) => title ? `${title} · 我的站點` : '我的站點',
})
</script>

任何頁面呼叫 useHead({ title: 'Blog' }) 都會顯示為 Blog · 我的站點

Template params

需要多個佔位符?

<script setup lang="ts">
useHead({
  titleTemplate: (title) => title ? `${title} %separator %siteName` : '%siteName',
  templateParams: { siteName: '我的站點', separator: '·' },
})
</script>

響應式

所有屬性皆支援 computed、ref 或 getter,可直接跟隨路由或 API 資料:

<script setup lang="ts">
const route = useRoute()
const post = await $fetch(`/api/posts/${route.params.id}`)

useSeoMeta({
  title: () => post.title,
  description: () => post.excerpt,
  ogImage: () => post.image,
})
</script>

definePageMeta 聯動

若希望頁面層級的靜態資訊被其他層讀取,可放於 definePageMeta,再於佈局內讀:

<script setup lang="ts">
definePageMeta({ title: '某個頁面' })
</script>
<!-- 預設佈局 -->
<script setup lang="ts">
const route = useRoute()
useHead({
  meta: [{ property: 'og:title', content: `我的站點 · ${route.meta.title}` }],
})
</script>

該選哪個?

需求 用甚麼
全站固定標籤 app.head
響應式、任意 head 標籤 useHead
型別安全的 SEO 標籤 useSeoMeta
偏好模板寫法 <Head> / <Title>

Body 底部標籤

部分腳本需置於 <body> 尾端,使用 tagPosition

<script setup lang="ts">
useHead({
  script: [
    {
      src: 'https://third-party-script.com',
      tagPosition: 'bodyClose',
    },
  ],
})
</script>

useHeadSafe

當需將使用者輸入合併進 head 時,優先採用 useHeadSafe,它會過濾危險屬性(如 innerHTML、事件處理器)以防 XSS。

把中繼資料當作程式碼認真維護,是 Nuxt 最便宜也最划算的優化之一。

SEO and Meta

Nuxt uses Unhead under the hood for head management. You get reactive composables, type-safe SEO helpers, and built-in components — use whichever style fits your team.

nuxt.config.ts — global defaults

Set tags that don't change per page:

export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s · Nuxt Book',
      htmlAttrs: { lang: 'en' },
      link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    },
  },
})

Nuxt also injects sensible defaults:

  • <meta charset="utf-8">
  • <meta name="viewport" content="width=device-width, initial-scale=1">

useHead — reactive composable

<script setup lang="ts">
const title = ref('Hello World')

useHead({
  title,
  meta: [{ name: 'description', content: 'My amazing site.' }],
  bodyAttrs: { class: 'app' },
  script: [{ innerHTML: 'console.log("loaded")' }],
})
</script>

useHead accepts reactive values and deduplicates tags across components.

useSeoMeta — type-safe SEO

useSeoMeta knows the difference between name and property, so typos and common mistakes are caught by the type system:

<script setup lang="ts">
useSeoMeta({
  title: 'My Amazing Site',
  description: 'This is my amazing site, let me tell you all about it.',
  ogTitle: 'My Amazing Site',
  ogDescription: 'This is my amazing site, let me tell you all about it.',
  ogImage: 'https://example.com/og.png',
  twitterCard: 'summary_large_image',
})
</script>

This is the path I recommend for most SEO work.

Component-style meta

If you prefer declarative templates:

<script setup lang="ts">
const title = ref('Hello World')
</script>

<template>
  <Head>
    <Title>{{ title }}</Title>
    <Meta name="description" :content="title" />
  </Head>
</template>

Supported tags: <Title>, <Meta>, <Link>, <Script>, <Style>, <Base>, <NoScript>, <Html>, <Body>.

Title template

Site-wide titles usually follow a pattern like "Page — Site Name":

<!-- app.vue -->
<script setup lang="ts">
useHead({
  titleTemplate: (title) => title ? `${title} · My Site` : 'My Site',
})
</script>

Any page that calls useHead({ title: 'Blog' }) shows up as Blog · My Site.

Template params

Need multiple placeholders?

<script setup lang="ts">
useHead({
  titleTemplate: (title) => title ? `${title} %separator %siteName` : '%siteName',
  templateParams: { siteName: 'My Site', separator: '·' },
})
</script>

Reactivity

Every property accepts computed, refs, or getters — so you can follow route or API data easily:

<script setup lang="ts">
const route = useRoute()
const post = await $fetch(`/api/posts/${route.params.id}`)

useSeoMeta({
  title: () => post.title,
  description: () => post.excerpt,
  ogImage: () => post.image,
})
</script>

Linking from definePageMeta

Sometimes you want per-page static info that other layers can read. Store it in definePageMeta and pipe it into useHead:

<script setup lang="ts">
definePageMeta({ title: 'Some Page' })
</script>
<!-- default layout -->
<script setup lang="ts">
const route = useRoute()
useHead({
  meta: [{ property: 'og:title', content: `My Site · ${route.meta.title}` }],
})
</script>

useHead vs the alternatives

You want… Reach for…
Site-wide constants app.head in config
Reactive, arbitrary head tags useHead
SEO tags with type safety useSeoMeta
Template-first authors <Head>, <Title>, …

Body tags

Some scripts belong at the bottom of <body>. Use tagPosition:

<script setup lang="ts">
useHead({
  script: [
    {
      src: 'https://third-party-script.com',
      tagPosition: 'bodyClose',
    },
  ],
})
</script>

useHeadSafe

When you need to merge user-provided values into the head, prefer useHeadSafe. It strips dangerous attributes (e.g. innerHTML, event handlers) to avoid XSS.

Well-wired metadata is one of the cheapest performance wins Nuxt gives you — treat it as code, not an afterthought.