FlexCraft Logo
FlexCraftBETA
模板 Blog

Nuxt 3 國際化 i18n 遭遇 GSC 拒絕索引?Canonical 標籤劫持慘案與完美複盤

發布於 June 2, 2026 3 次閱讀


   在將我的 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 底層盒模型中引發了嚴重的邏輯衝突:


  1.        hreflang="ja" 告訴 Googlebot:「如果你是日本用戶,請引導他們看 /ja 這個路徑。」
       

  2.        然而,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
   標籤是不是也正在悄悄背叛你!