Skip to content

Latest commit

 

History

History
183 lines (121 loc) · 19.5 KB

File metadata and controls

183 lines (121 loc) · 19.5 KB

404 根因調查:我們自己是最大的死連結發行者

2026-07-17 manual session(哲宇 /goal「最近好像蠻多 404,用一切方式查出根因,並且讓未來可以被自動監測與進化相關 DNA / pipeline」)

一句話結論:CF 404 率高檔盤整 12 個 cycle 的主因,是站體自己在每一頁的 hreflang 標籤裡對外公告死 URL——全站 5,031 篇文章頁吐出 13,014 條指向 404 的 alternate(99.8% 頁面中招),而唯一能看見人類 404 的儀器已經無聲死了三個月。

v2.0(同日三波收官):根因修復+監測儀器+slug 全站統一(102 檔)+CI/CD 契約閘門+部署平台真相修正(GitHub Pages)。本報告是這整場戰役的完整歸檔。


一、症狀

  • CF 404 率 14.99%(2026-07-17 am),data-refresh 連續 12 cycle 記帳「高檔盤整未 break-out」。
  • CONSCIOUSNESS §適應性反應掛著一條 🟠,附註寫「已知組成含 crawler 掃舊 URL」——這句話從四月起沒有被重新驗證過。
  • 哲宇 GA4 即時畫面:過去 30 分鐘瀏覽第一名是 404 頁(8 次 / 13.56%)。GA4 即時看的是帶 JS 的人類,跟「crawler 掃舊 URL」的既有解釋對不上——這就是本次調查的起點。

二、方法

既有工具 analyze-crawler-404.pyTARGET_CRAWLERS 是 10 個 bot UA 的 allowlist,不在名單上的 UA 一律丟棄——它由建構決定看不見人類。本次直接打 CF GraphQL httpRequestsAdaptiveGroupsedgeResponseStatus: 404 filter、不濾 UA,取 3 天全量(每日 top-10k row,截斷有標註),再與 route 表、語言註冊表交叉對賬。

三、根因 A:hreflang 用字串拼接製造死 URL(主因)

SEO.astro 的 hreflang 區塊用「剝掉語言前綴、貼上另一個前綴」的字串拼接組 alternate URL。但同一篇文章的 slug 在各語言本來就不同:

語言 李珠珢的真實 URL
zh /people/李珠珢/
en /en/people/lee-ju-eun/
ja/ko/es/fr /{lang}/people/lee-ju-eun-singer/

拼接的結果:修復前,中文頁對 Google 公告的 6 條 alternate 裡 5 條是 404(en/ja/ko/es/fr 全指向 /{lang}/people/李珠珢/);每一篇翻譯頁的 zh-Hantx-default 也全部指向不存在的 /{cat}/{羅馬化 slug}/

量測(route 表模擬全站 emit):

  • 全站 5,031 篇文章頁 → 35,217 條 hreflang → 13,014 條(37.0%)指向 404
  • 5,019 / 5,031 頁(99.8%)至少一條死
  • 抽 12 條雙向實測(宣稱死的 curl 全 404、宣稱活的全 200)驗證量測工具本身

與 CF 實際 404 交叉:3 天 34,201 筆 404 中 9,685 筆(28.3%)落在我們自己公告的死 URL 集合——這是下界,因為 top-10k 截斷吃掉長尾。加上「疑似真實文章 URL」家族占全部 404 的 78.1%(94% 是 bot)——爬蟲不是在亂猜舊 URL,是在忠實地跟隨我們發給它們的死連結。四月報告的「crawler 掃舊 URL」量對了,因果反了:一個是修不了的外部噪音,一個是我們自己造的、可修。

最痛的一層:主權的巴別塔對 Google 的 hreflang 叢集是隱形的。這篇文章五種語言都有翻譯,但 hreflang 指向的位置全不存在,真正的譯本全成了叢集外孤兒。多語投射的 SEO 面等於沒接上。

四、根因 B:page_404 儀器無聲死亡三個月

404.astro 的追蹤事件:

const article = articleIndex[key];   // 宣告在兩層 if 之內(block scope)
...
had_suggestion: !!article,           // 使用在兩層 if 之外 → ReferenceError

payload 物件求值時就丟 ReferenceErrorgtag() 根本沒被呼叫,而外層 try/catch 的註解寫著「Never let tracking break the 404 page itself」——把錯誤靜默吞掉。瀏覽器實證(複現 scope 結構):try block entered → gtagOk: true → ReferenceError: article is not defined

