Sitemap 網站地圖

如果你沒接觸過 sitemap,這篇從零講起:sitemap 是什麼、為什麼要做、在 swift-web 裡怎麼用(靜態頁和動態頁分別怎麼進 sitemap)。讀完你就能讓自己的站點被搜尋引擎完整收錄。

一、什麼是 sitemap(先掃盲)

sitemap(網站地圖)是一個列出你網站所有「想被搜尋引擎收錄」頁面 URL 的檔案,通常叫 sitemap.xml,放在站點根目錄(https://你的網域/sitemap.xml)。它就像交給 Google / Bing 的一份「頁面清單」,告訴爬蟲:我這站有哪些頁、在哪、什麼時候更新過。

它長這樣(一段最小的 XML):

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2026-06-23</lastmod>
  </url>
  <url>
    <loc>https://example.com/series/123</loc>
  </url>
</urlset>

為什麼內容站尤其需要它

搜尋引擎主要靠爬蟲順著頁面裡的連結一層層爬來發現頁面。這對內容站有兩個痛點:

  • 頁面太多爬不全:一個影視 / 漫畫站可能有上萬個詳情頁,爬蟲不一定能從首頁的連結全部爬到,沒被發現的頁 = 拿不到自然搜尋流量。
  • 新頁面發現慢:剛上線的詳情頁,爬蟲可能幾天後才順著連結發現。

sitemap 把「我有哪些頁」直接餵給搜尋引擎,繞過「靠連結慢慢爬」,讓收錄更全、更快。對靠搜尋流量賺廣告費的內容站,這是 SEO 的基礎設施,不是可選項。

sitemap 是「幫助發現」,不是「保證收錄 / 排名」。搜尋引擎仍會自己判斷每個頁要不要收、排多少。但沒進 sitemap 的頁,被發現的機率明顯更低。

二、swift-web 怎麼實現:統一一套 @nuxtjs/sitemap

你什麼都不用裝、不用配,sitemap 就已經在運作了。 framework 把 @nuxtjs/sitemap 作為 SEO 套件(sitemap + robots + schema-org)的一部分統一安裝並配置好:

  • 站點網域 —— 設了 NUXT_PUBLIC_SITE_URL 就用它;留空則跟隨使用者實際存取的網域(與 useSeo 的 canonical 同一套邏輯,詳見 SEO 指南)。
  • 預設排除私有路徑 —— /login、/mine/**、/admin/** 等登入態 / 後台頁預設不進 sitemap(這些頁本就不該被搜尋引擎收錄)。
  • 站點名兜底 —— sitemap 人類可讀視圖(瀏覽器直接打開 /sitemap.xml 看到的樣式頁)的標題取 nuxt-site-config 的站點名,framework 會自動從 app.head.title 兜底(缺失時該視圖會顯示 "undefined",僅外觀、不影響爬蟲讀原始 XML)。
  • 自動寫進 robots.txt —— framework 同時裝了 @nuxtjs/robots,會在 robots.txt 自動加一行 Sitemap: https://你的網域/sitemap.xml 指向,無需手動(搜尋引擎讀 robots.txt 時就能發現 sitemap)。

也就是說,swift-web 專案的 sitemap 是單一管線、開箱即用的。

::: tip 不要在 nuxt.config 的 modules 裡再列 @nuxtjs/sitemap framework 已經幫你安裝並配好了。如果你在 nuxt.config.ts 的 modules 陣列裡再裸列一次 '@nuxtjs/sitemap',會重複註冊、可能把 framework 配好的預設值(網域鎖定 / 私有路徑排除)衝掉。也不要自己寫定時任務 / 伺服端路由產生 sitemap.xml——那會和 framework 的這套打架(兩套搶同一個 /sitemap.xml)。統一用 framework 這一套即可。 :::

三、靜態路由頁:自動收錄,零配置

有真實路由檔案的頁面(app/pages/ 下的 index.vue、about-us.vue、terms.vue 等),framework 的 sitemap 會自動發現並收錄,你什麼都不用做。

驗證很簡單 —— 本地起 dev 或部署後,存取 /sitemap.xml:

curl https://你的網域/sitemap.xml

你會看到一個 <urlset>,裡面列著你那些靜態頁的 URL。

想排除某些頁

私有 / 後台頁 framework 已預設排除。要額外排除自己的某些路徑(比如某個臨時活動頁不想被收錄),在 swift.config.json 裡加:

{
  "framework": {
    "seo": {
      "sitemap": { "exclude": ["/campaign-temp", "/preview/**"] }
    }
  }
}

** 是萬用字元(比對該前綴下所有頁)。

四、動態路由頁:用 sources 接入

這是唯一需要你動手的部分。

問題:按後端 ID 產生的動態詳情頁 —— 比如 /series/123、/series/456……(對應 app/pages/series/[id].vue 一個檔案)—— framework 自動發現不了。因為它只知道有 [id].vue 這個路由檔案,不知道後端到底有哪些 id。

解法:把「有哪些動態 URL」做成一個伺服端點(資料源 / source),讓 sitemap 模組來拉。三步:

① 寫一個回傳 URL 清單的端點 server/routes/__sitemap__/urls.ts:

// server/routes/__sitemap__/urls.ts
export default defineSitemapEventHandler(async () => {
  const locs: string[] = []
  let page = 1
  // 分頁拉後端的 ID 清單,直到拉空
  while (true) {
    const ids = await nitroApiFetch<(string | number)[]>('/movie/sitemap', { page: String(page) })
    if (!ids?.length) break
    locs.push(...ids.map((id) => `/series/${id}`))
    page++
  }
  return locs.map((loc) => ({ loc, changefreq: 'weekly' as const, priority: 0.7 }))
})

defineSitemapEventHandler 是 sitemap 模組提供的伺服端 helper(自動匯入,無需 import);nitroApiFetch 是 framework 在「無請求上下文」(如此類建構期 / job 場景)下呼叫後端的工具。/movie/sitemap 是後端約定的「回傳某板塊所有 ID」介面,按你專案的後端契約調整。

⚠️ 端點務必放 server/routes/__sitemap__/(不在 /api/ 下)—— starter 模板自帶 /api/** 的 Nitro proxy + @nuxtjs/robots disallow /api/**;放 /api/ 下的端點會被代理到後端、命不中本 handler,模組拉不到 URL。

② 在 nuxt.config.ts 把這個端點配成 sitemap 資料源:

// nuxt.config.ts
export default defineNuxtConfig({
  sitemap: {
    sources: ['/__sitemap__/urls'],
  },
})

③ 完成。 存取 /sitemap.xml,那上萬個 /series/<id> 就都在裡面了。

starter 模板裡已經放了 server/routes/__sitemap__/urls.ts 的範例(預設回傳空陣列)+ nuxt.config 的註解配置。真接入時把範例裡的拉取邏輯取消註解、按後端契約改、再取消 nuxt.config 裡 sitemap.sources 的註解即可。

必要資料源失敗時停止生成

seo.sitemap.mandatorySources?: string[] 預設未啟用。將必須完整列舉的資料源明確列入後,framework 使用官方 sitemap:sources hook,以不攜帶訪客 cookie / Authorization 的本地 GET 讀取一次,再交原生 sitemap 管線產生 XML。缺少已設定的源、HTTP 失敗、重新導向、逾時或非法 JSON 會拋安全 503,並設定 Cache-Control: no-store;不發布靜態頁殘缺清單,也不另建 XML 或解析器。源必須回傳完整 URL 陣列或 { urls: [...] },不能吞分頁失敗。

// nuxt.config.ts
export default defineNuxtConfig({
  swiftConfig: { seo: { sitemap: { mandatorySources: ['/__sitemap__/urls'] } } },
  sitemap: { sources: ['/__sitemap__/urls'] },
})

mandatorySources 是同站 JSON 源端點的精確名單,不支援外部 URL、query、hash、目錄跳轉或 XML 路徑。它不負責驗證源內部的游標完整性、作品資格或索引狀態;這些由資料源實作並驗收。

本節 API 為本地新增,待 framework#430 發布。消費方須按實際安裝版本進行完整 XML 失敗 / 恢復驗收;本地通過不等於正式環境部署或收錄。

影片 sitemap:條目帶 videos(fw#381)

影視 / 影片站的詳情頁除了 loc,還可以在同一條目上帶 videos,輸出 Google 的 影片 sitemap 擴充(<video:video>)。framework 對此零設定:@nuxtjs/sitemap 原生支援,defineSitemapEventHandler 的回傳型別已含 videos,直接寫就有型別提示。

// server/routes/__sitemap__/urls.ts
export default defineSitemapEventHandler(async () => {
  const items = await nitroApiFetch<MovieRow[]>('/movie/sitemap')
  return items.map((m) => ({
    loc: `/series/${m.id}`,
    videos: [
      {
        title: m.title, // 必填
        description: m.summary, // 必填
        thumbnail_loc: m.seo.img, // 必填;必須是搜尋引擎能直接取的普通圖,**不能給 .bnc 加密圖**
        player_loc: `https://your.site/series/${m.id}`, // 與 content_loc 至少給一個;播放頁 URL
        duration: m.durationSeconds, // 可選,單位**秒**(JSON-LD 的 VideoObject 是 ISO 8601,這裡不是)
        publication_date: m.publishedAt, // 可選,ISO 8601
      },
    ],
  }))
})

要點:

  • title / description / thumbnail_loc 三項必填,content_loc(影片檔)或 player_loc(播放頁)至少一個;簽名 / 會過期的 m3u8 別當 content_loc,給 player_loc 即可
  • duration 單位是秒;expiration_date / rating / view_count / family_friendly / restriction 等欄位按 Google 規範原名傳,見 VideoEntry 型別
  • 與 useSeo({ video }) 的 VideoObject 是同一份事實的兩種輸出:sitemap 告訴爬蟲「有哪些影片頁」,JSON-LD 告訴它「這頁的影片是什麼」,兩邊的標題 / 封面 / 時長要一致
  • 一條 URL 可帶多個影片(多集同頁時);不帶 videos 的條目照舊只出普通 <url>

量大了會自動拆分

單個 sitemap 檔案有上限(見下方「協定速查」:5 萬個 URL / 50MB)。超了 @nuxtjs/sitemap 會自動拆成多個子 sitemap + 一個索引檔案(/sitemap_index.xml),你不用操心分片邏輯。你那 ~1 萬級別的量級,模組在執行時產生完全夠用。

::: tip 不再需要舊式「部署期產生靜態 XML」 有些老專案的做法是:部署腳本裡呼叫後端 API、把所有 URL 寫成靜態 .xml 檔案,再讓伺服器 serve 這些靜態檔案。在 swift-web 裡不需要這麼做了 —— sitemap.sources 讓模組在請求時即時從你的端點取資料、自動分片 + 產生索引,少寫一大堆 cron / 分片 / 索引程式碼。

舊式:部署期產生靜態 XMLswift-web:sitemap.sources
何時產生部署腳本呼叫後端 → 寫死靜態檔案模組在請求 / 預渲染時呼叫你的端點
誰管分片 / 索引自己寫模組自動
內容新鮮度要重新部署才更新端點即時反映後端
程式碼量多(cron + 分片 + 索引 + serve)少(一個端點 + 一行配置)

(極致想省執行時開銷,@nuxtjs/sitemap 也支援預渲染成靜態檔案,但 ~1 萬級別沒必要。) :::

五、協定規範速查(業界標準)

sitemap 是有公開協定的(sitemaps.org 0.9,各大搜尋引擎通用)。日常你靠 framework 自動產生、不用手寫,但了解這些規則有助於判斷「產生對不對」:

規則數值 / 說明
單檔 URL 上限50,000 個
單檔大小上限50MB(未壓縮)
超過上限拆多個子 sitemap + 一個 <sitemapindex> 索引(模組自動)
壓縮支援 gzip(.xml.gz),解壓後仍須 ≤ 50MB
<loc>頁面 URL,必填,≤ 2048 字元
<lastmod>最後修改時間(W3C 日期格式,如 2026-06-23),Google 會用、但要求真實準確
<changefreq> / <priority>更新頻率 / 優先級,Google 基本忽略,別指望靠它們影響排名

關鍵認知:priority / changefreq 是協定裡的「建議欄位」,但 Google 現在基本不看;它真正參考的是 lastmod(且必須真實,亂填反而失去信任)。所以別在這兩個欄位上花心思,把 lastmod 填準更有價值。

六、提交給搜尋引擎

產生了 sitemap,還要讓搜尋引擎知道它的存在。兩個途徑(swift-web 已幫你做了第一個):

  1. robots.txt 指向(自動)—— framework 的 robots 整合已在 robots.txt 寫好 Sitemap: https://你的網域/sitemap.xml,搜尋引擎抓 robots.txt 時即可發現。存取 /robots.txt 可確認。
  2. 搜尋引擎站長後台(手動,推薦)—— 在 Google Search Console / Bing Webmaster Tools 裡提交 sitemap URL。這是最權威的方式,還能看到「提交了多少 / 收錄了多少 / 哪些報錯」。上線後建議手動提交一次。

七、上線自檢清單

  • 存取 /sitemap.xml 回傳 200 + <urlset>(或多 sitemap 時的 /sitemap_index.xml)
  • 靜態頁(首頁 / about / terms 等)都在裡面
  • 私有頁(login / mine / admin)不在裡面
  • 動態詳情頁:若需收錄,已配 sitemap.sources + 端點回傳正確 URL
  • 存取 /robots.txt 能看到 Sitemap: 指向
  • 有 prerender 頁的專案已設 NUXT_PUBLIC_SITE_URL(建置期沒有真實請求,只能用設定值);無 prerender 頁時留空是合法的,sitemap 會跟隨存取網域
  • 上線後在 Google Search Console 手動提交一次

關聯閱讀