跳至主要內容

開發紀錄

靜態 HTML、canonical、hreflang 與 sitemap 如何一起成立

從目前的靜態輸出與驗證腳本,整理頁面正文、主要網址、語言替代連結和 sitemap 如何使用同一套公開路由契約。

林予晴
發布
  • Astro
  • 靜態網站
  • SEO 基礎
  • 路由驗證

靜態網站不是把 HTML 產出來就結束了。對一個有三種語言、舊網址相容入口和 Markdown 筆記的網站來說,至少還要回答四個彼此相關的問題:讀者拿到的 HTML 是不是這一頁的內容、主要網址是哪一個、其他語言版本是否真的存在,以及 sitemap 要列出哪些網址。

這四件事如果各自維護,很容易出現互相矛盾的結果。頁面看起來是英文,canonical 卻指向繁中;語言切換顯示一個不存在的文章翻譯;sitemap 列出草稿,正式路由卻沒有那一頁。主要判斷是:canonical、hreflang、sitemap 和靜態 HTML 必須從同一份實際路由與發布資料推導,不能把它們當成四份獨立的文案。

先確認 HTML 裡真的有頁面內容

目前的網站使用 Astro 靜態輸出。頁面元件在建置時執行,首頁、固定頁、筆記列表和正式文章會直接產生 HTML。這個結果和只有一個 #app 掛載點的瀏覽器外殼不同:讀者第一次取得文件時,就應該能看到標題、主要內容與頁面資訊。

產物檢查會逐頁確認幾個基本條件:

  • HTML 有正確的 lang。
  • 頁面只有一個主要 h1。
  • 有可定位的主要內容區域。
  • 有 description、Open Graph title 和 description。
  • 頁面不是依賴 hydrated framework 才能顯示正文。
  • 參照到的本地資源確實存在。

這些條件先確認「這個網址回傳的文件是可讀的頁面」,之後才有意義討論它要被搜尋引擎或分享服務如何理解。canonical 不能挽救一份沒有正文的 HTML;sitemap 也不能代替頁面本身。

canonical 只在可索引頁面上出現

共用 layout 會依頁面是否可索引,決定要不要輸出 canonical、語言替代連結、og:url 和結構化資料。一般正式頁會使用目前語言對應的網址;舊的 /work 則把 canonical 指向目前主要的 /about/work。

這裡有一個容易被忽略的界線:不是每個回傳 HTML 的頁面都應該宣稱自己是正式索引頁。

  • 正式固定頁和已發布文章:輸出 canonical。
  • dev-only 文章預覽:輸出 noindex, nofollow,不宣稱正式網址。
  • 404 頁:輸出 noindex,不把自己 canonical 到首頁。
  • 舊相容路由:保留可讀入口,但 canonical 指向主要路由。

如果預覽頁也輸出正式 canonical,校稿網址就會看起來像正式內容;如果 404 頁 canonical 到首頁,未知網址可能被錯誤描述成首頁的另一個入口。這些不是 SEO 文案問題,而是頁面身份沒有分清楚。

hreflang 要描述存在的替代頁

固定頁的語言替代連結可以從語言集合和固定頁路由產生。繁中 /about、英文 /en/about、日文 /ja/about 都有實際頁面,所以它們可以互相列為替代版本。

文章頁則不同。文章版型收到的 translations 是同一篇內容目前存在的翻譯集合,替代連結從這份集合產生。沒有英文文章,就不應該在中文文章的 head 裡放一個猜出來的 /en/notes/...。

這個差異讓 hreflang 的責任很清楚:它不是語言清單,也不是「未來可能會有的頁面」清單,而是目前可供讀者切換的實際替代網址。多語固定頁可以全量列出;多語文章要依內容資料列出。

sitemap 只列公開輸出

sitemap 不是把 src/content/notes/ 裡所有檔案掃一遍就輸出。它會先取得符合發布條件的文章,再加入:

  • 各語言的固定頁。
  • 正式筆記文章頁。
  • 內容量足夠時的筆記分頁網址。

草稿、沒有 publishedAt 的文章、發布日期尚未到的文章和不存在的翻譯,都不應該出現在 sitemap。舊的 /work 相容入口也不列入 sitemap,因為它的責任是接住既有連結,不是成為新的主要索引網址。

目前固定頁 URL 和文章 URL 使用不同的產生方式,但公開資格仍然要一致:只有真正會在正式 build 產生的頁面,才有理由進入 sitemap。

驗證腳本在檢查什麼

這個專案把驗證拆成不同範圍,而不是只跑一次 build:

驗證範圍 它確認的事情
靜態輸出檢查 固定頁、相容路由、404、HTML 內容、metadata、語言標記和本地資源
筆記輸出檢查 已發布文章的頁面、canonical、sitemap,以及草稿和未公開正文沒有洩漏
Content Collection 測試 日期、slug、stable ID、發布過濾和 dev-only 預覽等資料契約
build 目前程式能否產生預期的靜態路由與資產

這些檢查彼此不能互相取代。build 成功不會自動證明 sitemap 沒有草稿;sitemap 正確也不會自動證明手機頁面的內容順序舒服。每個檢查只應該支持它實際涵蓋的結論。

404 和預覽要有不同的 head

正式內容、預覽和錯誤頁都有 HTML,但它們的公開身份不同。這也是為什麼 layout 會把 notFound、noindex 和正式 metadata 分開處理。

文章預覽需要讓作者看得到標題、正文和撰稿日期,卻不應該被正式輸出、sitemap 或搜尋索引當成已發布內容。404 則需要提供可讀錯誤訊息和恢復導覽,但不應該提供首頁的 canonical、Open Graph URL 或結構化資料。

這種分支不是為了把 head 寫得更複雜,而是把「讀者看得到」和「網站把它宣告為正式公開頁」分成兩件事。預覽可以可讀,並不代表它是公開內容;錯誤頁可以有導覽,並不代表它是正常路由。

這套契約能證明什麼,不能證明什麼

目前的程式和靜態檢查可以支持以下判斷:

  • 正式固定頁和已發布文章會產生可讀 HTML。
  • canonical 依主要路由輸出,舊相容網址會指向新的主要路由。
  • 固定頁與實際存在的文章翻譯使用不同的替代連結來源。
  • sitemap 只從固定頁和正式發布集合建立。
  • 草稿預覽與 404 不會沿用正式頁面的索引 metadata。

但這些證據不能直接推出搜尋排名、收錄速度、社群分享表現或所有爬蟲都會如何處理頁面。它們證明的是輸出契約成立,不是外部平台已經給了什麼結果。

寫靜態多語網站時的檢查順序

  1. 先確認每個公開網址是否真的產生了對應 HTML。
  2. 再決定頁面的主要網址與相容網址責任。
  3. 只為實際存在的替代頁輸出 hreflang。
  4. 讓 sitemap 讀取和正式頁面相同的公開集合。
  5. 把預覽與 404 的 noindex 規則獨立出來。
  6. 用產物檢查讀回 HTML、sitemap 和資源,不只看原始元件。
  7. 把機器檢查、瀏覽器操作和站主的主觀驗收分開回報。

靜態輸出的價值不只是「伺服器不用現場組頁面」。它也要求每一個公開網址、頁面 metadata 和內容狀態在建置時先對得上。只要其中一層仍然靠猜,網站看起來有很多頁,公開身份卻可能是混亂的。

紀錄範圍

本文整理目前固定頁、筆記文章、預覽、404、canonical、hreflang 和 sitemap 的實作與輸出檢查。它沒有宣稱搜尋引擎排名、索引結果、分享平台抓取或使用者理解效果已被量測。