開發紀錄
靜態 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。
但這些證據不能直接推出搜尋排名、收錄速度、社群分享表現或所有爬蟲都會如何處理頁面。它們證明的是輸出契約成立,不是外部平台已經給了什麼結果。
寫靜態多語網站時的檢查順序
- 先確認每個公開網址是否真的產生了對應 HTML。
- 再決定頁面的主要網址與相容網址責任。
- 只為實際存在的替代頁輸出
hreflang。 - 讓 sitemap 讀取和正式頁面相同的公開集合。
- 把預覽與 404 的 noindex 規則獨立出來。
- 用產物檢查讀回 HTML、sitemap 和資源,不只看原始元件。
- 把機器檢查、瀏覽器操作和站主的主觀驗收分開回報。
靜態輸出的價值不只是「伺服器不用現場組頁面」。它也要求每一個公開網址、頁面 metadata 和內容狀態在建置時先對得上。只要其中一層仍然靠猜,網站看起來有很多頁,公開身份卻可能是混亂的。
紀錄範圍
本文整理目前固定頁、筆記文章、預覽、404、canonical、hreflang 和 sitemap 的實作與輸出檢查。它沒有宣稱搜尋引擎排名、索引結果、分享平台抓取或使用者理解效果已被量測。