從無效的 GitHub 連結談起:那些型別與測試抓不到的外部資料地雷


為了做好 SEO,滿足 Google 的 E-E-A-T(經驗、專業度、權威性、可信度)原則幾乎是不可或缺的功課。為了向搜尋引擎聲明作者的外部權威身分,我在部落格的文章頁面輸出了包含 Person 節點的 JSON-LD 結構化資料,並透過 sameAs 欄位提供作者的 GitHub 連結。

sameAs 的語意是「這是同一個實體在別處的權威來源」——你告訴搜尋引擎「文章作者就是這個 GitHub 帳號的人」,讓它把身分串起來。實作起來並不複雜,在專案裡它就是一組常數:

export const AUTHOR: { name: string; sameAs: string[] } = {
	name: 'Vincent C.K. Chen',
	sameAs: ['https://github.com/tp6xup6'],
};

一切看似順利,直到某天回頭盤點進度時,踩到了一個埋了一陣子的地雷。

消失的連結,與一場不必要的考古

進度文件上明明記載著「已補齊作者的 GitHub 連結」,但翻開程式碼,sameAs 是個空陣列。文件跟程式碼對不上。

往回追 commit,最近一筆是:

fix: remove broken github link

只有一行標題,沒有 body。看不出「為什麼 broken」。git show 才看到被移除的內容,git log -S 才確認它是什麼時候進來的——結果是,站台第一次正式上線時它就在裡面了。

再用兩個獨立來源交叉查真值:

$ git remote -v
origin  https://github.com/VincentCKChen/<repo>.git

$ gh api user --jq '.login'
VincentCKChen

tp6xup6 這個帳號名,是從網域 tp6xup6.cc 推測出來的。 從來沒有人打開那個網址確認過。它不存在。

為什麼整條管線都沒攔下來

臆測的識別碼有個特質:它看起來完全合理。

網域叫 tp6xup6.cc,GitHub 帳號叫 tp6xup6——這個推論在絕大多數情況下是對的,很多人確實是同一組名字到處用。它不像 typo 那樣一眼有問題,它讀起來就是正確答案的樣子。

而整條管線沒有任何一道關卡驗得到它:

  • TypeScript 只知道那是 string[],字串內容它不管
  • Schema 驗證用的是 z.string().url(),只驗格式合不合法,而 https://github.com/完全不存在的東西 格式完全合法
  • 建置不會去打那個 URL
  • 測試測的是「sameAs 有沒有被正確放進 JSON-LD」,不是「那個 URL 指向的東西存不存在」

這些工具保證的是資料結構的合法性,不是與現實的一致性。格式對了,系統就買單。

錯的比沒有更糟

這裡有個容易被輕輕帶過的差別。

sameAs 空著,Google 就是不知道作者的其他身分——中性的缺失。

sameAs 指向一個不存在的帳號,是主動聲明了一個假的身分連結。你叫搜尋引擎去驗證作者,然後給它一個 404。在一個正想證明「作者是真人、有可驗證專業」的站上,這個方向完全反了。

第二個錯誤:「移除」看起來像修好了

發現連結是壞的之後,當時的修法是把它拿掉。

這只是把「錯誤的資料」換成「沒有資料」。 一開始想達成的事——讓作者身分有外部驗證——還是沒達成,只是不再顯眼了。而因為進度文件同時還寫著「已補齊」,這個缺口就安靜地留著。

更麻煩的是那則 commit message。它記了「做了什麼」,沒記「為什麼壞」。結果幾天後真要修的時候,得重新 git show、重新查 remote、重新驗證帳號——把當初其實已經有人知道過一次的事,再考古一遍。

如果當時 message 多兩句「這個帳號是照網域名猜的,沒驗證過,實際不存在」,後面整段考古都不必發生。

後來調整的幾個習慣

1. 外部識別碼一律交叉查證,不接受推論

帳號名、profile URL、外部服務 ID——這些東西沒有「應該是」,只有「查過」跟「沒查過」。GitHub 這種至少有兩個獨立來源可以對:

