header_logo

用 Nuxt Server Routes 打造動態 Sitemap:從靜態到自動化的完整實作

JordanTTC

為什麼需要動態 Sitemap?

在我們接的專案中,像是文化機構網站或企業官網,經常會有以下情況:

  • 內容持續更新: 新聞、展覽、活動等內容不斷新增
  • 多語系支援: 同一內容可能有多個語言版本
  • 大量動態頁面: 透過 API 產生的頁面數量可能達到數百甚至數千頁
  • 即時性需求: 希望搜尋引擎能快速收錄新內容

如果使用靜態生成的方式,每次內容更新就需要重新 build 並部署,不僅耗時也容易出錯。動態 sitemap 則能在每次訪問時自動抓取最新資料,完美解決這個痛點。

Nuxt Server Routes 的優勢

Nuxt 3 提供了 Server Routes 功能,讓我們可以在 server/routes/ 目錄下建立 API 端點或動態檔案生成器。相較於其他方案:

  • 無需額外套件: 使用 Nuxt 內建功能,減少依賴
  • 完全控制: 可以自由決定快取策略、資料來源和生成邏輯
  • 與專案整合: 直接使用專案的 runtime config 和 API 設定
  • 效能優化: 可以實作記憶體快取,避免每次請求都重新計算

核心實作架構

1. 檔案結構

server/routes/ ├── sitemap.xml.ts # Sitemap 生成器

當使用者訪問 https://your-domain.com/sitemap.xml 時,Nuxt 會自動執行 sitemap.xml.ts 中的程式碼。

2. 核心流程