後果鏈:GA4 過去 7 天 page_404 事件數 0;2026-05-29 有個 session 盡責地為這個事件註冊了 4 個 GA4 custom dimension——為一個從未發射過的事件註冊了維度,而 instrumentation-audit.py 三方對齊查的是「param 有沒有註冊、code 有沒有引用」,全綠。存在性全對,效果為零(REFLEXES #82 教科書案例)。

這顆儀器的死亡不是獨立事件:它正是四月報告誤判根因 A 的原因——唯一能說出「人類踩到哪個 URL、從哪裡來」的感知器官不在場。

五、根因 C:語言註冊表 fromZh 被別名蓋掉

generate-lang-switch-map.mjsgetLangSwitchPath.tsadd():原生 slug 先註冊、en-slug 別名後註冊,fromZh[k] = nL last-write-wins → 別名蓋掉唯一真實存在的原生 route。ja 的李珠珢被指到 /ja/people/lee-ju-eun(404)而不是 /ja/people/lee-ju-eun-singer。這代表可見的語言切換器對 slug 分歧文章也一直在吐死鏈——不只 hreflang。修法:fromZh 改 first-wins(輸出面只信原生 slug);toZh 保留雙鍵(輸入容錯)。

六、其他家族(3 天全量分類)

家族 3d 次數 佔比 UA 主體 處置
疑似真實文章 URL(含根因 A) 26,731 78.1% bot 94% 根源已修+redirect 收尾
其他(apple-touch-icon 變體等) 3,632 10.6% bot /apple-touch-icon-* 一條 redirect
掃描器 PHP/WordPress 1,046 3.1% empty UA 不修,監測中分離
聯絡頁探測(/contact 等五連號) 805 2.4% browser UA 但次數均勻=wordlist 掃描器 不修,分離
.md 副檔名請求 596 1.7% bot 67% 404 頁救援導向 raw 視圖
掃描器 .env 竊取 400 1.2% 不修,分離
<strange-chars> 編碼壞 227 0.7% bot 平反:這是 CF 對壞位元組序列的正規化佔位符(2,411 條正常中文 path 都以 200 完整保留可證),不是站上模板 bug。四月報告把儀器自己的佔位符當成我們的資料在追(REFLEXES #24)
stale _astro hash 223 0.7% browser 99% 部署後暫態,監測分離、只警告 spike
缺靜態資產 207 0.6% browser 95% 大宗是 taiwan-towns-{縣代碼}.topo.json:taiwan-shape 頁公告了 22 縣市 pattern 卻只放 6 個直轄市檔——真開發者猜代碼。補齊檔案

另有一筆真實的模板洩漏:/[lang/]raw/<category>/<slug>(Googlebot 一筆)——來源是 ReaderSettings.astro 內嵌 script 的註解被爬蟲 regex 成 URL。註解改寫掉。

apple-touch-icon-120x120.png 404 而主檔 200:四月 EXP-A 只修了 base 檔,iOS 會請求尺寸變體——「已驗過」的結論要帶時間戳(REFLEXES #67)。

七、三代儀器全在量替身(本次最大的教訓)

這次不是「沒有儀器」,是三代儀器每一代都在量替身

  1. analyze-crawler-404.py:UA allowlist 只看 10 個 bot → 結構性看不見人類,「404 是 crawler 造成的」成了儀器建構的必然結論
  2. 四月報告:把 CF 的 <strange-chars> 佔位符當站上模板 bug 追;用 path 形狀猜 cause 把 hreflang 誤判成 Header nav
  3. instrumentation-audit.py:查「param 註冊了沒、code 引用了沒」——存在性,不是效果。page_404 全綠地死了三個月

加上第四層:2026-04-18 報告的 handoff 五個 checkbox(含「每週跑 analyze-crawler-404 追蹤趨勢」)三個月後仍全部未勾——對自己的 bug 有洞察 ≠ apply 了 fix,memory 是自律,canonical gate 才是閘門(REFLEXES #15,第 N 次驗證)。

統一根因(架構層):站體對機器公告的 URL 契約(hreflang / sitemap / canonical / 文件承諾的檔案 pattern),跟真實 route 表之間沒有任何一道對賬閘門。

八、修復(本次 ship)

已 commit(f369f3c8e):

  1. SEO.astro hreflang 改走 LangMap 註冊表(與 Header 切換器同源),沒有翻譯就不吐;叢集 < 2 條整組省略;trailing slash 對齊 canonical;已編碼路徑不重編碼
  2. getLangSwitchPath.ts + generate-lang-switch-map.mjs fromZh 改 first-wins(原生 slug 贏)+ registry 重新生成
  3. 404.astro page_404 復活(scope 修正 + catch 改會叫)

驗證:dev 上李珠珢三個語言版本吐出完全一致的 7 條叢集、逐條 curl 全 200;404 頁 dataLayer 收到完整 payload。

平行實作中(Sonnet 分身五路,本報告 ship 時整合):

  1. monitor-404.py:全流量、resolution-based 分類(不是 regex 猜測)、家族×UA 組成、新 pattern 警報,接 refresh-data + dashboard alerts
  2. generate-redirects.mjs:資料驅動 _redirects(觀測到的死 URL × 註冊表可推導目標,cap 1500)收爬蟲快取的尾
  3. 22 縣市 topo 檔補齊(授權查證後從原 source 補 vendor)
  4. 404 頁 .md 救援(導向 raw 視圖——對 LLM reader 的 UX)
  5. check-url-contract.mjs:build 後對賬 dist 內所有自我公告 URL(hreflang / canonical / sitemap)vs 真實檔案樹——那道缺席的閘門。黃燈起步(report-only),數據穩了再升 hard

九、監測與可證偽預測

監測儀器的設計原則:量 ground truth 不量替身(#82)、閾值用今天的真實資料校準(#66)、截斷必標註、掃描器噪音與可修訊號分離。接進 data-refresh 日常 riding。

EXP(進 UNKNOWNS,機械到期檢查):修復部署後 21 天內,CF 7d 404 率從 14.99% 降到 ≤ 8%;30 天內「可解析家族」(slug-variant + cross-lang + untranslated + renamed)從 ~8.9k/日降 ≥ 60%。反駁條件:若 D+21 仍 > 12% → 我對「爬蟲跟隨自家死鏈」的因果權重判斷錯誤,另有大宗來源未被本次分類捕捉——回來重讀 monitor 的 unknown 家族。

十、長期發展與延伸

  1. 翻譯需求飛輪untranslated-demand 家族就是讀者用 404 投票的翻譯清單(/en/people/lee-ju-eun-singer 3 天 83 次=英文版需求排行第一)。monitor 已輸出該欄位,接進 babel 優先序(SQUEEZE P-schema)是下一步——404 從 entropy 變成選題訊號。
  2. i18n 反查 404 middleware(CONSCIOUSNESS 原「結構解」構想):CF Pages Functions 動態救援任何語言任何 slug。決策閘門:D+30 若可解析家族仍 > 2%/日再上——redirect + 根源修復可能已夠。
  3. 改名即 redirect 的 pre-commit gate:偵測 knowledge/ rename 提示補 redirect。目前 monitor→redirect 迴路幾天內自動收尾,此 gate 屬邊際改善,roadmap 待命。
  4. sitemap alternates:目前 sitemap 無 xhtml:link alternate。未來若補,必須走同一個註冊表模組——否則就是在 sitemap 重演 hreflang 的病。
  5. AI reader 人體工學.md 請求與巢狀 wikilink 解析請求證明 LLM agent 正在讀 raw markdown 並跟隨相對連結。llms.txt 之外,raw 視圖的可發現性(如 Link header、404 救援)是「為 AI 讀者做 SEO」的下一章。

十一、第二波(同日晚間):slug 統一與 CI/CD 契約閘門

哲宇看完第一波後下了三條 directive:slug 各語言不一致本身就是錯(理論上非中文語言應共用同一 slug,分歧是巴別塔免費模型翻譯時自作主張取名造成的);build/CI 要每次自動對賬每條路徑;sitemap 全部頁面要能被爬、SC 要第一時間收到。第二波據此收官:

  1. slug 統一(en slug = canonical)unify-translation-slugs.py 帶兩道防呆(token 零重疊疑殭屍對、目標碰撞)改名 87 檔+梅雨 4 檔(斷詞差異騙過防呆、被新閘門抓回),全部舊 URL 寫成 301。7 案防呆保留人工 review(含 FAB DAO 疑似 translatedFrom 錯指、ko/Economy/taiwan-stock-market.md 孤兒殭屍對)。
  2. 防再犯閘門check-slug-consistency.py 進 pre-commit——未來任何翻譯落地,檔名與 en 不同當場擋下。第一次 dogfood 就抓到 unify 自己的漏網(兩把獨立尺對賬的價值當場示範)。
  3. CI/CD 契約閘門check-url-contract 升 v2(sitemap 反向覆蓋——存在卻不公告的頁面也有人看,redirect stub 排除)接進 postbuild:本地與 CF Pages CI 每次 build 自動跑 --strict,死公告 URL 紅字擋 deploy,URL_CONTRACT_MODE=warn 留逃生口。基線:dead 0、覆蓋缺席 0。
  4. 舊 redirect 清償:astro.config 六條四月 redirect 的目標自己也搬過家(渲染出死 canonical),逐條對 route 表修正;_redirects 的 apple-touch-icon 萬用字元從未匹配過(CF Pages 不支援段中 *),改逐尺寸枚舉。
  5. SC 即時收錄submit-sitemap-sc.py(sitemaps.submit)ready;實測 service account 在 SC 是受限權限(Sitemaps API 要完整),待哲宇在 SC 使用者權限升完整後跑一次。

十二、第三波(同日夜間):平台真相與 slug 全站歸零

部署平台真相:GitHub Pages,不是 CF Pages

_redirects 部署後全線無效的偵錯揭露:taiwan.md 一直部署在 GitHub Pagesactions/deploy-pages),Cloudflare 只是前面的 DNS/CDN 代理層——這也解釋了為什麼 CF 能看到全部流量卻管不了 redirect。認知層(MANIFESTO / ANATOMY / CLAUDE.md)寫的「CF Pages CI」是過時的自我描述,會讓每個 session 對 redirect / header / edge 能力做錯假設,已全部修正。

redirect 因此重新平台化:generate-redirects.mjs 多產出 config/redirects-generated.jsonastro.config.mjs 讀入生成 meta-refresh + canonical + noindex stub 頁(GH Pages 唯一原生 redirect 路徑,與站上既有手寫 redirect 同機制);資產類請求不會跟 meta-refresh,apple-touch-icon 十七個尺寸變體改用 sips 產出真檔。_redirects 檔保留產出(未來若遷 CF Pages 即插即用)。

slug 錯誤全站歸零

review 保留的 7 案逐一人工判定清償:FAB DAO 五語統一 fab-dao(原 en slug 與另一篇撞題屬誤導名)、ko 台灣股市刪英文內文殭屍後真檔補位、fr 張懸刪重複殭屍、高速公路/八部合音/志工文化/雜學校同義詞分歧照 en 統一。終態:全站 3,381 個翻譯檔 slug 100% 與 en 對齊、白名單歸零、pre-commit 閘門對每個新翻譯生效。 兩波合計 102 檔改名 + 2 殭屍刪除 + 106 條 301。

十三、長期策略歸檔(現狀 → 何以至此 → 往哪走)

現狀(2026-07-17 夜)

狀態
對外 URL 契約 hreflang / canonical / sitemap 三來源 0 死;sitemap 反向覆蓋 0 缺席;每次 build postbuild --strict 自動對賬(CI 紅字擋 deploy)
slug 一致性 全站 100% 對齊 en canonical;pre-commit 閘門防再犯;unify 工具可重複使用
404 監測 monitor-404 每日 riding data-refresh,resolution-based 分家族、UA 拆分、新 pattern 警報進 dashboard alerts
Redirect 200 條(manual 123 + 資料驅動 77)→ astro stub;資料驅動端由 monitor 觀測值持續餵入
GA 人類側 page_404 復活(含 referrer),D+3 起可拆內部斷鏈 vs 外部舊連結
SC sitemap 提交工具 ready;service account 權限待哲宇升「完整」後一鍵提交

為什麼會走到這裡(三層病因的總結)

  1. 公告層各自為政:hreflang、切換器、sitemap、redirect 各自算 URL,唯一有框架對賬的 sitemap 是唯一乾淨的——證明問題從來不是「誰寫錯了」,是缺一道統一的對賬閘門
  2. 儀器量替身:三代儀器(UA allowlist/path 形狀猜因/查註冊不查發射)都誠實回答了錯的問題。
  3. 翻譯層放任命名:巴別塔免費模型自作主張取 slug,無 gate 攔截,漂移滲入 URL 層變成死鏈土壤。

長期策略(已編入器官與排程的部分)

  • 每日:monitor-404 記帳+alerts 黃燈(data-refresh riding);untranslated-demand 家族累積為翻譯需求排行
  • 每次 build:URL 契約 strict 對賬(本地+CI);每次 commit:slug 一致性 gate
  • D+21(2026-08-07):EXP-2026-07-17-G 機械到期——404 率 ≤8% 驗證因果判斷,反駁則 unknown 家族重讀+i18n middleware 升優先
  • 待哲宇一鍵:SC service account 權限升「完整」→ submit-sitemap-sc.py 提交,改名後的 URL 第一時間進索引
  • 延伸方向(依序解鎖):404 翻譯需求 → babel P-schema 接線;GA4 page_404 referrer 分析進 EVOLVE 選題;coverage 檢查黃燈轉硬;若未來遷 CF Pages,_redirects 即插即用取代 stub

附錄:資料

  • 量測腳本與中間資料:session scratchpad(probe-404.py / measure-hreflang.py / intersect.py / classify-rest.py)
  • 監測儀器落地後的持續資料:reports/404-monitor/
  • 修復 commit:f369f3c8e(根源三刀);後續整合 commit 見 git log 本日

v2.0 | 2026-07-17 manual session(三波完整收官:根因→儀器→slug 歸零→平台真相→CI/CD 閘門) 調查方法核心:把三代儀器的替身撕掉,直接對 ground truth(CF 全量 × route 表 × 註冊表 × 瀏覽器 dataLayer)交叉對賬