git remote -v                        # repo 掛在誰底下
gh api user --jq '.login, .html_url'  # 登入身分是誰

2. 寫進去之後,實際打一次

curl -s -o /dev/null -w '%{http_code}' https://github.com/<account>

回 200 才算數。這一步三秒鐘,但它是唯一真的能區分「看起來對」和「是對的」的動作。

3. 標記要留在問題會被問到的地方

移除是把問題換個形狀,不是解決。真的當下查不到真值,那就讓它是一個看得見的缺口,而不是一個安靜的空陣列。

這裡有個我一開始想錯的地方。我原本的防線是文件——把事件歸檔、在 CHANGELOG 記結論並附上歸檔連結,需要細節就進去找。這套做法沒有錯,但它在這次完全沒發揮作用,而失敗的方式很值得記下來:

進度文件上寫著「已補齊 GitHub 連結」,程式碼裡卻是空陣列。 文件不只沒幫上忙,它是錯的——而且錯得很有說服力,因為它讀起來像已經完成。任何相信那份文件的人都會直接跳過這個問題。

真正的癥結不是「文件 vs 註解」哪個比較好,是這兩者回答的是不同的問題,而且提問的時機不同:

  • 「這裡為什麼長這樣?」——問這句話的時候,你正盯著那一行程式碼。答案必須就在那裡。
  • 「當初為什麼那樣決定?經過是什麼?」——問這句話的時候,你是在回顧。這時你會主動去翻歸檔。

歸檔永遠只被「已經決定要去找」的人找到。而現場的標記,是誰站在那裡就會看到誰。這次的缺口之所以留了那麼久,就是因為它只存在於第二種形式裡,而沒有人有理由去翻。

所以現場留一個指標,細節仍然放歸檔:

// ❌ 只移除,不留線索——三個月後沒人知道這裡缺什麼
sameAs: [],

// ✅ 一兩行說明現況 + 指向細節,不重述調查過程
// sameAs 目前為空:原連結是照網域名推測的、帳號不存在(見 CHANGELOG 2026-07-29)。
// 空著代表作者身分沒有外部驗證,待確認正確帳號後補回。
sameAs: [],

注意它是指標不是副本。把整段調查經過搬進註解,只會變成沒人看的一大塊,然後在某次整理時被當成雜訊刪掉。

4. Commit message 記「為什麼」,不只記「做了什麼」

判準很簡單:如果三個月後的你會問「當初為什麼這樣做」,現在就寫下來。 尤其是 fix 類的 commit——fix: remove X 這種訊息,幾乎必然會在未來製造一次考古。

5. 想過在 CI 驗證外部連結,但沒有做

最直覺的自動化解法,是在建置時對所有 sameAs 發 HTTP 請求,回不是 200 就讓 CI 失敗,這樣這種錯誤永遠過不了關。

考慮之後沒有做,因為代價不對稱:CI 從此依賴外網。 GitHub 抽風、rate limit、辦公室網路不穩,都會讓一個跟程式碼完全無關的建置紅掉。用一個常態性的不穩定,去換一個一次性的查證動作,不划算。而且一旦紅得夠頻繁,下一步就是有人去把這個檢查設成 continue-on-error,那它就等於不存在了。

比較合理的位置是**「寫入的當下」而不是「每次 build」**——這是個習慣問題,不是自動化問題。不是所有錯誤都值得用 CI 去防。

結語

系統架構再嚴謹、型別系統再嚴格,終究擋不住「符合格式但不符合現實」的資料。型別檢查驗的是形狀,schema 驗的是格式,兩者都不會替你確認那個東西真的存在。

而這種「看起來合理的臆測」,在 AI 輔助開發的情境下會更常發生。不是因為模型比較會亂猜,而是因為模型非常擅長產生看起來合理的東西——一個從網域名推導出來的帳號名,正是那種讀起來毫無破綻、reviewer 也不會多看一眼的填空。

愈是這種「一看就對」的欄位,愈需要真的去打一次。