Nuxt 3 國際化 i18n 遭遇 GSC 拒絕索引?Canonical 標籤劫持慘案與完美複盤
在將我的 LINE Flex Message 視覺化平台
FlexCraft
遷移到 Cloudflare 邊緣網路、並全面啟用 Nuxt 3 進行伺服器端渲染(SSR)後,原本以為迎來的是極致的加載速度與跨國流暢度。
然而,兩週後當我打開 Google Search Console(GSC),卻迎來了當頭一棒:GSC 後台跳出了大面積的「驗證失敗」,多個國際化語系路由(如
/ja
、
/zh-TW/tpl
)通通被無情地歸類在
「已抓取 - 尚未編入索引」
(Crawled - currently not indexed)的冷宮中。
作為一個有 10 年經驗的全端架構師,這顯然不能忍。在手撕源碼、調研 Googlebot 抓取行為 2 小時後,我抓到了這個藏在 Nuxt 3 與多語言模組之間的「致命大坑」。今天這篇文章,就來像素級複盤這次的排障過程。
🔍 錯誤現場:全綠的「實時測試」與矛盾的原始碼
遇到「已抓取 - 尚未編入索引」時,第一步通常是使用 GSC 的網址檢查工具。詭異的是,當我點擊右上角的「測試實際版本」(實時測試)時,Google 卻給出了全綠的滿分回饋:
🟢 網址可編入 Google 索引
🟢 網頁可用性完全達標
既然技術層面(robots.txt、Cloudflare 防火牆、WAF 攔截)100% 沒有硬傷,那為什麼之前的正式抓取會被拒絕索引?
當我靜下心來,點開日文版頁面(
/ja
)右鍵檢視網頁原始碼(View Source)時,一行突兀的標籤讓我瞬間破案了:
<link rel="canonical" href="https://liming.me/zh-TW">
<link rel="alternate" hreflang="ja" href="https://liming.me/ja">
💡 病因剖析:當 Canonical 遇上 Hreflang 的「邏輯打架」
這段程式碼在 SEO 底層盒模型中引發了嚴重的邏輯衝突:
hreflang="ja" 告訴 Googlebot:「如果你是日本用戶,請引導他們看 /ja 這個路徑。」
然而,canonical 卻強烈宣告:「不管爬蟲你現在看到的是哪個語系,我的唯一標準(規範)網址都是繁體中文版的 /zh-TW!」
Google 爬蟲極度聽從
canonical
的權威性指引。既然我們在日文頁面上白紙黑字寫著規範網址是中文首頁,Google 抓取完日文頁面後,就會判定「這個日文頁面只是中文頁面的某種重複副產品,不需要獨立佔用搜尋庫索引」。
於是,Google 順從了指令,把所有的索引權重全部合流收斂到了中文首頁,進而直接拒絕索引日文網頁。這就是為什麼頁面流暢度全綠,卻遲遲進不了 Google 搜尋庫的底層邏輯。
🛠️ 根治方案:解除 Nuxt 3 的全域 App 頭部劫持
為什麼
@nuxtjs/i18n
明明設定了
seo: true
和
baseUrl
,編譯出來的
canonical
卻依然被硬編碼綁架?
問題出在
nuxt.config.ts
的全域配置優先權。許多架構師(包括我)為了省事,會在
app.head
中配置預設的元數據。如果在此處或全域的 layout(如
layouts/default.vue
)中,
不小心手動用 useHead 或 useSeoMeta 鎖死了 canonical 欄位
,它的優先權就會直接覆蓋並劫持多語言模組的動態計算。
1. 徹底清除全域硬編碼
首先,全域搜尋專案中的
rel: 'canonical'
,將所有手動寫死、硬編碼指向固定域名的
link
標籤通通刪除,把控制權完全歸還給 i18n 模組。
2. 在
app.vue
中啟用動態頭部接管
最健全(Robust)的全端解法,是在根組件
app.vue
中,利用官方封裝好的
useLocaleHead
組合式函數,動態計算並注入每個路由專屬的 SEO 標籤:
<template>
<div>
<NuxtPage />
</div>
</template>
<script setup>
// 引入官方多語言頭部管理函數
const head = useLocaleHead({
addDirAttribute: true, // 自動添加文字方向屬性(如 ltr)
addSeoAttributes: true // 自動啟用動態完美的 Canonical 與 Hreflang 雙向宣告
})
// 將動態計算好的多語言 SEO 數據交給 useHead 渲染
useHead({
htmlAttrs: {
lang: head.value.htmlAttrs?.lang // 動態切換 lang="zh-TW" 或 lang="ja"
},
link: [
...(head.value.link || [])
],
meta: [
...(head.value.meta || [])
]
})
</script>
🎯 成果驗收與複盤總結
代碼部署到 Cloudflare 邊緣網路並刷新快取後,再次檢視日文頁面的原始碼,標籤終於解開了綁架:
<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 小時,原本卡在排隊隊伍中的多語言路徑順利出線,正式變更為
「已編入索引」
。
💡 這次踩坑給全端開發者的兩點啟示:
不要試圖挑戰模組的底層自動化:在 Nuxt 3 中,既然把 SEO 交給了 @nuxtjs/i18n 或 Nuxt SEO 模組,就要確保全域 app.head 保持乾淨,避免硬編碼引發權限覆蓋。
善用 GSC 的「實時測試」與「源碼比對」:實時測試全綠代表你的伺服器(Server)與渲染(Client)代碼沒有 Bug;而正式抓取失敗則是策略與語意(Semantic)的邏輯錯誤。兩者結合,才是跨國站點排障的正確姿勢。
如果你的 Nuxt 專案也遇到了類似的索引僵局,不妨現在就打開 View Source,看看你的
canonical
標籤是不是也正在悄悄背叛你!