這篇要解決什麼

隨著 blog 文章與知識卡片成長,純靠寫作紀律維持排版一致性越來越不可靠。反覆踩到的問題橫跨兩個層級:

結構與安全層級(這是工具鏈存在的主要理由):

  • 裸 URL 在段落與表格中爆版(MD034),降低閱讀體驗。
  • 表格管線風格混用(MD060),同一張表格有的有空白、有的沒有。
  • 平行模板章節重複標題(MD024),例如多案例文章的 ### 弱點環節 出現 13 次。
  • 顯示文字與實際 href 不一致(反釣魚)— 不在標準 markdownlint 規則內,但紅隊教材脈絡下必要。
  • 卡片雙向完整性(orphan 卡片、斷連結、K4 合規)— 跨文件檢查,現成工具做不到。
  • Front matter schema — Hugo 依賴 YAML front matter 提供 title / date / weight 等欄位,缺失會破壞列表渲染、排序、SEO。

基礎格式層級(容易被忽略但影響 parser 穩定性或語義結構):

  • 正文禁止使用 H1(嚴於 MD025)— Hugo front matter title 已產生 H1。
  • 標題前後需保留空行(MD022),parser 才能正確識別標題邊界。
  • 標題結尾禁止標點(MD026)— 例如 ## 常見問題: 應改為 ## 常見問題
  • 禁止用 **bold** 段落當標題(MD036)— 破壞語義階層與 TOC 產生。
  • 程式碼區塊需註明語言(MD040),影響 syntax highlighting 與 accessibility。
  • 列表前後需空行(MD032)、fenced code block 前後需空行(MD031)— 否則部分 parser 會把列表吃進段落。
  • 有序列表編號風格一致(MD029)— 全部 1. 或全部 1./2./3.
  • 檔案結尾需有換行(MD047),POSIX 規範。
  • 行長度上限(MD013)— 預設關閉,中英混用技術寫作不適用 80-char 慣例。

前兩類混合在同一份寫作規範裡,因為都由同一個工具鏈檢查、都要落地到相同的 pre-commit hook。純靠紀律記住這十幾條在大型 repo 上不可行,純 regex 又無法穩定處理「平行結構下的標題重複」「卡片段落歸屬」這類語意判斷。因此 blog 專案採用 Go + goldmark AST 做自訂 linter:scripts/mdtools。本文是 linter 與寫作規範的對齊文件;AGENTS.md 引用本文作為排版規範來源。


1. 工具總覽

