0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Nuxt3のSEO対策とmeta設定ベストプラクティス|AI実務ノート 編集部

0
Last updated at Posted at 2026-01-03

1. 結論(この記事で得られること)

Nuxt3のSEO対策、正直「公式ドキュメント読めば分かるでしょ」って思ってた時期が私にもありました。でも実務で運用し始めると、「useHead」と「useSeoMeta」どっち使えばいいの?動的ルートのOGP画像どう管理する?SSRとCSRで挙動違うけど大丈夫?って問題が次々出てくるんですよね。

この記事では以下が手に入ります:

  • useHead / useSeoMeta / app.vue の使い分け基準(迷わない判断軸)
  • 動的meta・OGP設定の実装パターン(API連携含む)
  • SSR/SSG環境でのmeta反映を担保する実装とテスト手法
  • AI活用によるmeta設計レビューとクローラー検証の自動化
  • 本番障害を起こさないためのチェックリストと監視設計

コード例はそのままプロジェクトに持ち込めるレベルで書きます。レビューで「これ危ないです」と指摘される前に、安全な設計を身につけましょう。

2. 前提(環境・読者層)

想定環境

  • Nuxt 3.8以降(Nuxt 3.0〜3.7でも大半は動作します)
  • Node.js 18以上
  • SSR / SSG どちらかを採用(CSRオンリーは対象外)

読者層

  • Nuxt3でサービス開発中、SEOが要件に入っている方
  • useHeadは使ってるけど「これで合ってる?」と不安な方
  • 動的OGP画像やstructured dataまで実装したい中級者

前提知識

  • Nuxt3の基本(pages/components/composables の役割)
  • SSRとCSRの違いを理解している
  • Vue 3のComposition APIが読める

私も最初は「Nuxt2の「head()」と何が違うの?」状態だったので、つまずきポイントは丁寧に解説します。

3. Before:よくあるつまずきポイント

3-1. useHead と useSeoMeta どっち?問題

// ❌ こんなコードをよく見かけます
<script setup>
useHead({
  title: '記事タイトル',
  meta: [
    { name: 'description', content: '説明文' },
    { property: 'og:title', content: '記事タイトル' },
    { property: 'og:description', content: '説明文' }
  ]
})
</script>

これ自体は動くんですが、SEO特化のmetaタグには「useSeoMeta」の方が型安全で保守性が高いです。私も最初全部「useHead」で書いてて、レビューで「ここ分けましょう」と言われました。

3-2. 動的ページでmetaが反映されない

// ❌ ありがちな失敗パターン
// pages/articles/[id].vue
<script setup>
const route = useRoute()
const { data: article } = await useFetch(`/api/articles/${route.params.id}`)
 
useSeoMeta({
  title: article.value.title // 初回レンダリング時にundefinedになる
})
</script>

SSR時の非同期データ解決タイミングを理解してないと、クローラーには空のmetaが返ります。これが原因でOGP画像が出ない障害、過去に2回やらかしました。

3-3. 全ページ共通metaの二重管理

app.vueとnuxt.config.tsとlayouts/default.vueで同じOGP画像を3箇所に書いてる…みたいなプロジェクト、意外とあります。更新時の漏れの温床です。

3-4. metaタグの優先順位を知らない

Nuxt3はmeta管理を「@unhead/vue」でやってるんですが、複数箇所で同じmetaを定義すると後勝ちです。でもこのルールを知らないと「app.vueで設定したはずなのに反映されない!」ってハマります。

4. After:基本的な解決パターン

4-1. useHead / useSeoMeta の明確な使い分け

判断基準(シンプル版)

SEO関連meta(title, description, OGP, Twitter Card)
 使うAPI: 「useSeoMeta」
 理由: 型安全・自動補完・重複排除

link, script, style, htmlAttrs
 使うAPI: 「useHead」
 理由: useSeoMetaでは扱えない

structured data(JSON-LD)
 使うAPI: 「useHead」 + script
 理由: scriptタグが必要

実装例

// ✅ pages/articles/[id].vue
<script setup lang="ts">
const route = useRoute()
const { data: article } = await useFetch(`/api/articles/${route.params.id}`)
 
// SEO特化のmetaはuseSeoMeta
useSeoMeta({
  title: article.value?.title,
  description: article.value?.excerpt,
  ogTitle: article.value?.title,
  ogDescription: article.value?.excerpt,
  ogImage: article.value?.ogImage,
  ogUrl: `https://example.com/articles/${route.params.id}`,
  twitterCard: 'summary_large_image',
})
 
// script/linkなどはuseHead
useHead({
  link: [
    { rel: 'canonical', href: `https://example.com/articles/${route.params.id}` }
  ],
  script: [
    {
      type: 'application/ld+json',
      children: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Article',
        headline: article.value?.title,
        datePublished: article.value?.publishedAt,
      })
    }
  ]
})
</script>

ポイント:「article.value?.title」のOptional Chainingは必須です。SSR初回レンダリング時にdataがnullの可能性があるため。

4-2. 全体デフォルト設定の一元管理

// ✅ app.vue
<script setup lang="ts">
const config = useRuntimeConfig()
 
// 全ページ共通のデフォルト設定
useSeoMeta({
  titleTemplate: '%s | MyService', // 個別ページのtitleに自動付与
  ogSiteName: 'MyService',
  ogLocale: 'ja_JP',
  twitterSite: '@myservice',
})
 
useHead({
  htmlAttrs: { lang: 'ja' },
  link: [
    { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
  ]
})
</script>
 
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

これで各ページは差分だけ書けばOK。titleTemplateは地味に便利で、全ページに「| サイト名」を自動付与してくれます。

4-3. 動的データのSSR対応パターン

重要な原則:metaの設定はawait後に行う

// ✅ 正しいパターン
<script setup lang="ts">
const route = useRoute()
 
// awaitでデータ取得完了を待つ(SSR時にブロックされる)
const { data: article } = await useFetch(`/api/articles/${route.params.id}`)
 
// データ取得後にmeta設定(この時点でarticle.valueは確実に存在)
if (article.value) {
  useSeoMeta({
    title: article.value.title,
    ogImage: article.value.ogImage ?? 'https://example.com/default-ogp.jpg',
  })
}
</script>

SSRの仕組み:awaitで待機→HTML生成→クローラーに返す、この流れを理解してれば怖くないです。

4-4. 型安全なmeta管理用composable

// ✅ composables/useSeoArticle.ts
export const useSeoArticle = (article: {
  title: string
  excerpt: string
  ogImage?: string
  publishedAt: string
}) => {
  const route = useRoute()
  const url = `https://example.com${route.path}`
 
  useSeoMeta({
    title: article.title,
    description: article.excerpt,
    ogTitle: article.title,
    ogDescription: article.excerpt,
    ogImage: article.ogImage ?? 'https://example.com/default-ogp.jpg',
    ogUrl: url,
    ogType: 'article',
    twitterCard: 'summary_large_image',
  })
 
  useHead({
    link: [{ rel: 'canonical', href: url }],
    script: [
      {
        type: 'application/ld+json',
        children: JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'Article',
          headline: article.title,
          datePublished: article.publishedAt,
          url,
        })
      }
    ]
  })
}

使う側はシンプルに

// pages/articles/[id].vue
const { data: article } = await useFetch(`/api/articles/${route.params.id}`)
if (article.value) {
  useSeoArticle(article.value)
}

composable化するとテストしやすい・再利用できる・型チェックが効くの三拍子です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?