Coolkid mascot CoolkidLab Build in Public. Level up together.

SEO 菜鳥成長史 · #19

閱讀

為什麼 41 篇文章值得做一個日期 registry:從 LASTMOD 共用到單一事實來源

先講重點

#18 把站台跟文章日期拆兩個常數,但文章 schema dateModified 還是 19 篇共用一個 LASTMOD,跟每篇自己可見的「最後更新:YYYY-MM-DD」對不上。把 41 篇真實日期拆出 data/article_dates.py registry、用單一注入點 strip 掉舊日期、做到可見 == Article schema == og meta == registry 四方一致 — 41/41 全對齊,且未來改一處就好。

1. #18 結束時還剩一個尾巴

#18 拆完站台部署日(SITE_UPDATED)跟文章內容日(LASTMOD)兩個常數之後,GEO audit 的新鮮度訊號從 5/6 變成 6/6、Perplexity 引用力從 62 跳到 85。但我打開 pagespeed-67-to-93.html 的 source 一看,meta line 寫的「最後更新:2026-05-09」跟 Article schema 的 dateModified: 2026-05-28 對不上。

兩個都是我的,兩個都顯示「最後更新」,但讀者看到的是 5 月 9 日、Google 抓到的是 5 月 28 日。如果有任何爬蟲拿兩邊比對(很多 SEO 工具會),這個站立刻被標記成「schema 跟可見內容不一致」。E-E-A-T 的一致性傷害。

更尷尬的問題:所有 19 篇 SEO + pitfall 文章共用同一個 LASTMOD。我每次部署如果改 LASTMOD,所有文章 schema 都跟著動。如果不改,schema 永遠停在某一個日子。沒有中間地帶。

這就是「為什麼 41 篇文章值得做一個日期 registry」的起點。

2. 真實的選項只有兩個

把每篇文章的真實日期當資料管理,只有兩條路:

選項 A:在每個 build_*_article() 函式裡寫死 publish/modified 日期,傳進 article_schema(date_published=..., date_modified=...)。20 個 SEO 函式 + 13 個 pitfall spec + 8 個 workflow spec = 41 個改動點,全部分散。哪天規格變了改 41 個地方。

選項 B:寫一個 data/article_dates.py registry,key 是 url path、value 是 (published, modified) 兩個 ISO 字串。article_schema() 跟 page_shell() 自己去查 registry。改一處全站套用。

照 Python patterns 裡 single source of truth 的原則,選項 B 是正解。但選項 B 真正吸引人的地方不在「乾淨」 — 是它讓未來新增文章只要加一筆,而不是修改 N 個位置。

3. 真實日期從哪來:git first-add + 人工策劃日 + clamp

寫 registry 之前要先回答:每篇的 published 跟 modified 從哪知道?

published 用 git first-add:每個 HTML 檔在 repo 裡第一次被 commit 的日子。指令 git log --diff-filter=A --follow --format=%as -- <file>,是檔案進入 repo 的最權威時間。

modified 用人工策劃:文章內容真的有改的時候,本人決定那一天是修訂日。已存在的 19 篇我從 build.py 的 meta_text 字串裡抓「最後更新:YYYY-MM-DD」當策劃日。

但要 clamp:如果策劃日早於 git first-add(例如某篇文章內文裡寫「最後更新:2026-05-06」但 git 首建是 2026-05-08),那 modified 不可能早於 published — physics violation。對這幾個例外,modified 等於 published。

41 篇全部跑完,整理成像這樣的 dict:

ARTICLE_DATES: dict[str, tuple[str, str]] = {
    "/seo-journey/pagespeed-67-to-93.html": ("2026-05-09", "2026-05-09"),
    "/seo-journey/gsc-basics.html": ("2026-05-08", "2026-05-08"),  # clamped
    "/workflows/threads-auto-poster.html": ("2026-05-05", "2026-05-28"),  # 內文重寫過
    ...
}

def article_dates(path: str) -> tuple[str, str]:
    return ARTICLE_DATES.get(path, (LASTMOD, LASTMOD))

get 預設 fallback 到 LASTMOD — 未來新文還沒進 registry 的時候,schema 仍會出現一個合理日期、不會 KeyError。

4. 三處接 registry:schema / page_shell / 可見日期

Registry 是資料層,要連到三個輸出層:

