👻

Nuxt 4のuseHeadでタイトルが消える?SSR時の「評価タイミング」を理解して堅牢なコードを書く

に公開

こんにちは、フロントエンドエンジニアのてりーです。
本記事はVue/Nuxtを扱っていますが、Reactの書籍を2026年1月に大幅アップデートしました。

200冊以上売れているので、フロントエンド初学者やReactに興味ある人は、ぜひ手に取って下さい!
https://zenn.dev/gunners6518/books/4c4672f32dd100

はじめに

Nuxt4で useHead を使用して動的にページタイトルを設定する際、nuxt.config.ts で定義した titleTemplate が期待通りに動かず、タイトルが空(またはサイト名のみ)になってしまうケースがあります。

一見シンプルなバグですが、実はSSR(サーバーサイドレンダリング)環境におけるメタデータの評価タイミングが深く関わっています。
本記事では、この事象の技術的背景と、確実性を重視した設計判断について解説します。

1. 前提知識:なぜ useHead を使うのか?

まず、なぜ私たちが当たり前のようにuseHeadを使うのか、そのSEO的なメリットを整理しておきましょう。

useHeadに関するドキュメントはこちらです。
https://nuxt.com/docs/4.x/api/composables/use-head

① SSR(サーバーサイドレンダリング)対応

ブラウザがJavaScriptを実行する前の「素のHTML」にメタタグを書き込むため、クローラーが正しくコンテンツを巡回できるようになる

② 動的なメタ制御

記事タイトルや商品名をリアルタイムに反映し、SNSシェア(OGP)時に適切な情報を表示させることでクリック率(CTR)を向上させます。

③ タグの重複排除

複数のコンポーネントでuseHeadが呼ばれても、Nuxtが適切にマージし、<head> 内がタグだらけになるのを防ぎます。

基本的な書き方

通常、nuxt.config.tsでサイト全体の共通ルール(テンプレート)を決め、各ページでタイトルを差し込みます。

nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s | サンプルサイト' 
    }
  }
})
pages/about.vue
<script setup>
useHead({
  title: 'プロフィール' // 「プロフィール | サンプルサイト」として出力される
})
</script>

2. リアクティブな値をタイトルを渡すと「 | サイト名」になる

しかし、エラーページ(error.vue)などで ComputedRefを使って動的にタイトルを出そうとすると、SSR後のHTMLで以下のような表示になることがあります。

  • 期待: ページが見つかりません | サンプルサイト
  • 現実: | サンプルサイト (%s の部分が空、あるいは初期値のまま評価される)

この問題は、特にエラーページなど、ライフサイクルが特殊な場面や、プロパティの解決にわずかなラグが生じるケースで顕著に発生します。

3. 技術的考察:Nuxtの裏側「Unhead」の仕業

この挙動を理解するには、Nuxtのメタデータ管理を支える内部ライブラリUnheadの存在を知る必要があります。

💡 Unheadとは?

Nuxt3から採用された、軽量・高速なメタデータ管理ライブラリです。
VueやNuxtから「このタイトルにしてね」という依頼を受け取り、最終的なHTMLタグを生成する役割を担っています。

https://github.com/unjs/unhead/tree/main

SSRにおける「評価タイミング」のズレ

Unheadは、SSRのパフォーマンスを最大化するため、「全コンポーネントの setup() が終わった直後」にメタデータを確定(シリアライズ)させるという仕様を持っています。

  1. setup() 実行: useHead に ComputedRef が渡される。
  2. Unheadの確定処理: HTMLを書き出すために現在の title の値を評価する。
  3. タイミングの隙間: この「確定の瞬間」に、Vueのリアクティブな計算(Computed)が完了しておらず、値が undefined だと、空文字のままテンプレートが適用されてしまいます。

4. 実装の対比

「動かない」を解決するだけでなく、「なぜこれを選ぶのか」という設計判断の差を見てみましょう。

【Before】バグが発生しやすい実装

<script setup lang="ts">
const error = useError()

// ❌ Computedに依存しているため、SSR時にUnheadが評価するタイミングで
// まだ値が確定していないと、%s が空のままテンプレートが適用される
const errorTitle = computed(() => {
  return error.value?.statusCode === 404 ? 'ページが見つかりません' : 'エラーが発生しました'
})

useHead({
  title: errorTitle // nuxt.config の titleTemplate に依存
})
</script>

【After】確実性を担保する実装

<script setup lang="ts">
const error = useError()

const getErrorTitle = () => {
  if (error.value?.statusCode === 404) return 'ページが見つかりません'
  return `エラー ${error.value?.statusCode || ''}`
}

// ✅ 確実なパターン
useHead({
  // 1. computedの「値」をその場で評価し、静的な文字列として渡す
  title: `${getErrorTitle()} | サービス名`,
  
  // 2. titleTemplateをnullにして、グローバルの不確実な挙動を遮断する
  titleTemplate: null
})
</script>

5. なぜ「あえて静的な文字列」を選ぶのか

「リアクティブに書けるなら、すべて computed にすべき」という考えは、SSR環境ではリスクになることがあります。特に以下の2点は重要です。

異常系こそシンプルに

エラーページは、システムが不安定な時に動くページです。そこはフレームワークの複雑な魔法(リアクティブ)に頼らず、JSによる単純な文字列結合という「壊れにくく、予測可能」な手法を優先すべきです。

SSRは一発勝負

クライアントサイドと違い、SSRのHTML生成はやり直しが効きません。Unheadに「完成品(文字列)」を渡すのは、エンジニアとしての優しさです。

6. まとめ

  • titleTemplate は便利だが、SSR環境では評価タイミングのズレが起きる可能性がある。
  • 特にエラーページや非同期データが絡む場面では、ComputedRef が解決される前にHTMLが固定されるリスクを意識する。
  • 「異常系こそシンプルに」。確実性を求めるなら、useHead 内でテンプレートを使わず、直接フルタイトルを生成するのがシニアの選定です。

内部構造(Unhead)を意識できるようになると、Nuxt 4へのアップデートや、より複雑なSEO要件にも動じない対応力が身につきます。

参考文献

https://nuxt.com/docs/4.x/getting-started/seo-meta

https://nuxt.com/docs/4.x/api/composables/use-head

https://unhead.unjs.io/docs/typescript/head/guides/get-started/intro-to-unhead/

Discussion