Blog Markdown 寫作規範與 mdtools 檢查
這篇要解決什麼
隨著 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/**/*.md。public/、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.com | https://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 片段)使用 text 或 plain:
1```text
2Error: permission denied
3```Shell 範例統一用 bash(即使是 zsh 語法,讓 syntax highlighter 有合理預設);純設定檔依實際格式(toml、yaml、json、ini)。
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 file。mdtools 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.go 的 LineLengthLimit 欄位。
5.9 裝飾符號禁用(emoji / 視覺記號)
本節本身豁免:規範要描述「哪些符號禁用」必然要列舉這些符號(use-mention distinction)。本節舉例的 emoji 屬 mention(指稱)、非 use(裝飾使用)、不違反規則。掃描指令會 hit 到本節、判讀時跳過。
content/** 正文不可使用 emoji(如 ✅ ❌ ⚠️ 🚨 🟡 🟢 ⭐ 📌 💡 ⚡ 🎯)與裝飾性 unicode 符號(✓ ✗ ✘)。表格、列表、行內標記都不行。
替換策略(emoji 承載的語意要回到文字結構、不是純粹刪除符號):
| 原寫法 | 改成 |
|---|---|
表格 status | ✅ 解了 | | 純文字描述:「解了」/「是」/「適用」 |
表格 status | ❌ 漏 | | 純文字描述:「漏」/「否」/「不適用」 |
列表優缺點 - ✅ 簡單 / - ❌ 慢 | 拆成 **優點**:簡單 / **缺點**:慢 段落或標題段 |
列表錯誤示範 - ❌ 把 key 寄 email / - ✅ 用 CSR | 拆成 **錯誤做法**: / **正確做法**: 標題段 |
行內視覺強調 🚨 critical | markdown 粗體 **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 lint 對 content/** 跑兩個警告層掃描、來源是引用紀律卡(#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 lint 對 content/** 跑否定起手掃描、來源是 #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:字串,不可空。date:YYYY-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 個標籤。
推薦層是歷史內容的緩衝區,不是新增內容的放行條件。新增文章必須同時填寫 description 與 tags;修改既有文章時,若同一檔案缺少推薦欄位,應在同次變更補齊,避免每次驗證都被舊 warning 淹沒。
驗證時先跑 changed-set scoped lint 判斷本次變更品質,再視需要跑 full lint 觀察整體基線。回報 full lint 結果時,要把歷史 warning、已知 warning 與本次新增問題分開描述。
6.3 卡片嚴格層(content/backend/knowledge-cards/**)
知識卡片額外要求(對應 .codex/briefs/knowledge-cards.md K2):
title、date、description必填。weight:整數,決定在_index.md主題表格中的排序位置。
6.4 禁止欄位
以下欄位存在時 mdtools lint 警告(避免語義混淆):
author:本專案為單作者 blog,統一於 Hugo 設定。permalink:使用 Hugo 預設路徑規則,避免手動覆蓋。
若未來需要鬆綁,在 scripts/mdtools/internal/rules/frontmatter.go 的 DisallowedFields 清單調整。
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)
mdtools fmt --fix— 自動修格式;改動會git add回 staged,避免改完又沒進 commit。mdtools lint— 結構檢查;失敗阻擋 commit。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-hooksCI(.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. 規則擴充流程
新規則進入本文的路徑:
- 先在
scripts/mdtools/internal/rules/實作為可開關的 rule(預設關)。 - 在代表性檔案上測試誤判率。
- 誤判率 < 1% 且有明確教材品質收益時,預設開啟並更新本文。
- 預設開啟後同步修正既有違規;若違規數量大,可分批 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 實作保持同步。