article_schema() 內查 — 這個 generator 收 url_path,內部呼叫 article_dates(url_path) 拿 (published, modified) 寫進 datePublished / dateModified 兩個 JSON-LD 欄位。callers 不用改。

page_shell() 對 article 頁查 — page_shell 收 path,當 _is_article == True(caller 傳了 Article schema),就 lookup registry 寫進 article:published_time / article:modified_time 兩個 og meta。首頁/hub 不受影響。

可見「發布:X · 最後更新:Y」 — 這個最不直覺的部分。原本每篇的 meta line 裡寫死「最後更新:DATE」字串,跟 registry 各自為政。我不可能去 19 個 build_* 函式手改 19 個字串。所以做了一個 page_shell 等級的注入:

_META_P_RE = re.compile(r'(<p class='meta'>)(.*?)(</p>)', re.S)
_EMBED_DATE_RE = re.compile(r'\s*·?\s*最後更新[::]\s*\d{4}-\d{2}-\d{2}')

def _inject_article_dates(body, path):
    pub, mod = article_dates(path)
    dl = date_line(pub, mod)  # 渲染「發布:X · 最後更新:Y」span
    def repl(m):
        label = _EMBED_DATE_RE.sub('', m.group(2)).rstrip(' ·')
        return f'{m.group(1)}{label}{m.group(3)}\n{dl}'
    return _META_P_RE.sub(repl, body, count=1)

在 page_shell 裡,文章頁進來就跑一遍:清掉 meta line 裡舊的「· 最後更新:DATE」、把 registry 算出來的雙日期 span 接在後面。

這段是整個重構的關鍵。單一注入點 + strip 嵌入舊日期 = 零呼叫點改動。20 個 article_open() 呼叫沒動、19 個 SEO 函式沒動、13 個 pitfall spec 沒動、8 個 workflow spec 沒動。改動全部 contained 在 page_shell 跟一個正規表示式裡。

5. 4-way 一致性驗證

重構完最重要的事是驗證。schema 改了但 og meta 沒改怎麼辦?meta 改了但 visible 沒改怎麼辦?

寫一個 4-way 驗證腳本:對 41 篇文章,抓四個地方的日期(visible date line、Article schema dateModified、og article:modified_time meta、registry value),檢查全部相等。跑完一行 print:OK (4-way consistent): 41/41。

41/41。沒有半個漏網。包括 clamp 過的 gsc-basics(visible 原本 05-06,registry 05-08,注入後 visible 變 05-08)、原本沒有 visible date 的 gsc-weekly(注入後補上 05-19)、workflow 早上線晚改寫的 threads-auto-poster(05-05 發布 / 05-28 修訂雙日期同時顯示)。

這個 4-way 驗證的價值不在當下這次 41/41。是未來每次部署它都會跑一遍,新文章加錯了會立刻發現。比 audit 工具還可靠,因為它檢查的是「我自己定義的一致性」,不是「audit 工具理解的一致性」。

6. 新 SOP:新文章上線多一個步驟

整套 registry 制度落地後,新增 SEO 文章的 SOP 從原本 6 個註冊點變 7 個。一定要在 data/article_dates.py 加一筆 (published, modified),不然該篇 fallback 到站台 LASTMOD(不準)。Schema 跟 og meta 都會錯。

註解寫在 build.py 與 article_dates.py 裡,下次寫新文章看到就會想起來。這也是 single source of truth 的另一個價值 — 記憶量降了。我不用記「日期在 build.py 哪一行」,記得「日期在 article_dates.py」就好。

▸ 常見問題

Q1:為什麼不用 git mtime 自動算 modified?

repo 提交「已 build 過的 HTML」,每次部署都會動到所有 HTML 檔。git log 的 last commit 永遠是「上次 build」而不是「上次內容變更」。除非把 source 跟 output 拆開、用 source 端 mtime — 但重構 build.py 也會動到所有 builder 函式 mtime,一樣失準。最乾淨的還是手動 registry。

Q2:41 筆手動維護不會出錯嗎?

第一次填當然會錯。所以寫了一個 gen_article_dates.py 一次性腳本:讀 git first-add + built HTML 可見「最後更新」、clamp 不可能日期、輸出 registry 檔案。第一次生成跑完直接得到 41 筆,省去手抄。之後新增/修訂文章自己加一筆即可。