子命令職責改檔觸發時機
mdtools fmt [--fix|--check]格式正規化(URL、表格、空行、列表間距、trailing newline)--fix 會改pre-commit(--fix)、pre-push / CI(--check
mdtools lint結構檢查(標題、反釣魚、code block 語言、front matter schema)pre-commit、pre-push、CI
mdtools cards跨文件完整性(連結、orphan、K4)pre-commit、pre-push、CI

工具原始碼在 scripts/mdtools/,binary build 到 bin/mdtools(已 gitignore)。

作用範圍是 content/**/*.mdpublic/themes/node_modules/ 等輸出或第三方資源不檢查。


2. 標題規則

2.1 標題結構與格式規則

  • 正文禁止使用 H1。Hugo 的 front matter title 會自動產生 H1,若正文再寫 # ... 會出現兩個 H1 並列,破壞語義階層與 SEO 訊號。正文一律從 H2 開始,最深到 H6。
  • 同一父標題(直接上層)底下,子標題文字必須唯一(MD024 siblings_only 模式)。
  • 不同父標題底下,子標題允許重名。
  • 標題前後需保留空行(MD022),mdtools fmt --fix 自動補。
  • 標題結尾禁止標點(MD026)— 禁用字元:.,:;。允許 ?! 作為語氣結尾。mdtools fmt --fix 自動去除結尾禁用標點。
  • 禁止用粗體當標題(MD036)— 若段落整段只由 **文字***文字* 組成,視為視覺性標題濫用。mdtools lint 只報警、不自動修;作者需手動判斷正確的標題層級(通常是 H3 / H4)並改寫。

2.2 補充範例:MD026 與 MD036 的典型誤用

MD026(標題尾標點)常見誤用:

1#### 字型選擇說明:        ← 違規(結尾 `:`)
2#### 字型選擇說明          ← 合法

中文寫作習慣用冒號引入後續內容,這個模式在「段首句」合理、在「標題」就不合理 — 標題本身的存在就暗示了後續有內容,冒號變成冗餘訊號。

MD036(粗體當標題)常見誤用:

1**字型選擇說明**           ← 違規:整段只有粗體,視覺像標題但不是真標題
2
3這段內容...
4
5### 字型選擇說明           ← 合法:用正式的 H3 取代

差異看起來微小,實際影響包含:Hugo TOC 不會抓到、卡片反向連結失效、screen reader 無法跳轉。這是「語義 vs 視覺」錯位的典型案例,AST linter 容易檢出(Paragraph 節點唯一子節點為 Strong/Emph)。

2.3 為什麼採 siblings_only 而非全域唯一

平行結構(多案例、多模板章節)的複用語義來自上層標題賦予的脈絡。例如:

1## 【案例一】Uber 2022
2### 弱點環節        ← 合法
3### 攻擊路徑
4
5## 【案例二】Okta 2023
6### 弱點環節        ← 合法,因為在不同的父層下
7### 攻擊路徑

重名只有在同層並列時才代表結構錯誤。強制全域唯一會逼作者寫 ### 【案例二】弱點環節,破壞平行結構的視覺一致性,收益並不大。


3. URL 與連結規則

3.1 裸 URL 轉換(mdtools fmt --fix 自動處理)

段落或表格儲存格內的裸 URL 會自動包成 markdown 連結。顯示文字依路徑可識別性分級:

情境顯示文字範例(before → after)
路徑含識別碼(例如 CVE)domain.com/識別碼https://nvd.nist.gov/vuln/detail/CVE-2023-34362[nvd.nist.gov/CVE-2023-34362](https://nvd.nist.gov/vuln/detail/CVE-2023-34362)
路徑冗長但無識別性domain.comhttps://www.cisa.gov/news-events/alerts/2024/06/03/snowflake-recommends-...[cisa.gov](https://www.cisa.gov/news-events/alerts/2024/06/03/snowflake-recommends-...)
已是 markdown 連結不動

識別碼偵測用 regex 白名單,初始清單專注在高頻識別碼格式(例如 CVE-YYYY-N),其他格式以「遇到再加」原則擴充。清單維護在 scripts/mdtools/internal/rules/identifiers.go

3.2 反釣魚校驗(mdtools lint 強制檢查)

Markdown 語法允許顯示文字與實際 href 完全不符,這是釣魚攻擊的結構基礎。本規則在 AST 層阻擋此模式。

  • R-URL-1(URL 樣顯示文字一致性):若顯示文字含 .com / .org / .gov / .net / .io / .dev / .tw 等 TLD 字樣,則顯示文字的 domain 必須等於 href 的 domain(含子網域比對)。
  • R-URL-2(描述型顯示文字自由):顯示文字不含 TLD 字樣時,視為人類可讀描述,不做 domain 比對。

違規範例(會被 lint 阻擋):

1[nvd.nist.gov](https://malicious.example.com/fake)     ← 顯示文字暗示 NVD,href 卻不是
2[cisa.gov/advisory](https://cisa-gov.evil.example)     ← 顯示文字抄 CISA 格式,domain 不符

合法範例:

1[Uber 事件公告](https://www.uber.com/newsroom/security-update/)
2[nvd.nist.gov/CVE-2023-34362](https://nvd.nist.gov/vuln/detail/CVE-2023-34362)

這條規則在紅隊 / 安全相關教材中特別重要:讀者本來就該對來源警戒,排版規則不該削弱這個警戒訊號。縮短顯示文字提升可讀性,反釣魚校驗守住安全底線,兩者互補。

3.3 例外情境

  • 程式碼區塊(fenced code block,``` 包圍)內的 URL 不做任何處理(不縮短、不校驗)。代碼範例經常需要展示完整 URL 給讀者複製執行。
  • 引用區塊> 開頭)內的 URL 比照段落處理,會縮短也會做反釣魚校驗。

4. 表格規則

  • 統一使用 aligned 風格:每欄內容用空白補齊到該欄的最大寬度,使 | 在 monospace 渲染下垂直對齊。
  • 欄位分隔線使用 | --- | 形式,不含對齊冒號 :(分隔線內的 - 數量跟隨該欄寬度自動填足)。
  • 寬度計算使用顯示寬度(display width)— CJK 字元佔 2 欄寬、ASCII 佔 1 欄寬,分隔列與資料列按同一套寬度對齊。
  • mdtools fmt --fix 自動正規化:插入新行或改動欄寬時會全表重算,作者不需手工維持對齊。

選 aligned 而非 compact 的理由是原始檔可讀性:技術教材的表格常需在 code review 裡對照,aligned 風格讓 reviewer 直接看出哪些欄位對應哪些內容,不用在腦中解析鋸齒狀的 pipes。手工對齊在長表格反覆編輯時確實會失效(新增一行就全表要重對齊),但這正是 mdtools fmt --fix 接手的地方。


5. 基礎格式細節

這節整理容易被忽略、但會影響 parser 正確性或渲染品質的小規則。

5.1 程式碼區塊必須註明語言(MD040)

mdtools lint 檢查。未註明語言的 fenced code block 會被報警:

 1```                   ← 違規:缺語言標示
 2func main() {
 3    fmt.Println("hi")
 4}
 5```
 6
 7```go                 ← 合法
 8func main() {
 9    fmt.Println("hi")
10}
11```

純文字輸出(例如 terminal output、log 片段)使用 textplain

1```text
2Error: permission denied
3```

Shell 範例統一用 bash(即使是 zsh 語法,讓 syntax highlighter 有合理預設);純設定檔依實際格式(tomlyamljsonini)。

5.2 fenced code block 前後需空行(MD031)

mdtools fmt --fix 自動處理。缺空行會讓前後段落被 parser 併入 code block 或反之。

5.3 列表前後需空行(MD032)

mdtools fmt --fix 自動處理。

 1上一段結束。
 2- 列表項一           ← 違規:列表前無空行,會被部分 parser 當段落延續
 3- 列表項二
 4
 5上一段結束。
 6
 7- 列表項一           ← 合法
 8- 列表項二
 9
10下一段開始。

5.4 有序列表編號一致性(MD029)

mdtools fmt --fix 正規化。本專案採 ordered 風格(全部遞增編號):

11. 第一步
22. 第二步           ← 合法
33. 第三步
4
51. 第一步
61. 第二步           ← 違規:混用風格(fmt --fix 會改成 1./2./3.)
72. 第三步

選擇 ordered 的理由:原始檔可讀性高,作者直接看到步驟數;插入新項目的對齊代價比全部重新渲染低。

5.5 段落間空行

段落之間、標題前後、列表與段落之間都需空行。mdtools fmt --fix 會自動規範化多餘 / 缺失的空行,作者不需手工維護。

5.6 檔案結尾需有換行(MD047)

POSIX 文字檔規範;缺失時 git diff 會出現 \ No newline at end of filemdtools fmt --fix 自動補。

5.7 Tab 字元(MD010)— 僅限 fenced code block

mdtools lint 檢查(warn 等級)。Prose / 列表 / 表格 / 引用等非 code-block 行內若出現 tab 字元,會被標記並建議改成空白;fenced code block 內的 tab 保留(Go 原始碼依 gofmt 慣例用 tab,文章要讓讀者能直接複製貼用)。

Repo 根目錄的 .markdownlint.json"MD010": { "code_blocks": false } 告知 IDE 的 markdownlint extension 採用同一套 policy,讓編輯器跟 CI 的警告保持一致。

5.8 行長度上限(MD013)— 預設關閉

本規則預設關閉。中英混用的技術寫作不適用 80-char 慣例:

  • 中文每字元算 1 個寬度時,80-char ≈ 40 個中文字,寫到一半就要斷行,嚴重影響可讀性。
  • 中文每字元算 2 個寬度時,80-char 相當於 20-30 個中文字,更離譜。
  • Markdown 編輯器普遍支援軟斷行與 IDE word wrap,實體行長度對閱讀體驗影響小。

若未來需要打開(例如發現真的有人寫出 2000-char 單行段落),建議上限 400 字元(軟上限,warn 不阻擋)。設定在 scripts/mdtools/internal/rules/config.goLineLengthLimit 欄位。

5.9 裝飾符號禁用(emoji / 視覺記號)

本節本身豁免:規範要描述「哪些符號禁用」必然要列舉這些符號(use-mention distinction)。本節舉例的 emoji 屬 mention(指稱)、非 use(裝飾使用)、不違反規則。掃描指令會 hit 到本節、判讀時跳過。

content/** 正文不可使用 emoji(如 ✅ ❌ ⚠️ 🚨 🟡 🟢 ⭐ 📌 💡 ⚡ 🎯)與裝飾性 unicode 符號(✓ ✗ ✘)。表格、列表、行內標記都不行

替換策略(emoji 承載的語意要回到文字結構、不是純粹刪除符號):

原寫法改成
表格 status | ✅ 解了 |純文字描述:「解了」/「是」/「適用」
表格 status | ❌ 漏 |純文字描述:「漏」/「否」/「不適用」
列表優缺點 - ✅ 簡單 / - ❌ 慢拆成 **優點**:簡單 / **缺點**:慢 段落或標題段
列表錯誤示範 - ❌ 把 key 寄 email / - ✅ 用 CSR拆成 **錯誤做法**: / **正確做法**: 標題段
行內視覺強調 🚨 criticalmarkdown 粗體 **critical** 或引用塊 > **critical**:...

理由

  • Grep-ability:emoji 無法用 plain text grep 命中;視覺結構容易掩蓋語意結構、reviewer 看不出「優 / 缺」是用 emoji 區分還是用標題段區分
  • CLI parser 相容性:部分 multi-byte emoji 在 Rust-based CLI 工具(如某些 mdtools / pagefind / lint pipeline)觸發 char-boundary panic
  • 跨語境穩定:emoji 在不同字型 / 平台 / 終端機渲染差異大、容易斷行或顯示為框

掃描指令(提交前自己跑一次、有 hit 就替換):

1rg "✅|❌|⚠️|🚨|🟡|🟢|🔴|🟠|🔵|⭐|📌|💡|⚡|🎯|✨|📝|🔍|🛠|⛔|✓|✗|✘" content/

本規則目前未進 mdtools lint 自動掃描、靠人工 grep。未來會加進 lint pipeline。


5.10 位置引用與數量命名候選掃描(REF1 / REF2、警告層)

mdtools lintcontent/** 跑兩個警告層掃描、來源是引用紀律卡(#155 引用章節用語意標題#156 集合命名用角色):

  • REF1-positional-anchor:正文中的位置式引用候選 —「見第 3 點」「詳見第五章」「§4」。位置編號是當下排列的衍生值、目標是活文件時、結構重排會讓引用 silent 指向錯的內容。
  • REF2-count-in-name:標題與 front matter title 中內嵌成員數的集合命名 —「六大原則」「遷移五階段流程」。成員增減時名稱先失真、且名稱是被複製最多次的字串。

兩個規則都停在警告層、命中是候選、不是判決 — 回報前要做語意判定:

命中情境判定
引用發布方凍結的編號(RFC 段號、法條條號)合規、編號是 fact
數字緊鄰它描述的清單(「確認三件事:」)合規、漂移在編輯當下可見
外部凍結品牌名(SOLID 五原則)合規、數量由發布方凍結
目標是內部活文件 / 內部活集合改語意標題引用 / 角色命名

掃描器內建兩個自動豁免:被 「」 包住的命中視為反例引用、直接跳過;「第」開頭的序數(第三階段)不屬 REF2 的集合命名。既有內容的命中屬歷史基線、依「changed-set scoped lint」原則處理 — 新增與修改的檔案要判讀、存量警告分開回報。

5.11 否定起手候選掃描(POS-negation-lead、警告層)

mdtools lintcontent/** 跑否定起手掃描、來源是 #166 重點優先陳述是跨語言的資訊結構原則(搭配 #165 register 違規要文體異源):

  • POS-negation-lead:正文中「不是 X、而是 Y」「不是 X — 是 Y」「與其 X、不如 Y」的重點後置候選。核心概念(Y)被擠到「而是 / 不如」之後、讀者要先處理一個被否定的 X 才拿到重點。這是資訊結構效率問題、跨語言成立(英文「not X but Y」、日文「X ではなく Y」)、不是中文特有句型 — 偵測可機械化、判定不可。pattern 涵蓋的連接詞(而是 / 「— 是」/ 不如)枚舉不完、判準是「核心概念在不在句首」而非哪個連接詞 — 漏掉的變體只是讓候選 silent 到有人讀到(規則第一版就漏了「不是 X — 是 Y」、靠人發現才補)。

判定用「重點位置」、命中是候選、不是判決

命中情境判定
核心概念第一次正面出現在句首(「有深度、不是非黑即白的二元」)合規、重點先行
明示反例 / 對照段落內的否定(見 #94合規、否定是對照本體
核心概念被擠到「而是」之後(「不是二元、而是有深度」)改正向、把核心概念移到句首

掃描器豁免兩類命中:被 「」 包住的引用(反例引用 / 句型佔位符)、以及 backtick 行內程式碼內的 pattern(grep regex / 技術識別碼)。講這個句型的 meta 卡(#165 / #166)與本規範段落會大量自我觸發 — 屬候選人判定豁免、跟 REF1 / REF2 對 #155 / #156 一致。既有內容的命中(全 content/ 約數百個)屬歷史基線、依「changed-set scoped lint」原則處理 — 偵測把候選池曝光、判定漸進進行、存量警告分開回報。

6. Front matter schema(mdtools lint

Hugo 依賴 YAML front matter 提供 title / date / weight 等欄位給 render pipeline。缺欄位會讓列表頁、排序、SEO 壞掉,但 Hugo 本身不會失敗(靜默接受不完整資料),所以必須由 linter 守住。

6.1 通用層(content/**/*.md

所有內容文章必須有:

  • title:字串,不可空。
  • dateYYYY-MM-DD 格式(ISO 8601 date)。

Hugo _index.md section 頁面例外:這類檔案是 Hugo 的 section 列表 landing page,不是內容文章,沒有語意上的「日期」。只要求 title,不強制 date

6.2 推薦層(警告,不阻擋)

推薦填寫(mdtools lint warn level):

  • description:字串,建議 30–150 字,影響 SEO 與列表頁預覽。
  • tags:陣列,至少 1 個標籤。

推薦層是歷史內容的緩衝區,不是新增內容的放行條件。新增文章必須同時填寫 descriptiontags;修改既有文章時,若同一檔案缺少推薦欄位,應在同次變更補齊,避免每次驗證都被舊 warning 淹沒。

驗證時先跑 changed-set scoped lint 判斷本次變更品質,再視需要跑 full lint 觀察整體基線。回報 full lint 結果時,要把歷史 warning、已知 warning 與本次新增問題分開描述。

6.3 卡片嚴格層(content/backend/knowledge-cards/**

知識卡片額外要求(對應 .codex/briefs/knowledge-cards.md K2):

  • titledatedescription 必填。
  • weight:整數,決定在 _index.md 主題表格中的排序位置。

6.4 禁止欄位

以下欄位存在時 mdtools lint 警告(避免語義混淆):

  • author:本專案為單作者 blog,統一於 Hugo 設定。
  • permalink:使用 Hugo 預設路徑規則,避免手動覆蓋。

若未來需要鬆綁,在 scripts/mdtools/internal/rules/frontmatter.goDisallowedFields 清單調整。

6.5 slug 必填、跟檔名對齊

所有 content 文章 frontmatter 必須有 slug 欄位,值跟檔名(不含 .md)對齊。

1---
2title: "視覺手段對齊錯誤層次"
3slug: "visual-tool-error-layer-alignment"   # 跟檔名對齊
4date: 2026-04-28
5---

為什麼必填

slug 是 URL 的核心識別、跨多個工具共用(Hugo build、mdtools lint、跨檔 markdown link、search index)。若不顯式定義,slug 散落在三處推導鏈:

來源推導值
Hugo 預設(從 title 用 urlize)runtime 推導、隨 hugo 版本變化
mdtools 字面比對檔名 stem
跨檔連結時的引用寫作者手動算 / 複製

三個推導鏈不一致時 = silent broken link(mdtools pass 但 hugo build 後 404、或反過來)。把 slug 升成 frontmatter 顯式 fact、所有工具基於同一 source、消除 derivation 鏈。

詳細論述見 report #93 URL slug 必須顯式定義為 fact

檔名對齊規則

  • 檔名命名建議:英文小寫、kebab-case 或 snake_case、不含中文(避免 hugo urlize 規則跨版本變動)
  • slug 值 == 檔名 stem(不含 .md
  • 修檔名時必須同步修 slug;修 slug 時必須同步 rename 檔案

Hugo _index.md 例外:section 列表頁已有 slug: 欄位指定資料夾路徑、不適用本規則。


7. 卡片雙向完整性(mdtools cards

作用範圍:content/**/*.md,重點關注 content/backend/knowledge-cards/

層級規則實作
L1 連結有效性所有相對連結 [...](/posts/markdown-writing-spec/path) / [...](/posts/path) 的目標檔案必須存在AST 抽 Link node → 解析相對路徑 → stat 檔案
L2 卡片 orphan 偵測每張卡片至少被 content/** 中一篇非卡片正文引用建反向索引 → 找無 incoming edge 的卡片
L4 卡片 K4 結構合規卡片首段與「概念位置」段各至少 1 個相鄰卡片連結AST 定位段落節點 → 統計子樹 Link 數

L3(正文首次出現術語必須連結到卡片)暫不納入,待術語字典(.codex/briefs/knowledge-web-expansion.md)啟動後再開。

為什麼要做跨文件檢查

知識卡片是 blog 的核心知識資產。隨著卡片數量增加:

  • Orphan 卡片(沒有正文連結進來)會變成知識死角,讀者無法發現。
  • 斷掉的相對連結(檔案被改名或移動)肉眼難以發現,只有讀者點擊失敗才暴露。
  • K4 合規(首段 + 概念位置段要有鄰卡連結)保證卡片間的知識網不會鬆散。

這些檢查用 regex 做都卡在「段落歸屬怎麼判斷」。AST 天生知道節點的父子結構,做起來自然。


8. 執行時機

Pre-commit hook(.githooks/pre-commit

  1. mdtools fmt --fix — 自動修格式;改動會 git add 回 staged,避免改完又沒進 commit。
  2. mdtools lint — 結構檢查;失敗阻擋 commit。
  3. mdtools cards — 完整性檢查;失敗阻擋 commit。

Pre-push hook(.githooks/pre-push

pre-push 的責任是把 CI 同款全量檢查提前到本機。pre-commit 為了速度只處理 staged markdown;pre-push 會跑 make check,也就是 mdtools fmt --check content/mdtools lint content/mdtools cards content/,讓整個 content/ 的格式與連結 drift 在推送前被攔下。

啟用 hook:

1git config core.hooksPath .githooks
2# 或:make install-hooks

CI(.github/workflows/md-check.yml

三個子命令都跑 --check / 嚴格模式,任何違規 fail CI。


9. 寫作者使用指引

  • 寫作時優先遵循本規範。pre-commit / pre-push 報錯時讀訊息修正;不可用 git commit --no-verify 或跳過 hook 的方式繞過檢查
  • 新增案例平行章節(例如多個「工具評測」「事件時序」)時不需登記到任何白名單 — siblings_only 自動判讀。
  • 新增 URL 時優先採用裸 URL 轉換段的分級形式;若顯示文字含 TLD 字樣,確認 domain 與 href 完全一致。
  • 新增卡片時確認首段與「概念位置」段各有至少一個相鄰卡片連結(L4 要求);確認 front matter 含 title / date / description / weight(卡片嚴格層)。
  • 程式碼區塊養成習慣先寫語言標示再填內容;純文字輸出用 text

10. 規則擴充流程

新規則進入本文的路徑:

  1. 先在 scripts/mdtools/internal/rules/ 實作為可開關的 rule(預設關)。
  2. 在代表性檔案上測試誤判率。
  3. 誤判率 < 1% 且有明確教材品質收益時,預設開啟並更新本文。
  4. 預設開啟後同步修正既有違規;若違規數量大,可分批 PR。

11. 為什麼自訂而不是用現成 markdownlint

markdownlint-cli2 的 MD022 / MD024 / MD026 / MD029 / MD031 / MD032 / MD034 / MD036 / MD040 / MD047 / MD060 這些基礎規則都有(MD013 預設關閉、MD025 本規範嚴於原版),為什麼還要自寫?

關鍵差在卡片雙向完整性反釣魚校驗Front matter schema 這三類檢查,屬於跨文件 / AST 層 / 業務邏輯層的自訂邏輯,現成 linter 無法表達。這些檢查是 blog 品質的核心訊號,必須跟基礎格式檢查放在同一個工具鏈、同一次 AST parse 內處理,避免多個工具重複解析、重複維護。

另外 goldmark 是 Hugo 內建的 markdown parser。用同一個 parser 做 lint 保證「lint 通過 → Hugo render 一致」,杜絕兩套 parser 解讀不同的長尾 bug。


本文為 blog 專案 Markdown 寫作規範的單一真實來源。repo 根目錄的 AGENTS.md 引用本文作為排版規範權威,規則與 scripts/mdtools 實作保持同步。