- date
- entry
- 041
- topic
- infrastructure
- rev
- —
sitemap 檢查說多了一個網址:那頁沒進 git,但前一天就上線了
部署前的檢查停在 stale in sitemap:已 commit 的 sitemap 列著一個 git 裡沒有的頁面。它被當成草稿拿掉了,但那一頁前一天就從工作目錄部署上線。
〈從乾淨的 worktree 部署〉寫過一次:為了只部署 commit 過的內容,我改從乾淨的 worktree 部署,結果 --delete 把只存在工作目錄的 32 張圖刪掉。那次的教訓是:工作目錄裡沒進 git 的東西,可能早就在線上了。
這次是同一件事的另一面。10 月 10 日從乾淨的 worktree 部署公司官網,部署前的 sitemap 檢查停了下來:stale in sitemap: /services/partner-services/。已 commit 的 sitemap 列著一個網址,git 裡卻沒有那一頁。當時的工作階段把它判斷成「別人沒做完的草稿」,從 sitemap 拿掉了。但那一頁前一天就已經上線,而且送過 Google 和 IndexNow。這篇要談的是這個不一致怎麼來的、為什麼這次在乾淨的 worktree 裡才被看到,以及「沒進 git」為什麼不能讀成「沒上線」。依序回答五件事:時間線、工作目錄裡的檢查為什麼通過、順便發現的 lastmod 落後、本機重現,以及我會怎麼改。
時間線
以下時間是 +08:00,取自 git log、檔案的建立時間、工作階段的紀錄和部署 log:
| 時間 | 發生的事 |
|---|---|
| 10/9 21:19–21:20 | 一個工作階段建立了 services/partner-services/index.html(新的「合作服務」頁),本機建置後重產 sitemap:「新增 1、共 378 個 URL」 |
| 10/9 21:23 | 我確認後,從工作目錄部署官網,sitemap 檢查「一致(378 個 URL)」;部署後確認那一頁回 200、線上 sitemap 有它,接著重新提交 sitemap、送 IndexNow。這一頁當時一直沒有 commit |
| 10/9 21:46 | 另一批文章的 commit 26c2805 一起 commit 了工作目錄裡的 sitemap:377 → 385 個網址。其中兩個網址在這個 commit 裡沒有對應的頁面:這一頁,和一篇漏 add、1 分鐘後才補進 acefa73 的文章 |
| 10/10 19:17 | 另一個工作階段從乾淨的 sparse worktree 部署,檢查停下:stale in sitemap |
| 10/10 19:18 | 它查到那一頁在主工作目錄是未追蹤檔,判斷是別人的草稿,在乾淨的 worktree 重產 sitemap 並 commit(f2e260e,訊息寫「unpublished」)。接著的部署因為 AWS 帳號不對,沒有上傳任何東西 |
| 10/11 23:27、23:33 | 從乾淨的 worktree 部署兩次,上傳的 sitemap 是 402 個網址,沒有那一頁 |
| 10/13 01:42 | commit eae52b3 才把那一頁本身 commit 進來,sitemap 也加回去 |
官網部署腳本對 HTML 只上傳、不刪除(--delete 只作用在 assets/),所以 10 月 11 日那兩次部署之後,那一頁應該還在線上,只是不在 sitemap 裡。這一段是從腳本推的,我沒有那兩天直接抓那一頁的紀錄。
工作目錄裡的檢查為什麼通過
官網的 scripts/gen-sitemap.py 用 os.walk 走過磁碟上的檔案樹,每個有 index.html 的目錄就是一個網址(排除 dist、node_modules 等目錄,以及開頭 4,000 字元內符合它 noindex 規則的頁面)。是否收錄不看 git,git 只用來查 lastmod。--check 模式重新算一次網址集合,和磁碟上的 public/sitemap.xml 比,有新增或移除就結束碼 1;它不比 lastmod。
10 月 9 日那個工作階段在建立頁面之後,先跑檢查得到 MISSING from sitemap,重產之後再跑就一致了。之後只要網址集合沒變,在同一個工作目錄跑幾次都一致,即使其中有未追蹤的頁面:產生與檢查用的是同一套規則、同一個磁碟。
commit 記錄的是索引(index)裡的內容。那次索引裡有 sitemap,沒有那一頁,兩者在 commit 裡就對不上。在工作目錄裡,只要那一頁還在磁碟上,現有的檢查器就看不到這個不一致;10 月 10 日那個乾淨的 worktree 沒有未追蹤的檔案,才把它暴露出來。直接拿已 commit 的 sitemap 和 git ls-tree 列出的頁面比對,也看得到。
檢查本身擋對了:commit 裡的 sitemap 確實和 commit 裡的頁面不一致。出問題的是接下來的判斷。「在 git 裡沒有」只說明它沒被 commit,不說明它沒上線;那一頁是從工作目錄部署出去的,修正方向應該是把頁面補進 commit,而不是把網址從 sitemap 拿掉。
順便發現:lastmod 落在上一次 commit
f2e260e 的 diff 不只拿掉一個網址,還把 7 個網址的 lastmod 改成 2026-10-10。那 7 頁(部落格首頁和 6 篇既有文章)都是在 10 月 10 日 19:17 的 commit 96dc792 裡被改過的。
原因在 lastmod():它用 git log -1 --format=%cs -- <檔案> 取最後一次 commit 的日期,沒有結果或執行失敗才退回檔案修改時間。我的流程是先重產 sitemap、再一起 commit,產生的那一刻,這次改到的頁面還沒 commit,git log 回的是上一次 commit 的日期,要等下一次重產才補上(日期不一定差一天,看兩次 commit 隔多久)。--check 只比網址集合,這種落後不會讓任何檢查失敗。
本機重現
我把官網那支 gen-sitemap.py 原封不動複製進一個玩具 repo:首頁和 blog/a/ 以 10 月 5 日的日期 commit;之後改了 blog/a/,另外放一個沒有 commit 的 services/draft/,重產 sitemap,再把 blog/ 和 public/ 以 10 月 9 日的日期 commit(日期用 GIT_COMMITTER_DATE 和 GIT_AUTHOR_DATE 指定,重現當天是 10 月 15 日):
$ python3 scripts/gen-sitemap.py
+ /services/draft/
寫入:3 個 URL(新增 1、移除 0)
$ python3 scripts/gen-sitemap.py --check
sitemap 一致(3 個 URL)
# commit 之後,已 commit 的 sitemap:
/ lastmod 2026-10-05
/blog/a/ lastmod 2026-10-05 ← 這個 commit 改過它,日期卻是上一次的
/services/draft/ lastmod 2026-10-15 ← git 裡沒有,日期是檔案修改時間
$ git clone -q . ../clean && cd ../clean && python3 scripts/gen-sitemap.py --check
stale in sitemap: /services/draft/ # 節錄
$ echo $?
1
在乾淨的 clone 裡重產,diff 和 f2e260e 同一個形狀:/services/draft/ 那筆消失,/blog/a/ 的 lastmod 從 10-05 變成 10-09。
我會怎麼改
根源是「線上有什麼」和「git 裡有什麼」可以分開走:從工作目錄部署,未追蹤的頁面就能上線;從乾淨的 worktree 部署,它又被當成不存在。只修 sitemap 解決不了這件事,要讓兩者走同一條路。三件事:
-
部署前問 git。在工作目錄部署時,網站目錄底下有沒 commit 的變更就停下來;或者一律從乾淨的 worktree 部署。在玩具 repo 裡這樣寫:
實際的路徑清單要照網站的目錄結構寫。它擋的是「這次要部署的東西有沒 commit 的部分」;被$ cat gate.sh dirty=$(git status --porcelain --untracked-files=all -- blog services index.html) || exit 1 [ -z "$dirty" ] || { echo "工作目錄有沒 commit 的頁面,停止部署:" >&2; echo "$dirty" >&2; exit 1; } $ bash gate.sh; echo "exit=$?" 工作目錄有沒 commit 的頁面,停止部署: M blog/a/index.html ?? services/draft/index.html exit=1.gitignore排除的檔案不在檢查範圍,已經在線上的頁面也不會因此被清掉。 -
檢查報 stale 時,先看線上。
curl -s -o /dev/null -w '%{http_code}' https://…/services/partner-services/回 200,再看一眼內容,它多半已經上線,先問是不是該把頁面補進 commit;回 404,再查部署紀錄和當初要不要發布,決定補 commit 還是從 sitemap 拿掉。HTTP 狀態碼只是起點,不是判決。 -
lastmod 改看 git 知道的狀態。產生器只收
git ls-files列得到的頁面,跟 HEAD 不同的頁面用今天的日期:def git_set(*args): out = subprocess.run(["git", *args, "-z", "--", "."], cwd=ROOT, capture_output=True, text=True, check=True).stdout return {os.path.join(ROOT, f) for f in out.split("\0") if f} TRACKED = git_set("ls-files") # 目前索引裡的檔案 CHANGED = git_set("diff", "--name-only", "--relative", "HEAD") # 內容和 HEAD 不同git diff --name-only預設印相對於 repo 根目錄的路徑,官網的程式在 repo 的official_website/底下,所以要加--relative。在玩具 repo 和一個「網站在子目錄」的玩具 repo 裡跑過:未追蹤的services/draft/被跳過,/blog/a/的 lastmod 變成當天。CHANGED也包含還沒git add的修改,產生器讀的仍是工作目錄的檔案內容,不是索引快照。這一條單獨用會有反效果:如果第 1 條沒做、未追蹤的頁面照樣從工作目錄部署上去,它就會在線上卻不在 sitemap 裡,跟 10 月 11 日的狀況一樣。所以它要和第 1 條一起。另外,「今天」是重產的日期,和 commit 的日期可能不同。
這三件事我都還沒合進官網的 repo。
這個站也一樣
寫這篇時我回頭看了 baobaolin.com 自己的建置。vite.config.mjs 用 readdirSync 收集 posts/ 底下的每一篇,sitemap 由建置產物產生,scripts/deploy.sh 在 repo 目錄裡直接 npm run build,沒有檢查 git 狀態。照程式推,posts/ 裡一份沒 commit 的文章會被建置、部署上去(中文版也會列進 sitemap),和 10 月 9 日那一頁是同一條路。這個站的 sitemap 不 commit,所以不會出現「commit 裡不一致」,但「線上有、git 裡沒有」一樣會發生。
如果只記得一件事
「git 裡沒有」不等於「線上沒有」。開頭那個 stale in sitemap 是對的,它抓到 commit 裡的 sitemap 和頁面不一致;錯在把它讀成「這頁還沒發布」。從工作目錄部署過的東西,只能從線上確認它在不在。
修訂紀錄
- 首次發布