Q3:fallback 到 LASTMOD 會不會讓我忽略漏加的篇?

會。所以 4-way 驗證腳本是必備的。每次 build 後跑一次,缺 registry entry 的篇會被 visible 跟 schema 對不上抓出來。沒有驗證腳本的話這個系統很危險 — fallback 是 silent 的。

Q4:這套對 SEO/GEO 真的有差嗎?

直接的 SEO 加分有限,但間接的信任修復很重要。Schema 跟 visible 不一致是「站台健康度」的弱訊號 — 它不會讓你被懲罰,但會讓 ranker 給你低一階的信任分數。修好之後 brand_entity audit 從 6/10 變 7/10(雖然這 +1 主要靠 Service schema 加上去的,但 schema 一致性的清理是前提)。

41 篇文章值得做一個日期 registry。單一事實來源 + 單一注入點 + 4-way 驗證 = 零呼叫點改動。意外收穫不是「乾淨」是「記憶量降了」 — 不用記日期在 build.py 哪一行,記在 article_dates.py 就好。下篇 #20 講 audit 從 70 推到 81 的完整 5 步紀錄。

名詞解釋

SEO(搜尋引擎優化)
讓網站在 Google 搜尋結果排得更前面的一整套方法,涵蓋技術體質、內容品質、連結結構三層。
部署(deploy)
把做好的網站或程式「推上線」讓所有人用得到的動作。
結構化資料(Schema / JSON-LD)
用機器看得懂的格式跟搜尋引擎說明「這頁是文章、作者是誰、何時更新」,有機會換到更豐富的搜尋結果外觀。
提交(commit)
Git 的一個存檔點:把這次改動連同一句說明記錄下來,之後隨時可以回到這個版本。
JSON
程式之間交換資料的通用格式,長得像一層層的「名稱:內容」清單,人眼也讀得懂。
爬蟲(crawler)
自動瀏覽網頁、把內容抓回去的程式。Google 靠爬蟲收錄網頁,AI 公司靠爬蟲收集資料。
E-E-A-T
Google 評估內容可信度的四個面向:經驗(Experience)、專業(Expertise)、權威(Authoritativeness)、可信(Trustworthiness)。
Python
入門門檻最低的主流程式語言之一,資料處理、自動化、AI 領域的首選。

看完這篇之前先確認:

適合你
  • 靜態站台、多篇文章共用一個 build pipeline、文章內容偶爾修訂
  • 想做 dateModified 但不想被 git mtime 污染(每次 build 動到全部)
  • 想做 GEO 跟 freshness 但要守住誠實 — 沒改的內容不裝今天
不適合
  • CMS 站(WordPress / Ghost / Notion-as-CMS)每篇已內建 published/modified 欄位
  • 純 evergreen 內容站、文章從不修訂(registry 退化成單常數比較簡潔)
  • 文章內容跟 schema 由不同人/不同系統管理(同步成本太大)
最常踩
  • registry 加新文章前先檢查 path 對:少一個斜線、結尾 .html 漏寫,fallback 就靜默觸發
  • modified 比 published 早 → physics violation。clamp 規則要寫進 generator script
  • 改 LASTMOD 以為文章就更新了 — LASTMOD 現在只是 fallback,registry 才是真的

這篇是收斂後寫的版本。
我每兩週寄一封電子報,講「正在做但還沒寫成文章」的東西——
包含每月幫你過濾值得花時間的新 AI 工具,
以及 Lab 新文的個人版(你會比公開版早一週收到)。

→ 訂閱(雙週一封,第一封自動寄起步清單)

跳轉 Substack、隨時取消、不轉賣 email。

如果內容對你有用就太好了
隨喜斗內

Buy Me a Coffee at ko-fi.com
NEXT CHAPTER ▸ #20 把 GEO audit 從 70 推到 81:5 步、3 commit、每一步的分數與證據

相關閱讀

這篇背後的真實開發過程記錄在 Build Log搜尋標籤:georegistrysingle-source-of-truthdateModifiedrefactorbuild-in-public

本篇為個人實驗紀錄。本文做法不保證在你的網站產生相同結果,請依自身狀況驗證。教育研究用途,不構成投資建議。

← 回 SEO 菜鳥成長史

⚠ 本站所有內容僅供教育與研究用途,不構成投資建議,不保證任何獲利。投資有風險,使用者須自行判斷並承擔結果。