typescript /** * 動態生成 Sitemap * 訪問路徑:/sitemap.xml */ export default defineEventHandler(async (event) => { // 1. 檢查快取 if (cache && Date.now() - cacheTimestamp < CACHE_DURATION) { return cache } // 2. 準備靜態頁面 const staticUrls = [ { loc: '/', lastmod: new Date().toISOString() }, { loc: '/about', lastmod: new Date().toISOString() }, // ... 更多靜態頁面 ]

// 3. 從 API 取得動態內容 const dynamicUrls = await fetchAllPages()

// 4. 組合並生成 XML const xml = generateSitemapXml([...staticUrls, ...dynamicUrls])

// 5. 更新快取並回傳 updateCache(xml) return xml })

3. 關鍵功能實作

A. 快取機制

我們實作了兩層快取:

typescript // Server-side 記憶體快取 let cache: string | null = null let cacheTimestamp: number = 0 const CACHE_DURATION = 86400000 // 24 小時 // Browser 快取 setHeader(event, 'Cache-Control', 's-maxage=86400, stale-while-revalidate') setHeader(event, 'X-Cache', fromCache ? 'HIT' : 'MISS')

這樣做的好處:

  • 減少對後端 API 的請求壓力
  • 加快回應速度
  • 透過 X-Cache header 可以監控快取效果
B. 分頁處理

當 API 回傳的資料需要分頁時:

typescript async function fetchAllPages() { const allItems = [] let currentPage = 1 let hasMore = true while (hasMore) { const response = await $fetch(${apiUrl}?page=${currentPage}&pageSize=100) const items = response?.data?.content || [] allItems.push(...items)

const totalPages = response?.data?.pagination?.totalPages || 1 hasMore = currentPage < totalPages currentPage++ }

return allItems.map(item => ({ loc: /article/${item.id}, lastmod: item.updatedAt })) }

C. 錯誤處理

實務上,API 可能會失敗或回傳不完整的資料:

typescript try { const dynamicUrls = await fetchAllPages() allUrls.push(...dynamicUrls) } catch (error) { console.error('Failed to fetch dynamic content:', error) // 即使 API 失敗,仍然回傳靜態頁面的 sitemap }
D. XML 轉義

處理使用者輸入的內容時,必須進行 XML 轉義:

typescript function escapeXml(unsafe: string): string { return unsafe .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;') }

4. 完整的 Sitemap XML 格式

typescript const xml = <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> ${items.map(item => <url> <loc>${escapeXml(${baseUrl}${item.loc})}</loc> <lastmod>${item.lastmod}</lastmod> <changefreq>${item.changefreq || 'weekly'}</changefreq> <priority>${item.priority || '0.5'}</priority> </url>).join('\n')} </urlset>

實際應用場景

1. 多語系網站

typescript const languages = ['zh-TW', 'en', 'ja'] const dynamicContent = await fetchAllPages() const multilingualUrls = languages.flatMap(lang => dynamicContent.map(item => ({ loc: /${lang}/article/${item.id}, lastmod: item.updatedAt })) )

2. 分類頁面

typescript const categories = await $fetch('/api/categories') const categoryUrls = categories.map(cat => ({ loc: /category/${cat.slug}, lastmod: cat.updatedAt, changefreq: 'daily', priority: '0.8' }))

3. 優先級設定

typescript const urls = [ { loc: '/', priority: '1.0', changefreq: 'daily' }, { loc: '/about', priority: '0.8', changefreq: 'monthly' }, ...dynamicUrls.map(url => ({ ...url, priority: '0.6', changefreq: 'weekly' })) ]

效能優化建議

1. 控制快取大小

typescript const MAXCACHESIZE = 5 * 1024 * 1024 // 5 MB if (xml.length < MAXCACHESIZE) { cache = xml cacheTimestamp = Date.now() }

2. API 超時處理

typescript const response = await $fetch(apiUrl, { timeout: 10000 // 10 秒超時 })

3. 分批處理大量資料

如果頁面數量超過 50,000 筆,建議使用 Sitemap Index:

typescript // sitemap-index.xml.ts const xml = <?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>${baseUrl}/sitemap-static.xml</loc> </sitemap> <sitemap> <loc>${baseUrl}/sitemap-articles.xml</loc> </sitemap> <sitemap> <loc>${baseUrl}/sitemap-products.xml</loc> </sitemap> </sitemapindex>

部署注意事項

1. 環境變數設定

typescript // nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { public: { APP_URL: process.env.APP_URL || 'https://your-domain.com', APP_API: process.env.APP_API || '' } } })

2. 監控和日誌

typescript console.log([Sitemap] Generated with ${items.length} URLs) console.log([Sitemap] Cache status: ${fromCache ? 'HIT' : 'MISS'}) console.log([Sitemap] XML size: ${(xml.length / 1024).toFixed(2)} KB)

3. robots.txt 設定

別忘了在 public/robots.txt 中加入:

User-agent: * Allow: / Sitemap: https://your-domain.com/sitemap.xml

測試與驗證

1. 本地測試

bash npm run dev // http://localhost:3000/sitemap.xml

2. 檢查快取效果

觀察 Console 輸出或 Response Headers 中的 X-Cache 欄位。

3. XML 格式驗證

使用 Google Search Console 的 Sitemap 測試工具,或線上 XML 驗證器。

4. 效能測試

bash // 測試回應時間

curl -w "@curl-format.txt" -o /dev/null -s https://your-domain.com/sitemap.xml


可復用的模板和 MD 檔案

為了讓團隊其他成員也能快速實作類似功能,我們整理了一個通用模板,以及給 ai 讀取的 md 檔案。

  1. API 端點: 改成你的實際 API 路徑
  2. 靜態頁面列表: 加入專案的靜態頁面
  3. XML 結構: 根據需求調整欄位
  4. 快取設定: 依據更新頻率調整時間

這樣每次有新專案需要 sitemap 時,只要複製模板檔案並調整設定,給 ai 生調整一下細節,

不到 10 分鐘就能完成。

總結

使用 Nuxt Server Routes 實作動態 sitemap 的優點:

即時性: 內容更新後立即反映在 sitemap

自動化: 無需手動維護或重新 build

彈性: 完全掌控生成邏輯和快取策略

效能: 透過記憶體快取達到毫秒級回應

可維護: 程式碼集中管理,易於修改和擴充

這個方案已經在我們多個正式專案中穩定運行,包括政府機關網站和大型企業官網。如果你也在用 Nuxt 3 開發需要 SEO 的專案,不妨試試這個做法!

延伸閱讀
回到列表

隨時掌握設計趨勢

訂閱即表示您同意我們的隱私政策,並同意接收我們公司的更新。

Copyright © WebCRACK. All rights reserved.

版塊創意有限公司

| hello@webcrack.tw

| 統編:83197129