Nuxt 4のuseHeadでタイトルが消える?SSR時の「評価タイミング」を理解して堅牢なコードを書く
こんにちは、フロントエンドエンジニアのてりーです。
本記事はVue/Nuxtを扱っていますが、Reactの書籍を2026年1月に大幅アップデートしました。
200冊以上売れているので、フロントエンド初学者やReactに興味ある人は、ぜひ手に取って下さい!
はじめに
Nuxt4で useHead を使用して動的にページタイトルを設定する際、nuxt.config.ts で定義した titleTemplate が期待通りに動かず、タイトルが空(またはサイト名のみ)になってしまうケースがあります。
一見シンプルなバグですが、実はSSR(サーバーサイドレンダリング)環境におけるメタデータの評価タイミングが深く関わっています。
本記事では、この事象の技術的背景と、確実性を重視した設計判断について解説します。
1. 前提知識:なぜ useHead を使うのか?
まず、なぜ私たちが当たり前のようにuseHeadを使うのか、そのSEO的なメリットを整理しておきましょう。
useHeadに関するドキュメントはこちらです。
① SSR(サーバーサイドレンダリング)対応
ブラウザがJavaScriptを実行する前の「素のHTML」にメタタグを書き込むため、クローラーが正しくコンテンツを巡回できるようになる
② 動的なメタ制御
記事タイトルや商品名をリアルタイムに反映し、SNSシェア(OGP)時に適切な情報を表示させることでクリック率(CTR)を向上させます。
③ タグの重複排除
複数のコンポーネントでuseHeadが呼ばれても、Nuxtが適切にマージし、<head> 内がタグだらけになるのを防ぎます。
基本的な書き方
通常、nuxt.config.tsでサイト全体の共通ルール(テンプレート)を決め、各ページでタイトルを差し込みます。
export default defineNuxtConfig({
app: {
head: {
titleTemplate: '%s | サンプルサイト'
}
}
})
<script setup>
useHead({
title: 'プロフィール' // 「プロフィール | サンプルサイト」として出力される
})
</script>
2. リアクティブな値をタイトルを渡すと「 | サイト名」になる
しかし、エラーページ(error.vue)などで ComputedRefを使って動的にタイトルを出そうとすると、SSR後のHTMLで以下のような表示になることがあります。
- 期待: ページが見つかりません | サンプルサイト
- 現実: | サンプルサイト (%s の部分が空、あるいは初期値のまま評価される)
この問題は、特にエラーページなど、ライフサイクルが特殊な場面や、プロパティの解決にわずかなラグが生じるケースで顕著に発生します。
3. 技術的考察:Nuxtの裏側「Unhead」の仕業
この挙動を理解するには、Nuxtのメタデータ管理を支える内部ライブラリUnheadの存在を知る必要があります。
💡 Unheadとは?
Nuxt3から採用された、軽量・高速なメタデータ管理ライブラリです。
VueやNuxtから「このタイトルにしてね」という依頼を受け取り、最終的なHTMLタグを生成する役割を担っています。
SSRにおける「評価タイミング」のズレ
Unheadは、SSRのパフォーマンスを最大化するため、「全コンポーネントの setup() が終わった直後」にメタデータを確定(シリアライズ)させるという仕様を持っています。
- setup() 実行: useHead に ComputedRef が渡される。
- Unheadの確定処理: HTMLを書き出すために現在の title の値を評価する。
- タイミングの隙間: この「確定の瞬間」に、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要件にも動じない対応力が身につきます。
参考文献
Discussion