【Nuxt 3】i18n導入後にGSCで「クロール済み - インデックス未登録」になる原因と解決策 | Canonicalタグの競合を修正する
LINEメッセージの視覚化自動化プラットフォーム [FlexCraft] を、極致的な配信速度とグローバル対応のために Cloudflare エッジネットワーク(Nuxt 3 + SSR)へ移行した後のことです。
デプロイから2週間後、Google Search Console(GSC)を開くと、予期せぬ一撃に見舞われました。多言語展開している一部のルート(例:/ja や /zh-TW/tpl)が、すべて 「クロール済み - インデックス未登録」(Crawled - currently not indexed)という冷遇モードに分類されていたのです。
フルスタックエンジニアとして、このまま放置するわけにはいきません。ソースコードを深く掘り下げ、Googlebotの挙動を2時間調査した結果、Nuxt 3と多言語(i18n)モジュールの間に潜む「致命的な罠」を特定しました。本記事では、このトラブルシューティングのプロセスをピクセル単位で詳細に復盤(レトロスペクティブ)します。
🔍 エラーの現場:合格する「公開URLテスト」と矛盾するソースコード
GSCで「クロール済み - インデックス未登録」に遭遇した場合、最初のステップは「URL検査」ツールです。奇妙なことに、右上にある「公開URLのテスト」(リアルタイムテスト)をクリックすると、Googleはオールグリーンの満点評価を返してきました。
- 🟢 URLはインデックスに登録できます
- 🟢 ページのユーザビリティは完全に合格しています
インフラや技術的なレイヤー(robots.txt、Cloudflare WAFによる遮断など)には100%問題がないことが証明されました。では、なぜ過去の正式なクロール時にインデックス登録が拒絶されたのでしょうか?
落ち着いて日本語版ページ(/ja)をブラウザで開き、右クリックから「ページのソースを表示」した瞬間、原因が判明しました。そこには、互いに矛盾し合うタグが生成されていたのです。
<!-- 現在のアクセスURLは https://liming.me/ja -->
<link rel="canonical" href="https://liming.me/zh-TW">
<link rel="alternate" hreflang="ja" href="https://liming.me/ja">
💡 病因の解剖:Canonical と Hreflang の「論理的な競合」
この数行のコードは、SEOのボックスモデルの根幹において深刻な論理バグを引き起こしていました。
hreflang="ja" の主張:「日本のユーザーには、この/jaルートを見せてください。」しかし、canonical の強力な上書き宣告:「クローラーが今どの言語を見ていようが関係なく、このサイトの唯一の標準(正規化)URLは、繁体字中国語版の/zh-TWである!」
Googleのクローラーは canonical タグの権威的な指示に極めて忠実です。日本語ページにおいて「正規化URLは中国語トップページである」と白紙黒字で宣言されているため、Googlebotは「この日本語ページは中国語ページの重複した副産物に過ぎず、検索インデックスを個別に割り当てる価値はない」と判定します。
その結果、Googleは指示に従ってすべてのインデックス評価を中国語トップページに集約し、日本語ルートのインデックス登録を直接拒絶しました。これこそが、ユーザビリティがオールグリーンであるにもかかわらず、Google検索に一向に表示されなかった真のロジックです。
🛠️ 根本的な解決策:Nuxt 3 の全域 App ヘッドによる「ハイジャック」を解除する
なぜ @nuxtjs/i18n モジュールで seo: true と baseUrl を正しく指定しているにもかかわらず、生成される canonical がハードコーディングされたように固定されてしまうのでしょうか?
原因は、nuxt.config.ts におけるグローバル設定の優先順位にあります。多くの開発者は初期設定時、便利さのために app.head 内にデフォルトのメタデータを配置します。もし、この場所やグローバルなレイアウトファイル(例:layouts/default.vue)の中で、不用意に useHead や useSeoMeta を使って canonical フィールドを固定(ハードコーディング)してしまっていると、その優先度がi18nモジュールの動的計算ロジックを上書きし、完全にハイジャックしてしまうのです。
1. グローバルなハードコーディングの徹底排除
まず、プロジェクト全体を rel: 'canonical' や canonical: で検索し、特定のドメインを決め打ちして指定している古いコードをすべて削除します。ヘッダーの制御権を完全にi18nモジュールへ返還します。
2. app.vue で動的ヘッダー割り当てを実装する
最も堅牢(Robust)な解決策は、ルートコンポーネントである app.vue(または共通レイアウト)内で、公式にラップされている useLocaleHead コンポーザブル関数を呼び出し、現在のルートに応じたSEOタグを動的に計算して注入することです。
<template>
<div>
<NuxtPage />
</div>
</template>
<script setup>
// 1. 公式の多言語ヘッド管理関数を呼び出す
const head = useLocaleHead({
addDirAttribute: true, // テキスト方向(ltrなど)を自動追加
addSeoAttributes: true // 動的で完璧な Canonical と Hreflang の双向宣言を有効化
})
// 2. 動的に計算されたSEOデータを useHead に渡してレンダリング
useHead({
htmlAttrs: {
lang: head.value.htmlAttrs?.lang // lang="zh-TW" や lang="ja" を動的に切り替え
},
link: [
...(head.value.link || [])
],
meta: [
...(head.value.meta || [])
]
})
</script>
🎯 成果の検証とまとめ
コードをCloudflareエッジにデプロイし、キャッシュをパージした後、再度日本語ページのソースコードを確認すると、ついにハイジャック状態が解消されていました。
<!-- 完璧な国際化SEOヘッダー構造 -->
<link rel="canonical" href="https://liming.me/ja">
<link rel="alternate" hreflang="zh-TW" href="https://liming.me/zh-TW">
<link rel="alternate" hreflang="ja" href="https://liming.me/ja">
この状態で Google Search Console に戻り、再度「インデックス登録をリクエスト」をクリックします。すると48時間以内に、これまで順番待ちの列でスタックしていた多言語ルートが次々と通過し、ステータスが正式に 「インデックス登録済み」 へと変更されました。
💡 今回のトラブルから得られた全端開発者への2つの教訓:
- モジュールの自動化に中途半端に干渉しない:Nuxt 3において、SEOの制御を
@nuxtjs/i18nやNuxt SEOに委ねることを決めたなら、全域のapp.headはクリーンに保ち、ハードコーディングによる設定の上書き競合を避けるべきです。 - GSCの「公開URLテスト」と「ソースコード比較」を併用する:リアルタイムテストの合格は、サーバー(Server)とクライアント(Client)のレンダリングコードに技術的なバグがないことを意味します。一方で、実際のインデックス登録の失敗は、戦略とセマンティクス(Semantics)の論理的エラーを意味します。この2つを組み合わせて分析することこそが、グローバルサイトにおける正しい障害調査の姿勢です。
もし、あなたのNuxtプロジェクトも原因不明のインデックス停滞に陥っているなら、今すぐブラウザで「ページのソースを表示」してみてください。もしかすると、あなたの canonical タグが、裏でこっそりサイトを裏切っているかもしれません!
🚀 LINE Flex Messageの開発効率を10倍にする可視化プラットフォームに興味がある方は、ぜひ私のオープンソースリポジトリもチェックしてみてください:GitHub: line-flex-message-templates