<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Markdown on Tarragon</title><link>https://tarrragon.github.io/blog/tags/markdown/</link><description>Recent content in Markdown on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 15 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/markdown/index.xml" rel="self" type="application/rss+xml"/><item><title>文章列表</title><link>https://tarrragon.github.io/blog/posts/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/</guid><description>&lt;p>這個資料夾收錄 blog 本身的&lt;strong>規範文件&lt;/strong>、&lt;strong>設計/架構筆記&lt;/strong>，以及不屬於特定語言教材區（&lt;code>content/backend/&lt;/code>、&lt;code>content/go/&lt;/code>、&lt;code>content/python/&lt;/code> 等）的雜項技術筆記。&lt;/p>
&lt;p>內容大致分三類：&lt;/p>
&lt;p>&lt;strong>規範與契約&lt;/strong> — agent / 工具鏈行為的單一真實來源，被 &lt;code>AGENTS.md&lt;/code> 或其他 config 引用：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查&lt;/a> — 排版規則、反釣魚校驗、卡片雙向完整性的工具化契約&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/blog-article-template-design/" data-link-title="Blog 文章模板設計：作者品質閘門與正文分工" data-link-desc="文章模板的定位與 SSoT 歸屬：模板是作者品質閘門、正文仍走技術推導、backend 正文不暴露填表結構。">Blog 文章模板設計：作者品質閘門與正文分工&lt;/a> — 文章模板的 blog-specific SSoT，供人類作者、Claude Code 與 Codex 共用&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST — 從字串到語法樹的視角轉換&lt;/a> — 為什麼 blog 選 AST-based linter 而非 regex&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &amp;#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &amp;#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools：Go + goldmark 的 markdown 工具鏈設計&lt;/a> — 子命令架構、語言選擇 tripwire、pre-commit 與 CI 整合&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Hugo 與 Markdown 操作經驗&lt;/strong> — 具體寫作與渲染問題的事故紀錄。&lt;/p>
&lt;p>&lt;strong>AI 協作與工程心得&lt;/strong> — CI 自動除錯、技術寫作結構、專案經營相關反思。&lt;/p>
&lt;p>底下自動列出本資料夾的所有文章，依日期排序。&lt;/p></description><content:encoded><![CDATA[<p>這個資料夾收錄 blog 本身的<strong>規範文件</strong>、<strong>設計/架構筆記</strong>，以及不屬於特定語言教材區（<code>content/backend/</code>、<code>content/go/</code>、<code>content/python/</code> 等）的雜項技術筆記。</p>
<p>內容大致分三類：</p>
<p><strong>規範與契約</strong> — agent / 工具鏈行為的單一真實來源，被 <code>AGENTS.md</code> 或其他 config 引用：</p>
<ul>
<li><a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查</a> — 排版規則、反釣魚校驗、卡片雙向完整性的工具化契約</li>
<li><a href="/blog/posts/blog-article-template-design/" data-link-title="Blog 文章模板設計：作者品質閘門與正文分工" data-link-desc="文章模板的定位與 SSoT 歸屬：模板是作者品質閘門、正文仍走技術推導、backend 正文不暴露填表結構。">Blog 文章模板設計：作者品質閘門與正文分工</a> — 文章模板的 blog-specific SSoT，供人類作者、Claude Code 與 Codex 共用</li>
<li><a href="/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST — 從字串到語法樹的視角轉換</a> — 為什麼 blog 選 AST-based linter 而非 regex</li>
<li><a href="/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools：Go + goldmark 的 markdown 工具鏈設計</a> — 子命令架構、語言選擇 tripwire、pre-commit 與 CI 整合</li>
</ul>
<p><strong>Hugo 與 Markdown 操作經驗</strong> — 具體寫作與渲染問題的事故紀錄。</p>
<p><strong>AI 協作與工程心得</strong> — CI 自動除錯、技術寫作結構、專案經營相關反思。</p>
<p>底下自動列出本資料夾的所有文章，依日期排序。</p>
]]></content:encoded></item><item><title>Blog 文章模板設計：作者品質閘門與正文分工</title><link>https://tarrragon.github.io/blog/posts/blog-article-template-design/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/blog-article-template-design/</guid><description>&lt;h2 id="問題定位">問題定位&lt;/h2>
&lt;p>文章模板的責任是穩定作者流程，正文的責任是承載技術文章本身。讀者進入文章時，需要看到概念、判讀、取捨與路由；作者在寫作時，需要一組欄位檢查文章是否具備可維護的最低結構。&lt;/p>
&lt;p>本 blog 同時由人類作者、Claude Code 與 Codex 協作產生內容。模板若只放在單一 agent 的設定裡，就會形成工具分岔；模板若直接放進 backend 正文，又會把作者工作流暴露成讀者負擔。因此模板的單一真實來源放在 &lt;code>content/posts/&lt;/code>，作為本 blog 專屬的寫作設定記錄。&lt;/p>
&lt;h2 id="放置決策">放置決策&lt;/h2>
&lt;p>模板放置位置的核心判準是讀者與維護者是否一致。backend 文章面向技術讀者，report 面向可重用事後檢討，posts 面向 blog 自身的規範、設計與工具鏈紀錄。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>適合內容&lt;/th>
 &lt;th>本議題判斷&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>content/backend/&lt;/code>&lt;/td>
 &lt;td>技術文章正文、概念推導、案例分析&lt;/td>
 &lt;td>保持讀者主線，作者模板留在上游&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>content/report/&lt;/code>&lt;/td>
 &lt;td>從具體 case 抽出的工程原則&lt;/td>
 &lt;td>可寫抽象原則，操作模板留在 posts&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>content/posts/&lt;/code>&lt;/td>
 &lt;td>blog 規範、設計決策、工具鏈契約&lt;/td>
 &lt;td>作為模板設計的 SSoT&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>.claude/&lt;/code>&lt;/td>
 &lt;td>Claude Code 執行規則&lt;/td>
 &lt;td>可引用本文，本文維持語意來源&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>.codex/&lt;/code>&lt;/td>
 &lt;td>Codex 執行規則&lt;/td>
 &lt;td>可引用本文，本文維持語意來源&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個分工讓模板同時支援 Claude Code 與 Codex，也保留文章對讀者的自然技術敘事。&lt;/p>
&lt;h2 id="模板責任">模板責任&lt;/h2>
&lt;p>模板是品質閘門，責任是讓文章保留關鍵判準。它要檢查文章是否具備責任、判讀、風險、邊界與下一步路由，正文仍用技術文章的推導順序排列。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模板欄位&lt;/th>
 &lt;th>作者要回答的問題&lt;/th>
 &lt;th>正文呈現方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>責任&lt;/td>
 &lt;td>這篇文章解決哪一類工程問題&lt;/td>
 &lt;td>概念定位或開頭原則段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>判讀&lt;/td>
 &lt;td>讀者如何知道自己遇到這個問題&lt;/td>
 &lt;td>核心判讀、判讀訊號、表格&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>風險&lt;/td>
 &lt;td>判讀錯誤或缺漏會造成什麼代價&lt;/td>
 &lt;td>風險段、反模式、情境段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>邊界&lt;/td>
 &lt;td>這篇文章處理到哪裡，交給誰接續&lt;/td>
 &lt;td>交接路由、與其他章節分工&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回寫&lt;/td>
 &lt;td>案例或事故教訓應回寫到哪個章節&lt;/td>
 &lt;td>下一步路由、復盤段&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>模板欄位可以出現在文章大綱、作者檢查清單或 agent brief 裡。正文要以技術文章方式展開，讓讀者看到推導，作者填表痕跡留在工作流內部。&lt;/p>
&lt;h2 id="backend-技術文章最小模板">Backend 技術文章最小模板&lt;/h2>
&lt;p>Backend 技術文章的最小模板是「概念定位 → 核心判讀 → 判讀訊號 → 風險與邊界 → 交接路由」。這組欄位適合 04 / 06 / 08 這類語言無關的後端能力文章。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="gu">## 概念定位
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">{概念} 是 {能力 / 流程 / 控制面}，責任是 {它在系統中承擔什麼問題}。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">這一頁處理的是 {抽象層級}。它跟 {相鄰章節} 的分工是 {邊界}。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="gu">## 核心判讀
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">判讀 {概念} 時，先看 {第一判準}，再看 {第二判準}。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">重點訊號包括：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> {訊號 1}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> {訊號 2}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> {訊號 3}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">| 判讀面向 | 最小可用判準 | 常見失真 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">| --- | --- | --- |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">| {面向} | {判準} | {失真} |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="gu">## 判讀訊號
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> {真實服務中會看到的徵兆}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> {工程團隊會踩到的操作問題}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> {事故或演練會暴露的缺口}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="gu">## 交接路由
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> {上游章節}：{承接內容}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">- {下游章節}：{下一步處理}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這份模板只定義文章最低結構。若某篇文章需要完整案例、方案比較或實作步驟，正文可以增加章節；增加章節時仍要保留責任、判讀、風險與路由。&lt;/p>
&lt;h2 id="案例前置欄位">案例前置欄位&lt;/h2>
&lt;p>案例前置欄位的責任是讓服務案例能回寫到文章系統。它屬於作者拆案例時的內部欄位，正文只吸收欄位背後的判讀與取捨。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>來源&lt;/td>
 &lt;td>案例來自事故、演練或公開實踐&lt;/td>
 &lt;td>post-incident review、SRE case&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>觸發&lt;/td>
 &lt;td>事件如何被發現&lt;/td>
 &lt;td>alert、customer ticket、vendor status&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence&lt;/td>
 &lt;td>判讀使用哪些證據&lt;/td>
 &lt;td>log、metric、trace、audit log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Decision&lt;/td>
 &lt;td>當時做了什麼取捨&lt;/td>
 &lt;td>rollback、degradation、containment&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Impact&lt;/td>
 &lt;td>影響到誰與什麼功能&lt;/td>
 &lt;td>tenant、region、feature、financial impact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回寫&lt;/td>
 &lt;td>教訓回到哪個章節&lt;/td>
 &lt;td>04.17、6.20、8.19&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>04 / 06 / 08 的案例拆解可共用這組欄位，但正文仍要用技術文章敘事。欄位幫作者維持可比性，文章幫讀者理解機制。&lt;/p></description><content:encoded><![CDATA[<h2 id="問題定位">問題定位</h2>
<p>文章模板的責任是穩定作者流程，正文的責任是承載技術文章本身。讀者進入文章時，需要看到概念、判讀、取捨與路由；作者在寫作時，需要一組欄位檢查文章是否具備可維護的最低結構。</p>
<p>本 blog 同時由人類作者、Claude Code 與 Codex 協作產生內容。模板若只放在單一 agent 的設定裡，就會形成工具分岔；模板若直接放進 backend 正文，又會把作者工作流暴露成讀者負擔。因此模板的單一真實來源放在 <code>content/posts/</code>，作為本 blog 專屬的寫作設定記錄。</p>
<h2 id="放置決策">放置決策</h2>
<p>模板放置位置的核心判準是讀者與維護者是否一致。backend 文章面向技術讀者，report 面向可重用事後檢討，posts 面向 blog 自身的規範、設計與工具鏈紀錄。</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>適合內容</th>
          <th>本議題判斷</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>content/backend/</code></td>
          <td>技術文章正文、概念推導、案例分析</td>
          <td>保持讀者主線，作者模板留在上游</td>
      </tr>
      <tr>
          <td><code>content/report/</code></td>
          <td>從具體 case 抽出的工程原則</td>
          <td>可寫抽象原則，操作模板留在 posts</td>
      </tr>
      <tr>
          <td><code>content/posts/</code></td>
          <td>blog 規範、設計決策、工具鏈契約</td>
          <td>作為模板設計的 SSoT</td>
      </tr>
      <tr>
          <td><code>.claude/</code></td>
          <td>Claude Code 執行規則</td>
          <td>可引用本文，本文維持語意來源</td>
      </tr>
      <tr>
          <td><code>.codex/</code></td>
          <td>Codex 執行規則</td>
          <td>可引用本文，本文維持語意來源</td>
      </tr>
  </tbody>
</table>
<p>這個分工讓模板同時支援 Claude Code 與 Codex，也保留文章對讀者的自然技術敘事。</p>
<h2 id="模板責任">模板責任</h2>
<p>模板是品質閘門，責任是讓文章保留關鍵判準。它要檢查文章是否具備責任、判讀、風險、邊界與下一步路由，正文仍用技術文章的推導順序排列。</p>
<table>
  <thead>
      <tr>
          <th>模板欄位</th>
          <th>作者要回答的問題</th>
          <th>正文呈現方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>責任</td>
          <td>這篇文章解決哪一類工程問題</td>
          <td>概念定位或開頭原則段</td>
      </tr>
      <tr>
          <td>判讀</td>
          <td>讀者如何知道自己遇到這個問題</td>
          <td>核心判讀、判讀訊號、表格</td>
      </tr>
      <tr>
          <td>風險</td>
          <td>判讀錯誤或缺漏會造成什麼代價</td>
          <td>風險段、反模式、情境段</td>
      </tr>
      <tr>
          <td>邊界</td>
          <td>這篇文章處理到哪裡，交給誰接續</td>
          <td>交接路由、與其他章節分工</td>
      </tr>
      <tr>
          <td>回寫</td>
          <td>案例或事故教訓應回寫到哪個章節</td>
          <td>下一步路由、復盤段</td>
      </tr>
  </tbody>
</table>
<p>模板欄位可以出現在文章大綱、作者檢查清單或 agent brief 裡。正文要以技術文章方式展開，讓讀者看到推導，作者填表痕跡留在工作流內部。</p>
<h2 id="backend-技術文章最小模板">Backend 技術文章最小模板</h2>
<p>Backend 技術文章的最小模板是「概念定位 → 核心判讀 → 判讀訊號 → 風險與邊界 → 交接路由」。這組欄位適合 04 / 06 / 08 這類語言無關的後端能力文章。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gu">## 概念定位
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">{概念} 是 {能力 / 流程 / 控制面}，責任是 {它在系統中承擔什麼問題}。
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">這一頁處理的是 {抽象層級}。它跟 {相鄰章節} 的分工是 {邊界}。
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="gu">## 核心判讀
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">判讀 {概念} 時，先看 {第一判準}，再看 {第二判準}。
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">重點訊號包括：
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">-</span> {訊號 1}
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">-</span> {訊號 2}
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">-</span> {訊號 3}
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">| 判讀面向 | 最小可用判準 | 常見失真 |
</span></span><span class="line"><span class="ln">18</span><span class="cl">| --- | --- | --- |
</span></span><span class="line"><span class="ln">19</span><span class="cl">| {面向} | {判準} | {失真} |
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="gu">## 判讀訊號
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="k">-</span> {真實服務中會看到的徵兆}
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="k">-</span> {工程團隊會踩到的操作問題}
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="k">-</span> {事故或演練會暴露的缺口}
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="gu">## 交接路由
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="k">-</span> {上游章節}：{承接內容}
</span></span><span class="line"><span class="ln">30</span><span class="cl">- {下游章節}：{下一步處理}</span></span></code></pre></div><p>這份模板只定義文章最低結構。若某篇文章需要完整案例、方案比較或實作步驟，正文可以增加章節；增加章節時仍要保留責任、判讀、風險與路由。</p>
<h2 id="案例前置欄位">案例前置欄位</h2>
<p>案例前置欄位的責任是讓服務案例能回寫到文章系統。它屬於作者拆案例時的內部欄位，正文只吸收欄位背後的判讀與取捨。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>用途</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>來源</td>
          <td>案例來自事故、演練或公開實踐</td>
          <td>post-incident review、SRE case</td>
      </tr>
      <tr>
          <td>觸發</td>
          <td>事件如何被發現</td>
          <td>alert、customer ticket、vendor status</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>判讀使用哪些證據</td>
          <td>log、metric、trace、audit log</td>
      </tr>
      <tr>
          <td>Decision</td>
          <td>當時做了什麼取捨</td>
          <td>rollback、degradation、containment</td>
      </tr>
      <tr>
          <td>Impact</td>
          <td>影響到誰與什麼功能</td>
          <td>tenant、region、feature、financial impact</td>
      </tr>
      <tr>
          <td>回寫</td>
          <td>教訓回到哪個章節</td>
          <td>04.17、6.20、8.19</td>
      </tr>
  </tbody>
</table>
<p>04 / 06 / 08 的案例拆解可共用這組欄位，但正文仍要用技術文章敘事。欄位幫作者維持可比性，文章幫讀者理解機制。</p>
<h2 id="agent-共用方式">Agent 共用方式</h2>
<p>Claude Code 與 Codex 共用模板時，本文是 blog 內的穩定契約。<code>.claude/</code> 與 <code>.codex/</code> 可以引用本文的欄位與判準，但實際模板語意以本文為準。</p>
<table>
  <thead>
      <tr>
          <th>使用者</th>
          <th>使用方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>人類作者</td>
          <td>寫作前確認文章是否需要這組欄位</td>
      </tr>
      <tr>
          <td>Claude Code</td>
          <td>依本文判斷正文與作者模板的分工</td>
      </tr>
      <tr>
          <td>Codex</td>
          <td>依本文建立、補寫與檢查 content 文章</td>
      </tr>
      <tr>
          <td>mdtools</td>
          <td>檢查 Markdown 結構與連結，語意模板交給作者流程</td>
      </tr>
  </tbody>
</table>
<p>這個安排避免 <code>.claude/</code> 與 <code>.codex/</code> 各自演化成不同模板，也避免把 agent 操作細節寫進 backend 讀者正文。</p>
<h2 id="使用邊界">使用邊界</h2>
<p>模板適合用在多篇系列文章、跨模組路由與案例回寫。單篇短文、事故紀錄或工具使用筆記可以採較輕的結構，只要仍能說清楚核心責任與下一步。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>使用方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backend 能力章節</td>
          <td>使用完整最小模板</td>
      </tr>
      <tr>
          <td>服務案例拆解</td>
          <td>使用案例前置欄位，正文呈現案例判讀與取捨</td>
      </tr>
      <tr>
          <td>Blog 工具鏈規範</td>
          <td>依主題調整，保留 posts 的規範與工具鏈定位</td>
      </tr>
      <tr>
          <td>Report 原則卡</td>
          <td>依 report 固有結構，維持 case-driven 原則抽象</td>
      </tr>
      <tr>
          <td>Skill reference</td>
          <td>使用 skill 自身 portable 結構，維持跨專案可移植</td>
      </tr>
  </tbody>
</table>
<p>模板開始主導正文時，需要降級成作者檢查清單。文章完成後的檢查重點是讀者能否理解技術推導，並確認欄位已在文章背後支撐判讀。</p>
<h2 id="完稿檢查">完稿檢查</h2>
<p>完稿檢查的責任是確認技術文章維持主線。檢查時先看正文是否能獨立閱讀，再看欄位是否完整支撐交接。</p>
<ul>
<li>首段是否先說概念責任</li>
<li>判讀訊號是否來自真實服務情境</li>
<li>表格項目是否有延伸說明</li>
<li>交接路由是否指向具體章節</li>
<li>案例欄位是否能回寫到 04 / 06 / 08</li>
<li>正文是否保留技術推導，並把欄位轉成讀者可理解的判讀</li>
</ul>
]]></content:encoded></item><item><title>Blog Markdown 寫作規範與 mdtools 檢查</title><link>https://tarrragon.github.io/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/</guid><description>&lt;h2 id="這篇要解決什麼">這篇要解決什麼&lt;/h2>
&lt;p>隨著 blog 文章與知識卡片成長，純靠寫作紀律維持排版一致性越來越不可靠。反覆踩到的問題橫跨兩個層級：&lt;/p>
&lt;p>&lt;strong>結構與安全層級&lt;/strong>（這是工具鏈存在的主要理由）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>裸 URL 在段落與表格中爆版&lt;/strong>（MD034），降低閱讀體驗。&lt;/li>
&lt;li>&lt;strong>表格管線風格混用&lt;/strong>（MD060），同一張表格有的有空白、有的沒有。&lt;/li>
&lt;li>&lt;strong>平行模板章節重複標題&lt;/strong>（MD024），例如多案例文章的 &lt;code>### 弱點環節&lt;/code> 出現 13 次。&lt;/li>
&lt;li>&lt;strong>顯示文字與實際 href 不一致&lt;/strong>（反釣魚）— 不在標準 markdownlint 規則內，但紅隊教材脈絡下必要。&lt;/li>
&lt;li>&lt;strong>卡片雙向完整性&lt;/strong>（orphan 卡片、斷連結、K4 合規）— 跨文件檢查，現成工具做不到。&lt;/li>
&lt;li>&lt;strong>Front matter schema&lt;/strong> — Hugo 依賴 YAML front matter 提供 title / date / weight 等欄位，缺失會破壞列表渲染、排序、SEO。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>基礎格式層級&lt;/strong>（容易被忽略但影響 parser 穩定性或語義結構）：&lt;/p>
&lt;ul>
&lt;li>正文禁止使用 H1（嚴於 MD025）— Hugo front matter &lt;code>title&lt;/code> 已產生 H1。&lt;/li>
&lt;li>標題前後需保留空行（MD022），parser 才能正確識別標題邊界。&lt;/li>
&lt;li>標題結尾禁止標點（MD026）— 例如 &lt;code>## 常見問題：&lt;/code> 應改為 &lt;code>## 常見問題&lt;/code>。&lt;/li>
&lt;li>禁止用 &lt;code>**bold**&lt;/code> 段落當標題（MD036）— 破壞語義階層與 TOC 產生。&lt;/li>
&lt;li>程式碼區塊需註明語言（MD040），影響 syntax highlighting 與 accessibility。&lt;/li>
&lt;li>列表前後需空行（MD032）、fenced code block 前後需空行（MD031）— 否則部分 parser 會把列表吃進段落。&lt;/li>
&lt;li>有序列表編號風格一致（MD029）— 全部 &lt;code>1.&lt;/code> 或全部 &lt;code>1./2./3.&lt;/code>。&lt;/li>
&lt;li>檔案結尾需有換行（MD047），POSIX 規範。&lt;/li>
&lt;li>行長度上限（MD013）— &lt;strong>預設關閉&lt;/strong>，中英混用技術寫作不適用 80-char 慣例。&lt;/li>
&lt;/ul>
&lt;p>前兩類混合在同一份寫作規範裡，因為都由同一個工具鏈檢查、都要落地到相同的 pre-commit hook。純靠紀律記住這十幾條在大型 repo 上不可行，純 regex 又無法穩定處理「平行結構下的標題重複」「卡片段落歸屬」這類語意判斷。因此 blog 專案採用 Go + goldmark AST 做自訂 linter：&lt;code>scripts/mdtools&lt;/code>。本文是 linter 與寫作規範的對齊文件；AGENTS.md 引用本文作為排版規範來源。&lt;/p>
&lt;hr>
&lt;h2 id="1-工具總覽">1. 工具總覽&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>子命令&lt;/th>
 &lt;th>職責&lt;/th>
 &lt;th>改檔&lt;/th>
 &lt;th>觸發時機&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>mdtools fmt [--fix|--check]&lt;/code>&lt;/td>
 &lt;td>格式正規化（URL、表格、空行、列表間距、trailing newline）&lt;/td>
 &lt;td>&lt;code>--fix&lt;/code> 會改&lt;/td>
 &lt;td>pre-commit（&lt;code>--fix&lt;/code>）、pre-push / CI（&lt;code>--check&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mdtools lint&lt;/code>&lt;/td>
 &lt;td>結構檢查（標題、反釣魚、code block 語言、front matter schema）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>pre-commit、pre-push、CI&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mdtools cards&lt;/code>&lt;/td>
 &lt;td>跨文件完整性（連結、orphan、K4）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>pre-commit、pre-push、CI&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>工具原始碼在 &lt;code>scripts/mdtools/&lt;/code>，binary build 到 &lt;code>bin/mdtools&lt;/code>（已 gitignore）。&lt;/p>
&lt;p>作用範圍是 &lt;code>content/**/*.md&lt;/code>。&lt;code>public/&lt;/code>、&lt;code>themes/&lt;/code>、&lt;code>node_modules/&lt;/code> 等輸出或第三方資源不檢查。&lt;/p>
&lt;hr>
&lt;h2 id="2-標題規則">2. 標題規則&lt;/h2>
&lt;h3 id="21-標題結構與格式規則">2.1 標題結構與格式規則&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>正文禁止使用 H1&lt;/strong>。Hugo 的 front matter &lt;code>title&lt;/code> 會自動產生 H1，若正文再寫 &lt;code># ...&lt;/code> 會出現兩個 H1 並列，破壞語義階層與 SEO 訊號。正文一律從 H2 開始，最深到 H6。&lt;/li>
&lt;li>&lt;strong>同一父標題（直接上層）底下，子標題文字必須唯一&lt;/strong>（MD024 siblings_only 模式）。&lt;/li>
&lt;li>不同父標題底下，子標題允許重名。&lt;/li>
&lt;li>標題前後需保留空行（MD022），&lt;code>mdtools fmt --fix&lt;/code> 自動補。&lt;/li>
&lt;li>&lt;strong>標題結尾禁止標點&lt;/strong>（MD026）— 禁用字元：&lt;code>.&lt;/code>、&lt;code>,&lt;/code>、&lt;code>:&lt;/code>、&lt;code>;&lt;/code>、&lt;code>。&lt;/code>、&lt;code>，&lt;/code>、&lt;code>：&lt;/code>、&lt;code>；&lt;/code>。允許 &lt;code>?&lt;/code>、&lt;code>！&lt;/code>、&lt;code>？&lt;/code>、&lt;code>!&lt;/code> 作為語氣結尾。&lt;code>mdtools fmt --fix&lt;/code> 自動去除結尾禁用標點。&lt;/li>
&lt;li>&lt;strong>禁止用粗體當標題&lt;/strong>（MD036）— 若段落整段只由 &lt;code>**文字**&lt;/code> 或 &lt;code>*文字*&lt;/code> 組成，視為視覺性標題濫用。&lt;code>mdtools lint&lt;/code> 只報警、不自動修；作者需手動判斷正確的標題層級（通常是 H3 / H4）並改寫。&lt;/li>
&lt;/ul>
&lt;h3 id="22-補充範例md026-與-md036-的典型誤用">2.2 補充範例：MD026 與 MD036 的典型誤用&lt;/h3>
&lt;p>MD026（標題尾標點）常見誤用：&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要解決什麼">這篇要解決什麼</h2>
<p>隨著 blog 文章與知識卡片成長，純靠寫作紀律維持排版一致性越來越不可靠。反覆踩到的問題橫跨兩個層級：</p>
<p><strong>結構與安全層級</strong>（這是工具鏈存在的主要理由）：</p>
<ul>
<li><strong>裸 URL 在段落與表格中爆版</strong>（MD034），降低閱讀體驗。</li>
<li><strong>表格管線風格混用</strong>（MD060），同一張表格有的有空白、有的沒有。</li>
<li><strong>平行模板章節重複標題</strong>（MD024），例如多案例文章的 <code>### 弱點環節</code> 出現 13 次。</li>
<li><strong>顯示文字與實際 href 不一致</strong>（反釣魚）— 不在標準 markdownlint 規則內，但紅隊教材脈絡下必要。</li>
<li><strong>卡片雙向完整性</strong>（orphan 卡片、斷連結、K4 合規）— 跨文件檢查，現成工具做不到。</li>
<li><strong>Front matter schema</strong> — Hugo 依賴 YAML front matter 提供 title / date / weight 等欄位，缺失會破壞列表渲染、排序、SEO。</li>
</ul>
<p><strong>基礎格式層級</strong>（容易被忽略但影響 parser 穩定性或語義結構）：</p>
<ul>
<li>正文禁止使用 H1（嚴於 MD025）— Hugo front matter <code>title</code> 已產生 H1。</li>
<li>標題前後需保留空行（MD022），parser 才能正確識別標題邊界。</li>
<li>標題結尾禁止標點（MD026）— 例如 <code>## 常見問題：</code> 應改為 <code>## 常見問題</code>。</li>
<li>禁止用 <code>**bold**</code> 段落當標題（MD036）— 破壞語義階層與 TOC 產生。</li>
<li>程式碼區塊需註明語言（MD040），影響 syntax highlighting 與 accessibility。</li>
<li>列表前後需空行（MD032）、fenced code block 前後需空行（MD031）— 否則部分 parser 會把列表吃進段落。</li>
<li>有序列表編號風格一致（MD029）— 全部 <code>1.</code> 或全部 <code>1./2./3.</code>。</li>
<li>檔案結尾需有換行（MD047），POSIX 規範。</li>
<li>行長度上限（MD013）— <strong>預設關閉</strong>，中英混用技術寫作不適用 80-char 慣例。</li>
</ul>
<p>前兩類混合在同一份寫作規範裡，因為都由同一個工具鏈檢查、都要落地到相同的 pre-commit hook。純靠紀律記住這十幾條在大型 repo 上不可行，純 regex 又無法穩定處理「平行結構下的標題重複」「卡片段落歸屬」這類語意判斷。因此 blog 專案採用 Go + goldmark AST 做自訂 linter：<code>scripts/mdtools</code>。本文是 linter 與寫作規範的對齊文件；AGENTS.md 引用本文作為排版規範來源。</p>
<hr>
<h2 id="1-工具總覽">1. 工具總覽</h2>
<table>
  <thead>
      <tr>
          <th>子命令</th>
          <th>職責</th>
          <th>改檔</th>
          <th>觸發時機</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>mdtools fmt [--fix|--check]</code></td>
          <td>格式正規化（URL、表格、空行、列表間距、trailing newline）</td>
          <td><code>--fix</code> 會改</td>
          <td>pre-commit（<code>--fix</code>）、pre-push / CI（<code>--check</code>）</td>
      </tr>
      <tr>
          <td><code>mdtools lint</code></td>
          <td>結構檢查（標題、反釣魚、code block 語言、front matter schema）</td>
          <td>否</td>
          <td>pre-commit、pre-push、CI</td>
      </tr>
      <tr>
          <td><code>mdtools cards</code></td>
          <td>跨文件完整性（連結、orphan、K4）</td>
          <td>否</td>
          <td>pre-commit、pre-push、CI</td>
      </tr>
  </tbody>
</table>
<p>工具原始碼在 <code>scripts/mdtools/</code>，binary build 到 <code>bin/mdtools</code>（已 gitignore）。</p>
<p>作用範圍是 <code>content/**/*.md</code>。<code>public/</code>、<code>themes/</code>、<code>node_modules/</code> 等輸出或第三方資源不檢查。</p>
<hr>
<h2 id="2-標題規則">2. 標題規則</h2>
<h3 id="21-標題結構與格式規則">2.1 標題結構與格式規則</h3>
<ul>
<li><strong>正文禁止使用 H1</strong>。Hugo 的 front matter <code>title</code> 會自動產生 H1，若正文再寫 <code># ...</code> 會出現兩個 H1 並列，破壞語義階層與 SEO 訊號。正文一律從 H2 開始，最深到 H6。</li>
<li><strong>同一父標題（直接上層）底下，子標題文字必須唯一</strong>（MD024 siblings_only 模式）。</li>
<li>不同父標題底下，子標題允許重名。</li>
<li>標題前後需保留空行（MD022），<code>mdtools fmt --fix</code> 自動補。</li>
<li><strong>標題結尾禁止標點</strong>（MD026）— 禁用字元：<code>.</code>、<code>,</code>、<code>:</code>、<code>;</code>、<code>。</code>、<code>，</code>、<code>：</code>、<code>；</code>。允許 <code>?</code>、<code>！</code>、<code>？</code>、<code>!</code> 作為語氣結尾。<code>mdtools fmt --fix</code> 自動去除結尾禁用標點。</li>
<li><strong>禁止用粗體當標題</strong>（MD036）— 若段落整段只由 <code>**文字**</code> 或 <code>*文字*</code> 組成，視為視覺性標題濫用。<code>mdtools lint</code> 只報警、不自動修；作者需手動判斷正確的標題層級（通常是 H3 / H4）並改寫。</li>
</ul>
<h3 id="22-補充範例md026-與-md036-的典型誤用">2.2 補充範例：MD026 與 MD036 的典型誤用</h3>
<p>MD026（標題尾標點）常見誤用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">#### 字型選擇說明：        ← 違規（結尾 `：`）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>#### 字型選擇說明          ← 合法</span></span></code></pre></div><p>中文寫作習慣用冒號引入後續內容，這個模式在「段首句」合理、在「標題」就不合理 — 標題本身的存在就暗示了後續有內容，冒號變成冗餘訊號。</p>
<p>MD036（粗體當標題）常見誤用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">**字型選擇說明**           ← 違規：整段只有粗體，視覺像標題但不是真標題
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">這段內容...
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">### 字型選擇說明           ← 合法：用正式的 H3 取代</span></span></code></pre></div><p>差異看起來微小，實際影響包含：Hugo TOC 不會抓到、卡片反向連結失效、screen reader 無法跳轉。這是「語義 vs 視覺」錯位的典型案例，AST linter 容易檢出（Paragraph 節點唯一子節點為 Strong/Emph）。</p>
<h3 id="23-為什麼採-siblings_only-而非全域唯一">2.3 為什麼採 siblings_only 而非全域唯一</h3>
<p>平行結構（多案例、多模板章節）的複用語義來自上層標題賦予的脈絡。例如：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 【案例一】Uber 2022
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu">### 弱點環節        ← 合法
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="gu">### 攻擊路徑
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="gu">## 【案例二】Okta 2023
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="gu">### 弱點環節        ← 合法，因為在不同的父層下
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="gu"></span>### 攻擊路徑</span></span></code></pre></div><p>重名只有在同層並列時才代表結構錯誤。強制全域唯一會逼作者寫 <code>### 【案例二】弱點環節</code>，破壞平行結構的視覺一致性，收益並不大。</p>
<hr>
<h2 id="3-url-與連結規則">3. URL 與連結規則</h2>
<h3 id="31-裸-url-轉換mdtools-fmt---fix-自動處理">3.1 裸 URL 轉換（<code>mdtools fmt --fix</code> 自動處理）</h3>
<p>段落或表格儲存格內的裸 URL 會自動包成 markdown 連結。顯示文字依路徑可識別性分級：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>顯示文字</th>
          <th>範例（before → after）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>路徑含識別碼（例如 CVE）</td>
          <td><code>domain.com/識別碼</code></td>
          <td><code>https://nvd.nist.gov/vuln/detail/CVE-2023-34362</code> → <code>[nvd.nist.gov/CVE-2023-34362](https://nvd.nist.gov/vuln/detail/CVE-2023-34362)</code></td>
      </tr>
      <tr>
          <td>路徑冗長但無識別性</td>
          <td><code>domain.com</code></td>
          <td><code>https://www.cisa.gov/news-events/alerts/2024/06/03/snowflake-recommends-...</code> → <code>[cisa.gov](https://www.cisa.gov/news-events/alerts/2024/06/03/snowflake-recommends-...)</code></td>
      </tr>
      <tr>
          <td>已是 markdown 連結</td>
          <td>不動</td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>識別碼偵測用 regex 白名單，初始清單專注在高頻識別碼格式（例如 <code>CVE-YYYY-N</code>），其他格式以「遇到再加」原則擴充。清單維護在 <code>scripts/mdtools/internal/rules/identifiers.go</code>。</p>
<h3 id="32-反釣魚校驗mdtools-lint-強制檢查">3.2 反釣魚校驗（<code>mdtools lint</code> 強制檢查）</h3>
<p>Markdown 語法允許顯示文字與實際 href 完全不符，這是釣魚攻擊的結構基礎。本規則在 AST 層阻擋此模式。</p>
<ul>
<li><strong>R-URL-1（URL 樣顯示文字一致性）</strong>：若顯示文字含 <code>.com</code> / <code>.org</code> / <code>.gov</code> / <code>.net</code> / <code>.io</code> / <code>.dev</code> / <code>.tw</code> 等 TLD 字樣，則顯示文字的 domain 必須等於 href 的 domain（含子網域比對）。</li>
<li><strong>R-URL-2（描述型顯示文字自由）</strong>：顯示文字不含 TLD 字樣時，視為人類可讀描述，不做 domain 比對。</li>
</ul>
<p>違規範例（會被 lint 阻擋）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">[<span class="nt">nvd.nist.gov</span>](<span class="na">https://malicious.example.com/fake</span>)     ← 顯示文字暗示 NVD，href 卻不是
</span></span><span class="line"><span class="ln">2</span><span class="cl">[<span class="nt">cisa.gov/advisory</span>](<span class="na">https://cisa-gov.evil.example</span>)     ← 顯示文字抄 CISA 格式，domain 不符</span></span></code></pre></div><p>合法範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">[<span class="nt">Uber 事件公告</span>](<span class="na">https://www.uber.com/newsroom/security-update/</span>)
</span></span><span class="line"><span class="ln">2</span><span class="cl">[<span class="nt">nvd.nist.gov/CVE-2023-34362</span>](<span class="na">https://nvd.nist.gov/vuln/detail/CVE-2023-34362</span>)</span></span></code></pre></div><p>這條規則在紅隊 / 安全相關教材中特別重要：讀者本來就該對來源警戒，排版規則不該削弱這個警戒訊號。縮短顯示文字提升可讀性，反釣魚校驗守住安全底線，兩者互補。</p>
<h3 id="33-例外情境">3.3 例外情境</h3>
<ul>
<li><strong>程式碼區塊</strong>（fenced code block，<code>```</code> 包圍）內的 URL <strong>不做任何處理</strong>（不縮短、不校驗）。代碼範例經常需要展示完整 URL 給讀者複製執行。</li>
<li><strong>引用區塊</strong>（<code>&gt;</code> 開頭）內的 URL <strong>比照段落處理</strong>，會縮短也會做反釣魚校驗。</li>
</ul>
<hr>
<h2 id="4-表格規則">4. 表格規則</h2>
<ul>
<li>統一使用 <strong>aligned 風格</strong>：每欄內容用空白補齊到該欄的最大寬度，使 <code>|</code> 在 monospace 渲染下垂直對齊。</li>
<li>欄位分隔線使用 <code>| --- |</code> 形式，不含對齊冒號 <code>:</code>（分隔線內的 <code>-</code> 數量跟隨該欄寬度自動填足）。</li>
<li>寬度計算使用顯示寬度（display width）— CJK 字元佔 2 欄寬、ASCII 佔 1 欄寬，分隔列與資料列按同一套寬度對齊。</li>
<li><code>mdtools fmt --fix</code> 自動正規化：插入新行或改動欄寬時會全表重算，作者不需手工維持對齊。</li>
</ul>
<p>選 aligned 而非 compact 的理由是<strong>原始檔可讀性</strong>：技術教材的表格常需在 code review 裡對照，aligned 風格讓 reviewer 直接看出哪些欄位對應哪些內容，不用在腦中解析鋸齒狀的 pipes。手工對齊在長表格反覆編輯時確實會失效（新增一行就全表要重對齊），但這正是 <code>mdtools fmt --fix</code> 接手的地方。</p>
<hr>
<h2 id="5-基礎格式細節">5. 基礎格式細節</h2>
<p>這節整理容易被忽略、但會影響 parser 正確性或渲染品質的小規則。</p>
<h3 id="51-程式碼區塊必須註明語言md040">5.1 程式碼區塊必須註明語言（MD040）</h3>
<p>由 <code>mdtools lint</code> 檢查。未註明語言的 fenced code block 會被報警：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl">``<span class="sb">`                   ← 違規：缺語言標示
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="sb">func main() {
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="sb">    fmt.Println(&#34;hi&#34;)
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="sb">}
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="sb">`</span>`<span class="sb">`
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="sb">`</span>`<span class="sb">`go                 ← 合法
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sb">func main() {
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sb">    fmt.Println(&#34;hi&#34;)
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sb">}
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sb">`</span>``</span></span></code></pre></div><p>純文字輸出（例如 terminal output、log 片段）使用 <code>text</code> 或 <code>plain</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="s">```text
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s"></span>Error: permission denied
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">```</span></span></span></code></pre></div><p>Shell 範例統一用 <code>bash</code>（即使是 zsh 語法，讓 syntax highlighter 有合理預設）；純設定檔依實際格式（<code>toml</code>、<code>yaml</code>、<code>json</code>、<code>ini</code>）。</p>
<h3 id="52-fenced-code-block-前後需空行md031">5.2 fenced code block 前後需空行（MD031）</h3>
<p>由 <code>mdtools fmt --fix</code> 自動處理。缺空行會讓前後段落被 parser 併入 code block 或反之。</p>
<h3 id="53-列表前後需空行md032">5.3 列表前後需空行（MD032）</h3>
<p>由 <code>mdtools fmt --fix</code> 自動處理。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl">上一段結束。
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">-</span> 列表項一           ← 違規：列表前無空行，會被部分 parser 當段落延續
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">-</span> 列表項二
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">上一段結束。
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">-</span> 列表項一           ← 合法
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">-</span> 列表項二
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">下一段開始。</span></span></code></pre></div><h3 id="54-有序列表編號一致性md029">5.4 有序列表編號一致性（MD029）</h3>
<p>由 <code>mdtools fmt --fix</code> 正規化。本專案採 <code>ordered</code> 風格（全部遞增編號）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">1.</span> 第一步
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">2.</span> 第二步           ← 合法
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">3.</span> 第三步
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">1.</span> 第一步
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">1.</span> 第二步           ← 違規：混用風格（fmt --fix 會改成 1./2./3.）
</span></span><span class="line"><span class="ln">7</span><span class="cl">2. 第三步</span></span></code></pre></div><p>選擇 <code>ordered</code> 的理由：原始檔可讀性高，作者直接看到步驟數；插入新項目的對齊代價比全部重新渲染低。</p>
<h3 id="55-段落間空行">5.5 段落間空行</h3>
<p>段落之間、標題前後、列表與段落之間都需空行。<code>mdtools fmt --fix</code> 會自動規範化多餘 / 缺失的空行，作者不需手工維護。</p>
<h3 id="56-檔案結尾需有換行md047">5.6 檔案結尾需有換行（MD047）</h3>
<p>POSIX 文字檔規範；缺失時 git diff 會出現 <code>\ No newline at end of file</code>。<code>mdtools fmt --fix</code> 自動補。</p>
<h3 id="57-tab-字元md010-僅限-fenced-code-block">5.7 Tab 字元（MD010）— 僅限 fenced code block</h3>
<p>由 <code>mdtools lint</code> 檢查（warn 等級）。Prose / 列表 / 表格 / 引用等非 code-block 行內若出現 tab 字元，會被標記並建議改成空白；fenced code block 內的 tab 保留（Go 原始碼依 gofmt 慣例用 tab，文章要讓讀者能直接複製貼用）。</p>
<p>Repo 根目錄的 <code>.markdownlint.json</code> 用 <code>&quot;MD010&quot;: { &quot;code_blocks&quot;: false }</code> 告知 IDE 的 markdownlint extension 採用同一套 policy，讓編輯器跟 CI 的警告保持一致。</p>
<h3 id="58-行長度上限md013-預設關閉">5.8 行長度上限（MD013）— 預設關閉</h3>
<p>本規則<strong>預設關閉</strong>。中英混用的技術寫作不適用 80-char 慣例：</p>
<ul>
<li>中文每字元算 1 個寬度時，80-char ≈ 40 個中文字，寫到一半就要斷行，嚴重影響可讀性。</li>
<li>中文每字元算 2 個寬度時，80-char 相當於 20-30 個中文字，更離譜。</li>
<li>Markdown 編輯器普遍支援軟斷行與 IDE word wrap，實體行長度對閱讀體驗影響小。</li>
</ul>
<p>若未來需要打開（例如發現真的有人寫出 2000-char 單行段落），建議上限 <strong>400 字元</strong>（軟上限，warn 不阻擋）。設定在 <code>scripts/mdtools/internal/rules/config.go</code> 的 <code>LineLengthLimit</code> 欄位。</p>
<h3 id="59-裝飾符號禁用emoji--視覺記號">5.9 裝飾符號禁用（emoji / 視覺記號）</h3>
<blockquote>
<p><strong>本節本身豁免</strong>：規範要描述「哪些符號禁用」必然要列舉這些符號（use-mention distinction）。本節舉例的 emoji 屬 mention（指稱）、非 use（裝飾使用）、不違反規則。掃描指令會 hit 到本節、判讀時跳過。</p></blockquote>
<p><code>content/**</code> 正文不可使用 emoji（如 ✅ ❌ ⚠️ 🚨 🟡 🟢 ⭐ 📌 💡 ⚡ 🎯）與裝飾性 unicode 符號（✓ ✗ ✘）。<strong>表格、列表、行內標記都不行</strong>。</p>
<p><strong>替換策略</strong>（emoji 承載的語意要回到文字結構、不是純粹刪除符號）：</p>
<table>
  <thead>
      <tr>
          <th>原寫法</th>
          <th>改成</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>表格 status <code>| ✅ 解了 |</code></td>
          <td>純文字描述：「解了」/「是」/「適用」</td>
      </tr>
      <tr>
          <td>表格 status <code>| ❌ 漏 |</code></td>
          <td>純文字描述：「漏」/「否」/「不適用」</td>
      </tr>
      <tr>
          <td>列表優缺點 <code>- ✅ 簡單</code> / <code>- ❌ 慢</code></td>
          <td>拆成 <code>**優點**：簡單</code> / <code>**缺點**：慢</code> 段落或標題段</td>
      </tr>
      <tr>
          <td>列表錯誤示範 <code>- ❌ 把 key 寄 email</code> / <code>- ✅ 用 CSR</code></td>
          <td>拆成 <code>**錯誤做法**：</code> / <code>**正確做法**：</code> 標題段</td>
      </tr>
      <tr>
          <td>行內視覺強調 <code>🚨 critical</code></td>
          <td>markdown 粗體 <code>**critical**</code> 或引用塊 <code>&gt; **critical**：...</code></td>
      </tr>
  </tbody>
</table>
<p><strong>理由</strong>：</p>
<ul>
<li><strong>Grep-ability</strong>：emoji 無法用 plain text grep 命中；視覺結構容易掩蓋語意結構、reviewer 看不出「優 / 缺」是用 emoji 區分還是用標題段區分</li>
<li><strong>CLI parser 相容性</strong>：部分 multi-byte emoji 在 Rust-based CLI 工具（如某些 mdtools / pagefind / lint pipeline）觸發 char-boundary panic</li>
<li><strong>跨語境穩定</strong>：emoji 在不同字型 / 平台 / 終端機渲染差異大、容易斷行或顯示為框</li>
</ul>
<p><strong>掃描指令</strong>（提交前自己跑一次、有 hit 就替換）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">rg <span class="s2">&#34;✅|❌|⚠️|🚨|🟡|🟢|🔴|🟠|🔵|⭐|📌|💡|⚡|🎯|✨|📝|🔍|🛠|⛔|✓|✗|✘&#34;</span> content/</span></span></code></pre></div><blockquote>
<p>本規則目前<strong>未進 <code>mdtools lint</code> 自動掃描</strong>、靠人工 grep。未來會加進 lint pipeline。</p></blockquote>
<hr>
<h3 id="510-位置引用與數量命名候選掃描ref1--ref2警告層">5.10 位置引用與數量命名候選掃描（REF1 / REF2、警告層）</h3>
<p><code>mdtools lint</code> 對 <code>content/**</code> 跑兩個警告層掃描、來源是引用紀律卡（<a href="/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">#155 引用章節用語意標題</a>、<a href="/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">#156 集合命名用角色</a>）：</p>
<ul>
<li><strong>REF1-positional-anchor</strong>：正文中的位置式引用候選 —「見第 3 點」「詳見第五章」「§4」。位置編號是當下排列的衍生值、目標是活文件時、結構重排會讓引用 silent 指向錯的內容。</li>
<li><strong>REF2-count-in-name</strong>：標題與 front matter <code>title</code> 中內嵌成員數的集合命名 —「六大原則」「遷移五階段流程」。成員增減時名稱先失真、且名稱是被複製最多次的字串。</li>
</ul>
<p>兩個規則都停在警告層、<strong>命中是候選、不是判決</strong> — 回報前要做語意判定：</p>
<table>
  <thead>
      <tr>
          <th>命中情境</th>
          <th>判定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用發布方凍結的編號（RFC 段號、法條條號）</td>
          <td>合規、編號是 fact</td>
      </tr>
      <tr>
          <td>數字緊鄰它描述的清單（「確認三件事：」）</td>
          <td>合規、漂移在編輯當下可見</td>
      </tr>
      <tr>
          <td>外部凍結品牌名（SOLID 五原則）</td>
          <td>合規、數量由發布方凍結</td>
      </tr>
      <tr>
          <td>目標是內部活文件 / 內部活集合</td>
          <td>改語意標題引用 / 角色命名</td>
      </tr>
  </tbody>
</table>
<p>掃描器內建兩個自動豁免：被 <code>「」</code> 包住的命中視為反例引用、直接跳過；「第」開頭的序數（第三階段）不屬 REF2 的集合命名。既有內容的命中屬歷史基線、依「changed-set scoped lint」原則處理 — 新增與修改的檔案要判讀、存量警告分開回報。</p>
<h3 id="511-否定起手候選掃描pos-negation-lead警告層">5.11 否定起手候選掃描（POS-negation-lead、警告層）</h3>
<p><code>mdtools lint</code> 對 <code>content/**</code> 跑否定起手掃描、來源是 <a href="/blog/report/lead-with-the-point-cross-language/" data-link-title="重點優先陳述是跨語言的資訊結構原則、不是中文句型問題" data-link-desc="正向陳述優先的本質是資訊結構效率：讀者拿到核心概念的認知步驟越少越好。「不是 X、而是 Y」表達能力差、是因為它讓讀者先處理一個被否定的錯誤理解 X 才拿到正確的 Y、重點後置多繞一步。這個缺陷跨語言成立——英文 not X but Y、日文 X ではなく Y 同樣高頻、換語言不打破（證偽過的反例假設）。判別線是「核心概念在不在最前」、統一了 #94（重點先行合法）與 #149（重點後置違規）、且可操作。LLM 系統性放水的根因是高頻偏置（把語料高頻句型評為表達好、高頻不等於資訊結構優、跨語言）。主解是強制執行重點位置判準、#165 的異源視角降為補充。">#166 重點優先陳述是跨語言的資訊結構原則</a>（搭配 <a href="/blog/report/register-violation-needs-cross-style-eyes/" data-link-title="register 違規：偵測可機械化、判定要靠文體異源的眼睛" data-link-desc="寫作規範的違規分兩類：形式違規（emoji / 編號 / broken link）可完全機械判定、該進工具鏈；register / 品味違規（概念前置 / 否定起手 / 喊話 / 誇飾）的判定有不可消除的品味核心。「不是 X、而是 Y」的陷阱是偵測可機械化（grep 抓得到句型）偽裝成判定可機械化、誘導無限投入更精緻的判定方法（grep → 概念位置 → 行為測試）、但判定始終在品味側、始終放水。更深一層：產出這類違規的 LLM 跟審查它的 LLM 共享文體直覺、同源自審對 register 違規有結構上限、加再多輪次都跨不過。結構解是引入文體異源的視角（人類冷讀 / reader-simulation / 對抗文體 reviewer）、並接受 100% 自動 catch 不可能。">#165 register 違規要文體異源</a>）：</p>
<ul>
<li><strong>POS-negation-lead</strong>：正文中「不是 X、而是 Y」「不是 X — 是 Y」「與其 X、不如 Y」的重點後置候選。核心概念（Y）被擠到「而是 / 不如」之後、讀者要先處理一個被否定的 X 才拿到重點。這是資訊結構效率問題、跨語言成立（英文「not X but Y」、日文「X ではなく Y」）、不是中文特有句型 — 偵測可機械化、判定不可。pattern 涵蓋的連接詞（而是 / 「— 是」/ 不如）枚舉不完、判準是「核心概念在不在句首」而非哪個連接詞 — 漏掉的變體只是讓候選 silent 到有人讀到（規則第一版就漏了「不是 X — 是 Y」、靠人發現才補）。</li>
</ul>
<p>判定用「重點位置」、<strong>命中是候選、不是判決</strong>：</p>
<table>
  <thead>
      <tr>
          <th>命中情境</th>
          <th>判定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心概念第一次正面出現在句首（「有深度、不是非黑即白的二元」）</td>
          <td>合規、重點先行</td>
      </tr>
      <tr>
          <td>明示反例 / 對照段落內的否定（見 <a href="/blog/report/positive-rewrite-preserves-contrast/" data-link-title="正向改寫要保留對照論據、不能空降結論" data-link-desc="把「X、不是 Y」改成正向陳述時、若直接刪除「不是 Y」會讓結論失去推理依據、變成空降斷言。對照論據（Y 是讀者直覺會想到的替代方案）跟結論（X）是同一個推理單元、抽掉一半就不成立。正向改寫的合法做法是補解釋、改成對照表、或保留 Y 當對比錨點 — 直接拿掉是 worst。">#94</a>）</td>
          <td>合規、否定是對照本體</td>
      </tr>
      <tr>
          <td>核心概念被擠到「而是」之後（「不是二元、而是有深度」）</td>
          <td>改正向、把核心概念移到句首</td>
      </tr>
  </tbody>
</table>
<p>掃描器豁免兩類命中：被 <code>「」</code> 包住的引用（反例引用 / 句型佔位符）、以及 backtick 行內程式碼內的 pattern（grep regex / 技術識別碼）。講這個句型的 meta 卡（#165 / #166）與本規範段落會大量自我觸發 — 屬候選人判定豁免、跟 REF1 / REF2 對 #155 / #156 一致。既有內容的命中（全 <code>content/</code> 約數百個）屬歷史基線、依「changed-set scoped lint」原則處理 — 偵測把候選池曝光、判定漸進進行、存量警告分開回報。</p>
<h2 id="6-front-matter-schemamdtools-lint">6. Front matter schema（<code>mdtools lint</code>）</h2>
<p>Hugo 依賴 YAML front matter 提供 title / date / weight 等欄位給 render pipeline。缺欄位會讓列表頁、排序、SEO 壞掉，但 Hugo 本身不會失敗（靜默接受不完整資料），所以必須由 linter 守住。</p>
<h3 id="61-通用層contentmd">6.1 通用層（<code>content/**/*.md</code>）</h3>
<p>所有內容文章必須有：</p>
<ul>
<li><code>title</code>：字串，不可空。</li>
<li><code>date</code>：<code>YYYY-MM-DD</code> 格式（ISO 8601 date）。</li>
</ul>
<p><strong>Hugo <code>_index.md</code> section 頁面例外</strong>：這類檔案是 Hugo 的 section 列表 landing page，不是內容文章，沒有語意上的「日期」。只要求 <code>title</code>，不強制 <code>date</code>。</p>
<h3 id="62-推薦層警告不阻擋">6.2 推薦層（警告，不阻擋）</h3>
<p>推薦填寫（<code>mdtools lint</code> warn level）：</p>
<ul>
<li><code>description</code>：字串，建議 30–150 字，影響 SEO 與列表頁預覽。</li>
<li><code>tags</code>：陣列，至少 1 個標籤。</li>
</ul>
<p>推薦層是歷史內容的緩衝區，不是新增內容的放行條件。新增文章必須同時填寫 <code>description</code> 與 <code>tags</code>；修改既有文章時，若同一檔案缺少推薦欄位，應在同次變更補齊，避免每次驗證都被舊 warning 淹沒。</p>
<p>驗證時先跑 changed-set scoped lint 判斷本次變更品質，再視需要跑 full lint 觀察整體基線。回報 full lint 結果時，要把歷史 warning、已知 warning 與本次新增問題分開描述。</p>
<h3 id="63-卡片嚴格層contentbackendknowledge-cards">6.3 卡片嚴格層（<code>content/backend/knowledge-cards/**</code>）</h3>
<p>知識卡片額外要求（對應 <code>.codex/briefs/knowledge-cards.md</code> K2）：</p>
<ul>
<li><code>title</code>、<code>date</code>、<code>description</code> 必填。</li>
<li><code>weight</code>：整數，決定在 <code>_index.md</code> 主題表格中的排序位置。</li>
</ul>
<h3 id="64-禁止欄位">6.4 禁止欄位</h3>
<p>以下欄位存在時 <code>mdtools lint</code> 警告（避免語義混淆）：</p>
<ul>
<li><code>author</code>：本專案為單作者 blog，統一於 Hugo 設定。</li>
<li><code>permalink</code>：使用 Hugo 預設路徑規則，避免手動覆蓋。</li>
</ul>
<p>若未來需要鬆綁，在 <code>scripts/mdtools/internal/rules/frontmatter.go</code> 的 <code>DisallowedFields</code> 清單調整。</p>
<h3 id="65-slug-必填跟檔名對齊">6.5 slug 必填、跟檔名對齊</h3>
<p>所有 content 文章 frontmatter 必須有 <code>slug</code> 欄位，值跟檔名（不含 <code>.md</code>）對齊。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;視覺手段對齊錯誤層次&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="nt">slug</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;visual-tool-error-layer-alignment&#34;</span><span class="w">   </span><span class="c"># 跟檔名對齊</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-04-28</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="nn">---</span></span></span></code></pre></div><p><strong>為什麼必填</strong>：</p>
<p>slug 是 URL 的核心識別、跨多個工具共用（Hugo build、mdtools lint、跨檔 markdown link、search index）。若不顯式定義，slug 散落在三處推導鏈：</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>推導值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hugo 預設（從 title 用 urlize）</td>
          <td>runtime 推導、隨 hugo 版本變化</td>
      </tr>
      <tr>
          <td>mdtools 字面比對</td>
          <td>檔名 stem</td>
      </tr>
      <tr>
          <td>跨檔連結時的引用</td>
          <td>寫作者手動算 / 複製</td>
      </tr>
  </tbody>
</table>
<p>三個推導鏈不一致時 = silent broken link（mdtools pass 但 hugo build 後 404、或反過來）。把 slug 升成 frontmatter 顯式 fact、所有工具基於同一 source、消除 derivation 鏈。</p>
<p>詳細論述見 <a href="/blog/report/url-slug-must-be-explicit-fact/" data-link-title="URL slug 必須顯式定義為 fact：跨工具 identifier 用單一定義源" data-link-desc="URL slug 在 Hugo 預設下從 title 自動推導、在 mdtools lint 下從檔名讀、在跨檔連結時又要寫第三個值 — 一個 identifier 散落在三個推導鏈、典型 SSoT 違反。當多個工具共用一個 identifier、推導不一致 = silent broken link。修法：把 slug 從 derivation（runtime 推導）升級成 fact（frontmatter 顯式定義）、檔名 / 連結都基於這個 fact。本卡是 #44 在 toolchain integration 情境的具體實例、是 #82 字面 vs 行為在 identifier 維度的展現。">report #93 URL slug 必須顯式定義為 fact</a>。</p>
<p><strong>檔名對齊規則</strong>：</p>
<ul>
<li>檔名命名建議：英文小寫、kebab-case 或 snake_case、不含中文（避免 hugo <code>urlize</code> 規則跨版本變動）</li>
<li>slug 值 == 檔名 stem（不含 <code>.md</code>）</li>
<li>修檔名時必須同步修 slug；修 slug 時必須同步 rename 檔案</li>
</ul>
<p><strong>Hugo <code>_index.md</code> 例外</strong>：section 列表頁已有 <code>slug:</code> 欄位指定資料夾路徑、不適用本規則。</p>
<hr>
<h2 id="7-卡片雙向完整性mdtools-cards">7. 卡片雙向完整性（<code>mdtools cards</code>）</h2>
<p>作用範圍：<code>content/**/*.md</code>，重點關注 <code>content/backend/knowledge-cards/</code>。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>規則</th>
          <th>實作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>L1 連結有效性</strong></td>
          <td>所有相對連結 <code>[...](/posts/markdown-writing-spec/path)</code> / <code>[...](/posts/path)</code> 的目標檔案必須存在</td>
          <td>AST 抽 Link node → 解析相對路徑 → stat 檔案</td>
      </tr>
      <tr>
          <td><strong>L2 卡片 orphan 偵測</strong></td>
          <td>每張卡片至少被 <code>content/**</code> 中一篇非卡片正文引用</td>
          <td>建反向索引 → 找無 incoming edge 的卡片</td>
      </tr>
      <tr>
          <td><strong>L4 卡片 K4 結構合規</strong></td>
          <td>卡片首段與「概念位置」段各至少 1 個相鄰卡片連結</td>
          <td>AST 定位段落節點 → 統計子樹 Link 數</td>
      </tr>
  </tbody>
</table>
<p>L3（正文首次出現術語必須連結到卡片）暫不納入，待術語字典（<code>.codex/briefs/knowledge-web-expansion.md</code>）啟動後再開。</p>
<h3 id="為什麼要做跨文件檢查">為什麼要做跨文件檢查</h3>
<p>知識卡片是 blog 的核心知識資產。隨著卡片數量增加：</p>
<ul>
<li><strong>Orphan 卡片</strong>（沒有正文連結進來）會變成知識死角，讀者無法發現。</li>
<li><strong>斷掉的相對連結</strong>（檔案被改名或移動）肉眼難以發現，只有讀者點擊失敗才暴露。</li>
<li><strong>K4 合規</strong>（首段 + 概念位置段要有鄰卡連結）保證卡片間的知識網不會鬆散。</li>
</ul>
<p>這些檢查用 regex 做都卡在「段落歸屬怎麼判斷」。AST 天生知道節點的父子結構，做起來自然。</p>
<hr>
<h2 id="8-執行時機">8. 執行時機</h2>
<h3 id="pre-commit-hookgithookspre-commit">Pre-commit hook（<code>.githooks/pre-commit</code>）</h3>
<ol>
<li><code>mdtools fmt --fix</code> — 自動修格式；改動會 <code>git add</code> 回 staged，避免改完又沒進 commit。</li>
<li><code>mdtools lint</code> — 結構檢查；失敗阻擋 commit。</li>
<li><code>mdtools cards</code> — 完整性檢查；失敗阻擋 commit。</li>
</ol>
<h3 id="pre-push-hookgithookspre-push">Pre-push hook（<code>.githooks/pre-push</code>）</h3>
<p><code>pre-push</code> 的責任是把 CI 同款全量檢查提前到本機。<code>pre-commit</code> 為了速度只處理 staged markdown；<code>pre-push</code> 會跑 <code>make check</code>，也就是 <code>mdtools fmt --check content/</code>、<code>mdtools lint content/</code>、<code>mdtools cards content/</code>，讓整個 <code>content/</code> 的格式與連結 drift 在推送前被攔下。</p>
<p>啟用 hook：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git config core.hooksPath .githooks
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或：make install-hooks</span></span></span></code></pre></div><h3 id="cigithubworkflowsmd-checkyml">CI（<code>.github/workflows/md-check.yml</code>）</h3>
<p>三個子命令都跑 <code>--check</code> / 嚴格模式，任何違規 fail CI。</p>
<hr>
<h2 id="9-寫作者使用指引">9. 寫作者使用指引</h2>
<ul>
<li>寫作時優先遵循本規範。pre-commit / pre-push 報錯時讀訊息修正；<strong>不可用 <code>git commit --no-verify</code> 或跳過 hook 的方式繞過檢查</strong>。</li>
<li>新增案例平行章節（例如多個「工具評測」「事件時序」）時不需登記到任何白名單 — siblings_only 自動判讀。</li>
<li>新增 URL 時優先採用裸 URL 轉換段的分級形式；若顯示文字含 TLD 字樣，確認 domain 與 href 完全一致。</li>
<li>新增卡片時確認首段與「概念位置」段各有至少一個相鄰卡片連結（L4 要求）；確認 front matter 含 <code>title</code> / <code>date</code> / <code>description</code> / <code>weight</code>（卡片嚴格層）。</li>
<li>程式碼區塊養成習慣先寫語言標示再填內容；純文字輸出用 <code>text</code>。</li>
</ul>
<hr>
<h2 id="10-規則擴充流程">10. 規則擴充流程</h2>
<p>新規則進入本文的路徑：</p>
<ol>
<li>先在 <code>scripts/mdtools/internal/rules/</code> 實作為可開關的 rule（預設關）。</li>
<li>在代表性檔案上測試誤判率。</li>
<li>誤判率 &lt; 1% 且有明確教材品質收益時，預設開啟並更新本文。</li>
<li>預設開啟後同步修正既有違規；若違規數量大，可分批 PR。</li>
</ol>
<hr>
<h2 id="11-為什麼自訂而不是用現成-markdownlint">11. 為什麼自訂而不是用現成 markdownlint</h2>
<p><code>markdownlint-cli2</code> 的 MD022 / MD024 / MD026 / MD029 / MD031 / MD032 / MD034 / MD036 / MD040 / MD047 / MD060 這些基礎規則都有（MD013 預設關閉、MD025 本規範嚴於原版），為什麼還要自寫？</p>
<p>關鍵差在<strong>卡片雙向完整性</strong>、<strong>反釣魚校驗</strong>、<strong>Front matter schema</strong> 這三類檢查，屬於跨文件 / AST 層 / 業務邏輯層的自訂邏輯，現成 linter 無法表達。這些檢查是 blog 品質的核心訊號，必須跟基礎格式檢查放在同一個工具鏈、同一次 AST parse 內處理，避免多個工具重複解析、重複維護。</p>
<p>另外 goldmark 是 Hugo 內建的 markdown parser。用同一個 parser 做 lint 保證「lint 通過 → Hugo render 一致」，杜絕兩套 parser 解讀不同的長尾 bug。</p>
<hr>
<p>本文為 blog 專案 Markdown 寫作規範的單一真實來源。repo 根目錄的 <code>AGENTS.md</code> 引用本文作為排版規範權威，規則與 <code>scripts/mdtools</code> 實作保持同步。</p>
]]></content:encoded></item><item><title>mdtools：Go + goldmark 的 markdown 工具鏈設計</title><link>https://tarrragon.github.io/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/</guid><description>&lt;h2 id="背景為什麼要自訂工具">背景：為什麼要自訂工具&lt;/h2>
&lt;p>Blog 專案的 markdown 規範有三類不同性質的檢查需求：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>基礎格式&lt;/strong>（MD022 / MD024 / MD034 / MD060 等）— 市面 linter 都有，但規則細節不一致，我們對 &lt;code>MD024&lt;/code> 要特殊處理（&lt;code>siblings_only&lt;/code> 模式允許平行結構下的同名標題）。&lt;/li>
&lt;li>&lt;strong>反釣魚校驗&lt;/strong>（R-URL-1/2）— 顯示文字含 TLD 字樣時必須與 href 的 domain 一致，避免釣魚型連結。這條規則不在 markdownlint 標準集內。&lt;/li>
&lt;li>&lt;strong>卡片雙向完整性&lt;/strong>（L1/L2/L4）— 跨文件的圖論檢查：每張卡片至少被一篇正文引用、相對連結目標存在、卡片首段含鄰卡連結。&lt;/li>
&lt;/ol>
&lt;p>三類檢查共享兩個技術需求：&lt;strong>AST 層的語法理解&lt;/strong>、&lt;strong>goldmark 與 Hugo render 的一致性&lt;/strong>。詳細原因寫在&lt;a href="https://tarrragon.github.io/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST&lt;/a>。&lt;/p>
&lt;p>Markdownlint-cli2 涵蓋第一類、無法表達第二、三類。現成方案湊不出來，就自己寫。&lt;/p>
&lt;h2 id="語言選擇go-vs-python-的-tripwire-式決策">語言選擇：Go vs Python 的 tripwire 式決策&lt;/h2>
&lt;p>這是實際討論過的決策，值得留下紀錄。&lt;/p>
&lt;h3 id="表面的直覺blog-用-hugogo-寫的所以用-go-最自然">表面的直覺：Blog 用 Hugo（Go 寫的），所以用 Go 最自然&lt;/h3>
&lt;p>這個推論有個破口：Hugo 雖然用 Go 寫，但我們用的是 &lt;strong>pre-built binary&lt;/strong>。&lt;code>hugo server&lt;/code> 本地跑的是下載好的執行檔，CI 用 &lt;code>peaceiris/actions-hugo&lt;/code> 這類 action，整個 blog 的 build 流程完全不碰 Go toolchain。&lt;/p>
&lt;p>「專案已有 Go 依賴」這個前提不成立。真正要問的是：&lt;strong>我是否願意為這組工具引入 Go toolchain 這個新依賴？&lt;/strong>&lt;/p>
&lt;h3 id="務實的對比">務實的對比&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Python&lt;/th>
 &lt;th>Go&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Pre-commit 啟動速度&lt;/td>
 &lt;td>~50ms（interpreter 啟動）&lt;/td>
 &lt;td>&lt;code>go run&lt;/code> ~500ms/次；pre-build binary 則要 commit 進 repo&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CI 新增依賴&lt;/td>
 &lt;td>&lt;code>setup-python&lt;/code>（runner 通常自帶）&lt;/td>
 &lt;td>&lt;code>setup-go&lt;/code> + build step&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>開發速度（regex / 字串處理）&lt;/td>
 &lt;td>快&lt;/td>
 &lt;td>慢 2-3x，boilerplate 較多&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AST 解析選擇&lt;/td>
 &lt;td>mistune / markdown-it-py&lt;/td>
 &lt;td>&lt;strong>goldmark（與 Hugo 同源）&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Go 唯一的決定性優勢是 goldmark — 跟 Hugo 用同一個 parser 可以保證「lint 通過 ↔ Hugo render 成功」等價。&lt;/p>
&lt;h3 id="關鍵一問現在需要-ast-嗎">關鍵一問：現在需要 AST 嗎？&lt;/h3>
&lt;p>我們最初傾向的是 tripwire 策略：&lt;strong>現在用 Python + regex 先頂著，等 rule 複雜度超過臨界就升級 Go + goldmark&lt;/strong>。Tripwire 條件大致是：&lt;/p>
&lt;ol>
&lt;li>Rule 數量超過 5 條。&lt;/li>
&lt;li>任一規則需要「這段文字在 code block 內嗎」這類上下文判斷。&lt;/li>
&lt;li>Hugo render 結果跟 lint 判讀開始不一致。&lt;/li>
&lt;/ol>
&lt;p>但事實是：&lt;/p>
&lt;ul>
&lt;li>MD024 的 siblings_only 已經需要父子關係追蹤 — 條件 2 馬上命中。&lt;/li>
&lt;li>卡片雙向完整性是當前任務（不是未來可能）— 跨文件檢查 regex 做不到。&lt;/li>
&lt;/ul>
&lt;p>兩個條件當下已經滿足，delay migration 反而要兩次寫工具。所以直接選 Go + goldmark。&lt;/p>
&lt;p>這個決定的邏輯層面是：&lt;strong>當需求已在手上而非 speculative，延遲決策的代價 &amp;gt; 直接上的代價&lt;/strong>。&lt;/p>
&lt;h2 id="為什麼選-goldmark">為什麼選 goldmark&lt;/h2>
&lt;p>三個具體理由：&lt;/p>
&lt;h3 id="1-解析結果與-hugo-一致">1. 解析結果與 Hugo 一致&lt;/h3>
&lt;p>Hugo 的 content render pipeline 走 goldmark。用同一個 parser 寫 lint，可以杜絕「lint 通過但 Hugo render 失敗」或「Hugo 看得懂但 lint 誤判」這類長尾 bug。&lt;/p></description><content:encoded><![CDATA[<h2 id="背景為什麼要自訂工具">背景：為什麼要自訂工具</h2>
<p>Blog 專案的 markdown 規範有三類不同性質的檢查需求：</p>
<ol>
<li><strong>基礎格式</strong>（MD022 / MD024 / MD034 / MD060 等）— 市面 linter 都有，但規則細節不一致，我們對 <code>MD024</code> 要特殊處理（<code>siblings_only</code> 模式允許平行結構下的同名標題）。</li>
<li><strong>反釣魚校驗</strong>（R-URL-1/2）— 顯示文字含 TLD 字樣時必須與 href 的 domain 一致，避免釣魚型連結。這條規則不在 markdownlint 標準集內。</li>
<li><strong>卡片雙向完整性</strong>（L1/L2/L4）— 跨文件的圖論檢查：每張卡片至少被一篇正文引用、相對連結目標存在、卡片首段含鄰卡連結。</li>
</ol>
<p>三類檢查共享兩個技術需求：<strong>AST 層的語法理解</strong>、<strong>goldmark 與 Hugo render 的一致性</strong>。詳細原因寫在<a href="/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST</a>。</p>
<p>Markdownlint-cli2 涵蓋第一類、無法表達第二、三類。現成方案湊不出來，就自己寫。</p>
<h2 id="語言選擇go-vs-python-的-tripwire-式決策">語言選擇：Go vs Python 的 tripwire 式決策</h2>
<p>這是實際討論過的決策，值得留下紀錄。</p>
<h3 id="表面的直覺blog-用-hugogo-寫的所以用-go-最自然">表面的直覺：Blog 用 Hugo（Go 寫的），所以用 Go 最自然</h3>
<p>這個推論有個破口：Hugo 雖然用 Go 寫，但我們用的是 <strong>pre-built binary</strong>。<code>hugo server</code> 本地跑的是下載好的執行檔，CI 用 <code>peaceiris/actions-hugo</code> 這類 action，整個 blog 的 build 流程完全不碰 Go toolchain。</p>
<p>「專案已有 Go 依賴」這個前提不成立。真正要問的是：<strong>我是否願意為這組工具引入 Go toolchain 這個新依賴？</strong></p>
<h3 id="務實的對比">務實的對比</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Python</th>
          <th>Go</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pre-commit 啟動速度</td>
          <td>~50ms（interpreter 啟動）</td>
          <td><code>go run</code> ~500ms/次；pre-build binary 則要 commit 進 repo</td>
      </tr>
      <tr>
          <td>CI 新增依賴</td>
          <td><code>setup-python</code>（runner 通常自帶）</td>
          <td><code>setup-go</code> + build step</td>
      </tr>
      <tr>
          <td>開發速度（regex / 字串處理）</td>
          <td>快</td>
          <td>慢 2-3x，boilerplate 較多</td>
      </tr>
      <tr>
          <td>AST 解析選擇</td>
          <td>mistune / markdown-it-py</td>
          <td><strong>goldmark（與 Hugo 同源）</strong></td>
      </tr>
  </tbody>
</table>
<p>Go 唯一的決定性優勢是 goldmark — 跟 Hugo 用同一個 parser 可以保證「lint 通過 ↔ Hugo render 成功」等價。</p>
<h3 id="關鍵一問現在需要-ast-嗎">關鍵一問：現在需要 AST 嗎？</h3>
<p>我們最初傾向的是 tripwire 策略：<strong>現在用 Python + regex 先頂著，等 rule 複雜度超過臨界就升級 Go + goldmark</strong>。Tripwire 條件大致是：</p>
<ol>
<li>Rule 數量超過 5 條。</li>
<li>任一規則需要「這段文字在 code block 內嗎」這類上下文判斷。</li>
<li>Hugo render 結果跟 lint 判讀開始不一致。</li>
</ol>
<p>但事實是：</p>
<ul>
<li>MD024 的 siblings_only 已經需要父子關係追蹤 — 條件 2 馬上命中。</li>
<li>卡片雙向完整性是當前任務（不是未來可能）— 跨文件檢查 regex 做不到。</li>
</ul>
<p>兩個條件當下已經滿足，delay migration 反而要兩次寫工具。所以直接選 Go + goldmark。</p>
<p>這個決定的邏輯層面是：<strong>當需求已在手上而非 speculative，延遲決策的代價 &gt; 直接上的代價</strong>。</p>
<h2 id="為什麼選-goldmark">為什麼選 goldmark</h2>
<p>三個具體理由：</p>
<h3 id="1-解析結果與-hugo-一致">1. 解析結果與 Hugo 一致</h3>
<p>Hugo 的 content render pipeline 走 goldmark。用同一個 parser 寫 lint，可以杜絕「lint 通過但 Hugo render 失敗」或「Hugo 看得懂但 lint 誤判」這類長尾 bug。</p>
<h3 id="2-ast-api-直觀">2. AST API 直觀</h3>
<p>Goldmark 的 AST 節點型別設計貼近 CommonMark spec：<code>Document</code> / <code>Heading</code> / <code>Paragraph</code> / <code>Link</code> / <code>Table</code> / <code>FencedCodeBlock</code>。要寫 rule 時幾乎不需要翻對照表，直接比對心中的 markdown 結構。</p>
<h3 id="3-活躍且嵌入在主流-go-生態">3. 活躍且嵌入在主流 Go 生態</h3>
<p>Goldmark 是 Hugo 使用的 parser，社群活躍、bug fix 持續進來。不會變成 abandoned dependency。</p>
<h2 id="架構設計單一-binary--子命令">架構設計：單一 binary + 子命令</h2>
<p>三個檢查功能分開寫比較好懂，但如果寫成三個 binary，每次 pre-commit 都要 parse markdown 三次，對大型 repo（我們這個已經超過 300 個 markdown）會明顯拖慢。</p>
<p>折衷方案是<strong>單一 binary + 子命令</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">scripts/mdtools/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── go.mod
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">├── main.go                    # subcommand dispatcher
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">├── cmd/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   ├── fmt.go                 # mdtools fmt [--fix|--check]
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│   ├── lint.go                # mdtools lint
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│   └── cards.go               # mdtools cards
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">├── internal/
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│   ├── astutil/               # goldmark 封裝（parse, walk, parent chain）
</span></span><span class="line"><span class="ln">10</span><span class="cl">│   ├── rules/                 # 規則定義（可被三個子命令共用）
</span></span><span class="line"><span class="ln">11</span><span class="cl">│   │   ├── config.go          # 全域開關與參數
</span></span><span class="line"><span class="ln">12</span><span class="cl">│   │   ├── headings.go        # 標題規則
</span></span><span class="line"><span class="ln">13</span><span class="cl">│   │   ├── urls.go            # URL + 反釣魚
</span></span><span class="line"><span class="ln">14</span><span class="cl">│   │   ├── tables.go          # 表格正規化
</span></span><span class="line"><span class="ln">15</span><span class="cl">│   │   ├── frontmatter.go     # front matter schema
</span></span><span class="line"><span class="ln">16</span><span class="cl">│   │   └── identifiers.go     # 識別碼白名單（CVE、KB、...）
</span></span><span class="line"><span class="ln">17</span><span class="cl">│   └── report/                # 統一錯誤輸出格式
</span></span><span class="line"><span class="ln">18</span><span class="cl">└── README.md</span></span></code></pre></div><p>三個子命令共享 <code>internal/astutil</code> 和 <code>internal/rules</code>，同一個 parse 結果可以在不同規則間重用。</p>
<h2 id="實際走訪md024-siblings_only-在-goldmark-上怎麼寫">實際走訪：MD024 siblings_only 在 goldmark 上怎麼寫</h2>
<p>這段是示範 AST-based rule 的可讀性，不是最終實作版本。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">package</span> <span class="nx">rules</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s">&#34;bytes&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="s">&#34;github.com/yuin/goldmark/ast&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="s">&#34;github.com/yuin/goldmark/text&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// CheckSiblingsOnlyHeadings walks the document and flags headings</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// that share the same text with a sibling under the same parent heading.</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kd">func</span> <span class="nf">CheckSiblingsOnlyHeadings</span><span class="p">(</span><span class="nx">doc</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">src</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="nx">Violation</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="kd">var</span> <span class="nx">violations</span> <span class="p">[]</span><span class="nx">Violation</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1">// parentMap[level] 保留目前走到的各層 heading，作為後續 H(n+1) 的 parent context</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">parentMap</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">int</span><span class="p">]</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Heading</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="c1">// 每個 parent context 下，收集已見過的子 heading 文字</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">seenUnderParent</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Heading</span><span class="p">]</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">ast</span><span class="p">.</span><span class="nf">Walk</span><span class="p">(</span><span class="nx">doc</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">n</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">WalkStatus</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">entering</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkContinue</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="nx">h</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.(</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Heading</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkContinue</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="nx">text</span> <span class="o">:=</span> <span class="nb">string</span><span class="p">(</span><span class="nx">h</span><span class="p">.</span><span class="nf">Text</span><span class="p">(</span><span class="nx">src</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="nx">parent</span> <span class="o">:=</span> <span class="nx">parentMap</span><span class="p">[</span><span class="nx">h</span><span class="p">.</span><span class="nx">Level</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="c1">// 直接上層 heading</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="nx">seen</span><span class="p">,</span> <span class="nx">exists</span> <span class="o">:=</span> <span class="nx">seenUnderParent</span><span class="p">[</span><span class="nx">parent</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">exists</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">            <span class="nx">seen</span> <span class="p">=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">            <span class="nx">seenUnderParent</span><span class="p">[</span><span class="nx">parent</span><span class="p">]</span> <span class="p">=</span> <span class="nx">seen</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="k">if</span> <span class="nx">prev</span><span class="p">,</span> <span class="nx">dup</span> <span class="o">:=</span> <span class="nx">seen</span><span class="p">[</span><span class="nx">text</span><span class="p">];</span> <span class="nx">dup</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">            <span class="nx">violations</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">violations</span><span class="p">,</span> <span class="nx">Violation</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">                <span class="nx">Rule</span><span class="p">:</span>    <span class="s">&#34;MD024-siblings_only&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">                <span class="nx">Node</span><span class="p">:</span>    <span class="nx">h</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">                <span class="nx">Message</span><span class="p">:</span> <span class="s">&#34;duplicate heading under the same parent: &#34;</span> <span class="o">+</span> <span class="nx">text</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">                <span class="nx">Prev</span><span class="p">:</span>    <span class="nx">prev</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">            <span class="p">})</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">            <span class="nx">seen</span><span class="p">[</span><span class="nx">text</span><span class="p">]</span> <span class="p">=</span> <span class="nx">h</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">
</span></span><span class="line"><span class="ln">49</span><span class="cl">        <span class="nx">parentMap</span><span class="p">[</span><span class="nx">h</span><span class="p">.</span><span class="nx">Level</span><span class="p">]</span> <span class="p">=</span> <span class="nx">h</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        <span class="c1">// 進到更深層時，清空下層的舊狀態</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">        <span class="k">for</span> <span class="nx">lv</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">Level</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">lv</span> <span class="o">&lt;=</span> <span class="mi">6</span><span class="p">;</span> <span class="nx">lv</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">            <span class="nb">delete</span><span class="p">(</span><span class="nx">parentMap</span><span class="p">,</span> <span class="nx">lv</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">
</span></span><span class="line"><span class="ln">55</span><span class="cl">        <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkContinue</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="k">return</span> <span class="nx">violations</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>對比 regex 版本要自己寫「目前 H2 是誰」狀態機 + 「切回上層時清狀態」— goldmark 的 walker pattern 把階層邏輯外部化到樹結構，rule 本身只處理「同一 parent 下有沒有重複」的核心語義。</p>
<p>幾百行 regex 才能穩定做到的事，AST 版本大概 30 行。規則越多，這個倍率越明顯。</p>
<h2 id="pre-commit-與-ci-整合">Pre-commit 與 CI 整合</h2>
<h3 id="本地開發githookspre-commit-與-githookspre-push">本地開發：<code>.githooks/pre-commit</code> 與 <code>.githooks/pre-push</code></h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 確保 binary 最新</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> ! -x bin/mdtools <span class="o">]]</span> <span class="o">||</span> <span class="o">[[</span> scripts/mdtools/main.go -nt bin/mdtools <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Rebuilding mdtools...&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="o">(</span><span class="nb">cd</span> scripts/mdtools <span class="o">&amp;&amp;</span> go build -o ../../bin/mdtools .<span class="o">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 三段式檢查</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">bin/mdtools fmt --fix   <span class="c1"># 自動修格式；改動會 re-stage</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">git add <span class="k">$(</span>git diff --name-only --cached --diff-filter<span class="o">=</span>AM <span class="p">|</span> grep <span class="s1">&#39;\.md$&#39;</span> <span class="o">||</span> <span class="nb">true</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">bin/mdtools lint        <span class="c1"># 結構檢查，失敗即阻擋</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">bin/mdtools cards       <span class="c1"># 跨文件檢查，失敗即阻擋</span></span></span></code></pre></div><p><code>pre-push</code> 補上全量 gate：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">make check</span></span></code></pre></div><p>關鍵設計：</p>
<ul>
<li><code>mdtools fmt --fix</code> 會改檔，改完後要 <code>git add</code> 回 staged，否則 commit 進去的還是舊內容。</li>
<li><code>lint</code> 和 <code>cards</code> 不改檔，只讀與報告。</li>
<li><code>pre-commit</code> 保持 staged-file scoped，讓 commit 回饋夠快；<code>pre-push</code> 跑全量 <code>make check</code>，讓本機結果和 CI 同步。</li>
<li>Binary mtime 檢查避免每次 commit 都 rebuild。</li>
<li><code>bin/mdtools</code> 本身 gitignore，不 commit 進 repo。</li>
</ul>
<h3 id="cigithubworkflowsmd-checkyml">CI：<code>.github/workflows/md-check.yml</code></h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">md-check</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">push, pull_request]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">check</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-go@v5</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">go-version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;stable&#39;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build mdtools</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">(cd scripts/mdtools &amp;&amp; go build -o ../../bin/mdtools .)</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Format check</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">bin/mdtools fmt --check</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Structural lint</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">bin/mdtools lint</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Cross-file completeness</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">bin/mdtools cards</span></span></span></code></pre></div><p>CI 用 <code>--check</code> 而非 <code>--fix</code> — 任何格式偏差都 fail，不自動修（避免 CI 把修復 commit 推回去造成誤會）。</p>
<h3 id="安裝-hook">安裝 hook</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git config core.hooksPath .githooks
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或用 Makefile target：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">make install-hooks</span></span></code></pre></div><h2 id="維運成本的長期考量">維運成本的長期考量</h2>
<h3 id="誤判率是規則生命週期的關鍵">誤判率是規則生命週期的關鍵</h3>
<p>每條規則都可能誤判。我們的處理策略寫在規範的規則擴充流程段：</p>
<ol>
<li>新規則先在 <code>internal/rules/</code> 實作為<strong>可開關</strong>（預設關）。</li>
<li>在代表性檔案上測試誤判率。</li>
<li>誤判率 &lt; 1% 且有明確教材品質收益時，預設開啟。</li>
<li>預設開啟後，同步修正既有違規。</li>
</ol>
<p>關鍵在「預設關閉」這一步 — 給規則一個試水期，不會直接擋 commit。</p>
<h3 id="規則與-spec-文件的同步">規則與 spec 文件的同步</h3>
<p>Rule config 在 <code>internal/rules/config.go</code>，spec 文件在 <code>content/posts/markdown-writing-spec.md</code>。兩者修改時必須同步，否則會出現「spec 寫的規則跟工具實際跑的規則不同步」的沉默 bug。</p>
<p>這是目前靠紀律維持的部分。未來如果發現同步偏差重複發生，可以考慮從 config.go 產生 spec 的片段（或反過來）。目前手動同步的成本還可接受。</p>
<h3 id="規則數量的預期曲線">規則數量的預期曲線</h3>
<p>當前覆蓋 22 條 rule-config 條目。接下來加規則的收益會遞減 — 大部分重要的基礎格式 + 結構 + 跨文件檢查都已在內。未來新增應該集中在：</p>
<ul>
<li>新內容類型帶來的 schema 擴充（例如做 podcast 或者 video posts）。</li>
<li>術語字典完成後的 <strong>L3 術語覆蓋</strong>（正文首次出現術語自動連卡片）。</li>
<li>特定領域的品質檢查（例如紅隊教材「每個案例必須有 3 來源」）。</li>
</ul>
<p>基礎 markdownlint 規則能加的都加完了，再追規則就是在吸邊際收益極低的條目，不值得。</p>
<h2 id="延伸閱讀">延伸閱讀</h2>
<ul>
<li><a href="/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST — 從字串到語法樹的視角轉換</a> — 為什麼要升級到 AST 工具鏈</li>
<li><a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查</a> — mdtools 檢查的完整規則清單</li>
<li><a href="https://github.com/yuin/goldmark">goldmark 官方 repo</a> — Hugo 所用的 markdown parser</li>
<li><a href="https://pkg.go.dev/github.com/yuin/goldmark/ast">goldmark AST package reference</a> — <code>ast.Walk</code>、節點型別、parent traversal API</li>
</ul>
]]></content:encoded></item><item><title>什麼是 AST — 從字串到語法樹的視角轉換</title><link>https://tarrragon.github.io/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/</guid><description>&lt;h2 id="為什麼會碰到這個詞">為什麼會碰到這個詞&lt;/h2>
&lt;p>最初的問題很小：blog 文章數量成長後，每次 commit 都會收到 markdownlint 的同類警告反覆出現。有三個代表性的：&lt;/p>
&lt;ul>
&lt;li>&lt;code>MD034/no-bare-urls&lt;/code> — 裸 URL 散落在段落與表格。&lt;/li>
&lt;li>&lt;code>MD024/no-duplicate-heading&lt;/code> — 平行結構章節（例如 13 個案例各自有 &lt;code>### 弱點環節&lt;/code>）全部被判重複。&lt;/li>
&lt;li>&lt;code>MD060/table-column-style&lt;/code> — 表格管線前後空白不一致。&lt;/li>
&lt;/ul>
&lt;p>前兩個用現成工具 &lt;code>--fix&lt;/code> 不一定修得乾淨，因為 &lt;code>MD024&lt;/code> 的「重複」在我們的語境下是&lt;strong>合法的平行結構&lt;/strong>（不同父標題下重名其實是特色），而「裸 URL 轉換」要處理表格儲存格、程式碼區塊等特殊情境，單純 regex 會誤判。&lt;/p>
&lt;p>討論到後來關鍵字出現：&lt;strong>要做得精確，可能要用 AST 工具，而不是 regex 工具&lt;/strong>。&lt;/p>
&lt;p>那麼 AST 到底是什麼？跟我們熟悉的 regex / 字串處理差在哪？&lt;/p>
&lt;h2 id="regex-工具看世界的方式字元序列">Regex 工具看世界的方式：字元序列&lt;/h2>
&lt;p>Regex 工具處理 markdown 的方式是「逐行掃描 + pattern matching」。它看到的是字元流，沒有語法結構的概念。&lt;/p>
&lt;p>舉個例子：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gu">## 【案例一】Uber 2022
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">事件中攻擊者取得 &lt;span class="sb">`session_token`&lt;/span>，參考：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">https://www.uber.com/newsroom/security-update/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">| 事件 | 來源 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">| --------- | ---------------- |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">| Uber 2022 | https://uber.com |&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Regex 工具看到的是：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Line 1: &amp;#34;## 【案例一】Uber 2022&amp;#34; ← ^#{1,6} match → heading
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Line 3: &amp;#34;事件中攻擊者取得 `session_token`...&amp;#34; ← 無 pattern 命中
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Line 4: &amp;#34;https://www.uber.com/...&amp;#34; ← ^https?:// match → bare URL!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">Line 7: &amp;#34;| Uber 2022 | https://uber.com |&amp;#34; ← ^\| match → table row&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每一行獨立判讀，沒有上下文。Regex 工具不知道 line 4 到底是「段落的一部分」、「引用區塊裡的連結」、還是「程式碼範例」。它只看 pattern。&lt;/p>
&lt;h2 id="ast-工具看世界的方式語法樹">AST 工具看世界的方式：語法樹&lt;/h2>
&lt;p>AST = Abstract Syntax Tree，抽象語法樹。AST 工具先把整段 markdown 用 parser &lt;strong>解析成結構化的樹&lt;/strong>，然後工具在樹上走訪（traverse），操作「節點」而不是「行」。&lt;/p>
&lt;p>同一段 markdown，goldmark（Hugo 內建的 markdown parser）解析後的樹大致是：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">Document
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">├── Heading (level=2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">│ └── Text: &amp;#34;【案例一】Uber 2022&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">├── Paragraph
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ ├── Text: &amp;#34;事件中攻擊者取得 &amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">│ ├── CodeSpan: &amp;#34;session_token&amp;#34; ← 知道這是 inline code
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ ├── Text: &amp;#34;，參考：&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">│ └── AutoLink: &amp;#34;https://www.uber.com/...&amp;#34; ← 知道這是段落中的裸 URL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">└── Table
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> ├── TableHeader: [事件, 來源]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> └── TableRow
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> ├── TableCell: &amp;#34;Uber 2022&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> └── TableCell: AutoLink &amp;#34;https://uber.com&amp;#34; ← 知道這是表格儲存格中的 URL&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對同一個 URL，AST 工具能分辨「它在段落裡」還是「它在表格儲存格裡」還是「它在程式碼區塊裡」— 因為節點的父子關係已經是樹的一部分。&lt;/p></description><content:encoded><![CDATA[<h2 id="為什麼會碰到這個詞">為什麼會碰到這個詞</h2>
<p>最初的問題很小：blog 文章數量成長後，每次 commit 都會收到 markdownlint 的同類警告反覆出現。有三個代表性的：</p>
<ul>
<li><code>MD034/no-bare-urls</code> — 裸 URL 散落在段落與表格。</li>
<li><code>MD024/no-duplicate-heading</code> — 平行結構章節（例如 13 個案例各自有 <code>### 弱點環節</code>）全部被判重複。</li>
<li><code>MD060/table-column-style</code> — 表格管線前後空白不一致。</li>
</ul>
<p>前兩個用現成工具 <code>--fix</code> 不一定修得乾淨，因為 <code>MD024</code> 的「重複」在我們的語境下是<strong>合法的平行結構</strong>（不同父標題下重名其實是特色），而「裸 URL 轉換」要處理表格儲存格、程式碼區塊等特殊情境，單純 regex 會誤判。</p>
<p>討論到後來關鍵字出現：<strong>要做得精確，可能要用 AST 工具，而不是 regex 工具</strong>。</p>
<p>那麼 AST 到底是什麼？跟我們熟悉的 regex / 字串處理差在哪？</p>
<h2 id="regex-工具看世界的方式字元序列">Regex 工具看世界的方式：字元序列</h2>
<p>Regex 工具處理 markdown 的方式是「逐行掃描 + pattern matching」。它看到的是字元流，沒有語法結構的概念。</p>
<p>舉個例子：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 【案例一】Uber 2022
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">事件中攻擊者取得 <span class="sb">`session_token`</span>，參考：
</span></span><span class="line"><span class="ln">4</span><span class="cl">https://www.uber.com/newsroom/security-update/
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">| 事件      | 來源             |
</span></span><span class="line"><span class="ln">7</span><span class="cl">| --------- | ---------------- |
</span></span><span class="line"><span class="ln">8</span><span class="cl">| Uber 2022 | https://uber.com |</span></span></code></pre></div><p>Regex 工具看到的是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Line 1: &#34;## 【案例一】Uber 2022&#34;            ← ^#{1,6} match  → heading
</span></span><span class="line"><span class="ln">2</span><span class="cl">Line 3: &#34;事件中攻擊者取得 `session_token`...&#34;  ← 無 pattern 命中
</span></span><span class="line"><span class="ln">3</span><span class="cl">Line 4: &#34;https://www.uber.com/...&#34;            ← ^https?:// match → bare URL!
</span></span><span class="line"><span class="ln">4</span><span class="cl">Line 7: &#34;| Uber 2022 | https://uber.com |&#34;   ← ^\| match     → table row</span></span></code></pre></div><p>每一行獨立判讀，沒有上下文。Regex 工具不知道 line 4 到底是「段落的一部分」、「引用區塊裡的連結」、還是「程式碼範例」。它只看 pattern。</p>
<h2 id="ast-工具看世界的方式語法樹">AST 工具看世界的方式：語法樹</h2>
<p>AST = Abstract Syntax Tree，抽象語法樹。AST 工具先把整段 markdown 用 parser <strong>解析成結構化的樹</strong>，然後工具在樹上走訪（traverse），操作「節點」而不是「行」。</p>
<p>同一段 markdown，goldmark（Hugo 內建的 markdown parser）解析後的樹大致是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">Document
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── Heading (level=2)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│   └── Text: &#34;【案例一】Uber 2022&#34;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">├── Paragraph
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   ├── Text: &#34;事件中攻擊者取得 &#34;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│   ├── CodeSpan: &#34;session_token&#34;            ← 知道這是 inline code
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│   ├── Text: &#34;，參考：&#34;
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│   └── AutoLink: &#34;https://www.uber.com/...&#34;  ← 知道這是段落中的裸 URL
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">└── Table
</span></span><span class="line"><span class="ln">10</span><span class="cl">    ├── TableHeader: [事件, 來源]
</span></span><span class="line"><span class="ln">11</span><span class="cl">    └── TableRow
</span></span><span class="line"><span class="ln">12</span><span class="cl">        ├── TableCell: &#34;Uber 2022&#34;
</span></span><span class="line"><span class="ln">13</span><span class="cl">        └── TableCell: AutoLink &#34;https://uber.com&#34;  ← 知道這是表格儲存格中的 URL</span></span></code></pre></div><p>對同一個 URL，AST 工具能分辨「它在段落裡」還是「它在表格儲存格裡」還是「它在程式碼區塊裡」— 因為節點的父子關係已經是樹的一部分。</p>
<p>這個差異乍看像技術細節，實際影響的是能寫出什麼樣的規則。</p>
<h2 id="典型意外情境regex-會誤判的三個-case">典型意外情境：regex 會誤判的三個 case</h2>
<h3 id="程式碼區塊內的-url">程式碼區塊內的 URL</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 測試範例
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">```bash
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s"></span>curl https://example.com/api  <span class="c1"># 這是程式碼範例，不該報 MD034</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">```</span></span></span></code></pre></div><p>Regex 看到 <code>https://</code> 開頭就標記裸 URL。AST 知道這一行在 <code>FencedCodeBlock</code> 子樹內，跳過。</p>
<h3 id="front-matter-裡的--被當-heading">Front matter 裡的 <code>#</code> 被當 heading</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">---
</span></span><span class="line"><span class="ln">2</span><span class="cl">title: &#34;Python 的 # 註解語法&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">---
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">真正的文章內容...</span></span></code></pre></div><p>Regex 看到 <code>^#</code> 就當 heading 記一筆（title 裡面有 <code>#</code> 字元）。AST 知道 <code>---...---</code> 區塊是 YAML front matter，title 的值是字串。</p>
<h3 id="平行結構標題被誤判為重複">平行結構標題被誤判為重複</h3>
<p>在多案例教材裡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 【案例一】Uber 2022
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="gu">### 弱點環節 ← 第 1 次出現
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="gu">### 攻擊路徑
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="gu">## 【案例二】Okta 2023
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">### 弱點環節 ← 第 2 次出現，regex 會直接報重複</span></span></code></pre></div><p>要用 regex 實作「不同父標題下允許重複」這種 <code>siblings_only</code> 規則，需要自己維護狀態機追蹤「目前 H2 是誰」「遇到 H3 時算哪個 H2 底下」。遇到 H4/H5 階層更複雜。</p>
<p>用 AST，父子關係已經內建在樹結構裡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 偽代碼，實際用 goldmark walker 取代</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">h2</span> <span class="o">:=</span> <span class="k">range</span> <span class="nf">allHeadingsAtLevel</span><span class="p">(</span><span class="nx">doc</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">children</span> <span class="o">:=</span> <span class="nf">childrenOfType</span><span class="p">(</span><span class="nx">h2</span><span class="p">,</span> <span class="nx">Heading</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nf">checkDuplicates</span><span class="p">(</span><span class="nx">children</span><span class="p">)</span>  <span class="c1">// 自動只比對同一 H2 下的子標題</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不用追蹤狀態，邏輯上直接表達。</p>
<h2 id="為什麼對我們特別重要goldmark--hugo-的-parser">為什麼對我們特別重要：goldmark = Hugo 的 parser</h2>
<p>Hugo（blog 的 static site generator）內建的 markdown parser 就是 goldmark。用 goldmark 寫 lint 有個平凡但關鍵的保證：<strong>lint 的判讀跟 Hugo render 的判讀完全一致</strong>。</p>
<p>如果用不同的 parser 寫 lint（例如 Python 的 <code>mistune</code>、JavaScript 的 <code>markdown-it</code>），很可能遇到這種尷尬：</p>
<ul>
<li>Lint 通過，但 Hugo 解析不出來，render 失敗或跑版。</li>
<li>Lint 報錯，但 Hugo 看得懂、實際沒有問題。</li>
</ul>
<p>兩套 parser 解讀差異是長尾 bug 的溫床。用同一個 parser 可以從源頭杜絕這類不一致。</p>
<h2 id="什麼時候-ast-不是必要的">什麼時候 AST 不是必要的</h2>
<p>不要為了「比較先進」就上 AST。Regex 在下列情境完全夠用：</p>
<ul>
<li>檢查每行開頭字元（<code>^#</code>、<code>^|</code>、<code>^- </code>）。</li>
<li>簡單字串替換（例如 URL 前後加 <code>&lt;&gt;</code> 包裹）。</li>
<li>不需要知道上下文的格式正規化（行尾空白、tab 轉空白）。</li>
</ul>
<p>需要 AST 才能穩定做到的是：</p>
<ul>
<li>判斷「這段文字在 code block 內嗎？」</li>
<li>判斷「這個 heading 的父 heading 是誰？」</li>
<li>追蹤跨文件的連結關係（卡片 backlink 完整性）。</li>
<li>檢查「這個 Strong 節點是不是整個段落的唯一子節點？」（MD036 粗體當標題濫用）</li>
</ul>
<p>一個實務判準：<strong>如果 rule 需要「知道這段文字處在什麼結構中」，regex 會卡住；AST 天生就有這個資訊。</strong></p>
<h2 id="我們的判斷什麼時機該升級到-ast">我們的判斷：什麼時機該升級到 AST</h2>
<p>blog 專案一開始也考慮過用 Python + regex 先頂著，等規則變複雜再升級 Go + goldmark。後來有兩件事讓我們直接選 AST：</p>
<ol>
<li><strong>MD024 siblings_only</strong> 已經是「需要上下文」的規則，regex 做得到但會寫得脆弱。</li>
<li><strong>知識卡片雙向完整性</strong>是當前在做的工作（每張卡片要被正文連到、每張卡片首段要有鄰卡連結），這類<strong>跨文件 + 段落歸屬</strong>的檢查，regex 做不出來。</li>
</ol>
<p>當需求已經在手上，延遲決策反而更貴。對我們來說，AST 不是超前部署，是<strong>現在的 blocker</strong>。</p>
<h2 id="延伸閱讀">延伸閱讀</h2>
<ul>
<li><a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查</a> — 所有規則的正式契約</li>
<li><a href="/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools：Go + goldmark 的 markdown 工具鏈設計</a> — 如何把 AST 能力組裝成 pre-commit hook</li>
<li><a href="https://github.com/yuin/goldmark">goldmark 官方 repo</a> — Hugo 所用的 markdown parser</li>
</ul>
]]></content:encoded></item><item><title>Hugo Shortcode 實現可折疊區塊</title><link>https://tarrragon.github.io/blog/posts/hugo-shortcode-%E5%AF%A6%E7%8F%BE%E5%8F%AF%E6%8A%98%E7%96%8A%E5%8D%80%E5%A1%8A/</link><pubDate>Thu, 09 Oct 2025 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/hugo-shortcode-%E5%AF%A6%E7%8F%BE%E5%8F%AF%E6%8A%98%E7%96%8A%E5%8D%80%E5%A1%8A/</guid><description>&lt;h2 id="問題背景">問題背景&lt;/h2>
&lt;p>在撰寫技術文章時，我們會在文章中加入程式碼範例。但是 blog 的目標是分享我處理問題的思路，而不是提供解決方案，所以我希望預設把程式碼隱藏。&lt;/p>
&lt;h3 id="最初的解決方案">最初的解決方案&lt;/h3>
&lt;p>使用 HTML5 的 &lt;code>&amp;lt;details&amp;gt;&lt;/code> 和 &lt;code>&amp;lt;summary&amp;gt;&lt;/code> 標籤：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>點擊查看程式碼&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">\```javascript
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">console.log(&amp;#39;Hello World&amp;#39;);
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">\```
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個方案雖然功能正常，但會觸發 &lt;strong>MD033 Markdown Linter 警告&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">MD033/no-inline-html: Inline HTML [Element: details]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="為什麼會有這個警告">為什麼會有這個警告？&lt;/h2>
&lt;h3 id="markdown-設計哲學">Markdown 設計哲學&lt;/h3>
&lt;p>Markdown 的設計理念是：&lt;/p>
&lt;ul>
&lt;li>保持純文字的可讀性&lt;/li>
&lt;li>避免直接使用 HTML 標籤&lt;/li>
&lt;li>使用語義化的標記語法&lt;/li>
&lt;/ul>
&lt;h3 id="md033-規則的目的">MD033 規則的目的&lt;/h3>
&lt;p>MD033 規則旨在：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>維持 Markdown 的純淨性&lt;/strong>：避免 HTML 與 Markdown 混用&lt;/li>
&lt;li>&lt;strong>提升可維護性&lt;/strong>：純 Markdown 更容易閱讀和維護&lt;/li>
&lt;li>&lt;strong>確保相容性&lt;/strong>：不同的 Markdown 渲染器對 HTML 的支援程度不同&lt;/li>
&lt;/ol>
&lt;h2 id="hugo-shortcode-解決方案">Hugo Shortcode 解決方案&lt;/h2>
&lt;h3 id="什麼是-shortcode">什麼是 Shortcode？&lt;/h3>
&lt;p>Hugo Shortcode 是 Hugo 靜態網站生成器提供的一個強大功能，允許你：&lt;/p>
&lt;ul>
&lt;li>在 Markdown 中使用自定義的簡短標記&lt;/li>
&lt;li>封裝複雜的 HTML 結構&lt;/li>
&lt;li>保持 Markdown 文件的整潔&lt;/li>
&lt;/ul>
&lt;h3 id="優勢分析">優勢分析&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>特性&lt;/th>
 &lt;th>HTML 標籤&lt;/th>
 &lt;th>Hugo Shortcode&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Markdown Linter&lt;/td>
 &lt;td>觸發警告&lt;/td>
 &lt;td>無警告&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可維護性&lt;/td>
 &lt;td>分散在各處&lt;/td>
 &lt;td>集中管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可讀性&lt;/td>
 &lt;td>較差&lt;/td>
 &lt;td>優秀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>彈性&lt;/td>
 &lt;td>固定結構&lt;/td>
 &lt;td>可自定義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hugo 最佳實踐&lt;/td>
 &lt;td>不推薦&lt;/td>
 &lt;td>官方推薦&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="實現步驟">實現步驟&lt;/h2>
&lt;h3 id="步驟-1創建-shortcode-檔案">步驟 1：創建 Shortcode 檔案&lt;/h3>
&lt;p>在專案根目錄創建 &lt;code>layouts/shortcodes/details.html&lt;/code>：&lt;/p>
&lt;details>
 &lt;summary>點擊查看 Shortcode 程式碼&lt;/summary>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">{{/* 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> Details shortcode - 用於創建可折疊的內容區塊
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> 使用方式:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>點擊展開&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> 內容...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> 參數:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> - summary: 摘要文字（可選，預設為 &amp;#34;點擊展開&amp;#34;）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">*/}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{ .Get &amp;#34;summary&amp;#34; | default &amp;#34;點擊展開&amp;#34; }}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> {{ .Inner | markdownify }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;p>&lt;strong>程式碼說明&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>註解區塊&lt;/strong>：&lt;code>{{/* ... */}}&lt;/code> 用於說明 shortcode 的用途和使用方式&lt;/li>
&lt;li>&lt;strong>參數獲取&lt;/strong>：&lt;code>.Get &amp;quot;summary&amp;quot;&lt;/code> 獲取 summary 參數&lt;/li>
&lt;li>&lt;strong>預設值&lt;/strong>：&lt;code>default &amp;quot;點擊展開&amp;quot;&lt;/code> 提供預設文字&lt;/li>
&lt;li>&lt;strong>內容處理&lt;/strong>：&lt;code>.Inner&lt;/code> 獲取標籤內的內容&lt;/li>
&lt;li>&lt;strong>Markdown 渲染&lt;/strong>：&lt;code>markdownify&lt;/code> 將內容中的 Markdown 語法轉換為 HTML&lt;/li>
&lt;/ol>
&lt;h3 id="步驟-2在-markdown-中使用">步驟 2：在 Markdown 中使用&lt;/h3>
&lt;h4 id="舊方式會觸發-md033">舊方式（會觸發 MD033）&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>點擊查看程式碼&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">\``&lt;span class="sb">`toml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="sb">[markup]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="sb"> [markup.tableOfContents]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="sb"> startLevel = 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="sb">\`&lt;/span>``
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">details&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="新方式符合-markdown-規範">新方式（符合 Markdown 規範）&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">

&lt;details>
 &lt;summary>點擊查看程式碼&lt;/summary>
 

\```toml
[markup]
 [markup.tableOfContents]
 startLevel = 2
\```


&lt;/details>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="步驟-3添加-css-樣式">步驟 3：添加 CSS 樣式&lt;/h3>
&lt;p>在 &lt;code>layouts/partials/custom_head.html&lt;/code> 中添加樣式：&lt;/p></description><content:encoded><![CDATA[<h2 id="問題背景">問題背景</h2>
<p>在撰寫技術文章時，我們會在文章中加入程式碼範例。但是 blog 的目標是分享我處理問題的思路，而不是提供解決方案，所以我希望預設把程式碼隱藏。</p>
<h3 id="最初的解決方案">最初的解決方案</h3>
<p>使用 HTML5 的 <code>&lt;details&gt;</code> 和 <code>&lt;summary&gt;</code> 標籤：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">details</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">summary</span><span class="p">&gt;</span>點擊查看程式碼<span class="p">&lt;/</span><span class="nt">summary</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">\```javascript
</span></span><span class="line"><span class="ln">5</span><span class="cl">console.log(&#39;Hello World&#39;);
</span></span><span class="line"><span class="ln">6</span><span class="cl">\```
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">&lt;/</span><span class="nt">details</span><span class="p">&gt;</span></span></span></code></pre></div><p>這個方案雖然功能正常，但會觸發 <strong>MD033 Markdown Linter 警告</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">MD033/no-inline-html: Inline HTML [Element: details]</span></span></code></pre></div><h2 id="為什麼會有這個警告">為什麼會有這個警告？</h2>
<h3 id="markdown-設計哲學">Markdown 設計哲學</h3>
<p>Markdown 的設計理念是：</p>
<ul>
<li>保持純文字的可讀性</li>
<li>避免直接使用 HTML 標籤</li>
<li>使用語義化的標記語法</li>
</ul>
<h3 id="md033-規則的目的">MD033 規則的目的</h3>
<p>MD033 規則旨在：</p>
<ol>
<li><strong>維持 Markdown 的純淨性</strong>：避免 HTML 與 Markdown 混用</li>
<li><strong>提升可維護性</strong>：純 Markdown 更容易閱讀和維護</li>
<li><strong>確保相容性</strong>：不同的 Markdown 渲染器對 HTML 的支援程度不同</li>
</ol>
<h2 id="hugo-shortcode-解決方案">Hugo Shortcode 解決方案</h2>
<h3 id="什麼是-shortcode">什麼是 Shortcode？</h3>
<p>Hugo Shortcode 是 Hugo 靜態網站生成器提供的一個強大功能，允許你：</p>
<ul>
<li>在 Markdown 中使用自定義的簡短標記</li>
<li>封裝複雜的 HTML 結構</li>
<li>保持 Markdown 文件的整潔</li>
</ul>
<h3 id="優勢分析">優勢分析</h3>
<table>
  <thead>
      <tr>
          <th>特性</th>
          <th>HTML 標籤</th>
          <th>Hugo Shortcode</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Markdown Linter</td>
          <td>觸發警告</td>
          <td>無警告</td>
      </tr>
      <tr>
          <td>可維護性</td>
          <td>分散在各處</td>
          <td>集中管理</td>
      </tr>
      <tr>
          <td>可讀性</td>
          <td>較差</td>
          <td>優秀</td>
      </tr>
      <tr>
          <td>彈性</td>
          <td>固定結構</td>
          <td>可自定義</td>
      </tr>
      <tr>
          <td>Hugo 最佳實踐</td>
          <td>不推薦</td>
          <td>官方推薦</td>
      </tr>
  </tbody>
</table>
<h2 id="實現步驟">實現步驟</h2>
<h3 id="步驟-1創建-shortcode-檔案">步驟 1：創建 Shortcode 檔案</h3>
<p>在專案根目錄創建 <code>layouts/shortcodes/details.html</code>：</p>
<details>
  <summary>點擊查看 Shortcode 程式碼</summary>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln"> 1</span><span class="cl">{{/* 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  Details shortcode - 用於創建可折疊的內容區塊
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  使用方式:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">&lt;</span><span class="nt">details</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">&lt;</span><span class="nt">summary</span><span class="p">&gt;</span>點擊展開<span class="p">&lt;/</span><span class="nt">summary</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  
</span></span><span class="line"><span class="ln">10</span><span class="cl">  內容...
</span></span><span class="line"><span class="ln">11</span><span class="cl">  
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">&lt;/</span><span class="nt">details</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  
</span></span><span class="line"><span class="ln">14</span><span class="cl">  參數:
</span></span><span class="line"><span class="ln">15</span><span class="cl">  - summary: 摘要文字（可選，預設為 &#34;點擊展開&#34;）
</span></span><span class="line"><span class="ln">16</span><span class="cl">*/}}
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">&lt;</span><span class="nt">details</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="p">&lt;</span><span class="nt">summary</span><span class="p">&gt;</span>{{ .Get &#34;summary&#34; | default &#34;點擊展開&#34; }}<span class="p">&lt;/</span><span class="nt">summary</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  {{ .Inner | markdownify }}
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">&lt;/</span><span class="nt">details</span><span class="p">&gt;</span></span></span></code></pre></div></details>
<p><strong>程式碼說明</strong>：</p>
<ol>
<li><strong>註解區塊</strong>：<code>{{/* ... */}}</code> 用於說明 shortcode 的用途和使用方式</li>
<li><strong>參數獲取</strong>：<code>.Get &quot;summary&quot;</code> 獲取 summary 參數</li>
<li><strong>預設值</strong>：<code>default &quot;點擊展開&quot;</code> 提供預設文字</li>
<li><strong>內容處理</strong>：<code>.Inner</code> 獲取標籤內的內容</li>
<li><strong>Markdown 渲染</strong>：<code>markdownify</code> 將內容中的 Markdown 語法轉換為 HTML</li>
</ol>
<h3 id="步驟-2在-markdown-中使用">步驟 2：在 Markdown 中使用</h3>
<h4 id="舊方式會觸發-md033">舊方式（會觸發 MD033）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">&lt;</span><span class="nt">details</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">&lt;</span><span class="nt">summary</span><span class="p">&gt;</span>點擊查看程式碼<span class="p">&lt;/</span><span class="nt">summary</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">\``<span class="sb">`toml
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="sb">[markup]
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="sb">  [markup.tableOfContents]
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="sb">    startLevel = 2
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sb">\`</span>``
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">&lt;/</span><span class="nt">details</span><span class="p">&gt;</span></span></span></code></pre></div><h4 id="新方式符合-markdown-規範">新方式（符合 Markdown 規範）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">

<details>
  <summary>點擊查看程式碼</summary>
  

\```toml
[markup]
  [markup.tableOfContents]
    startLevel = 2
\```


</details></span></span></code></pre></div><h3 id="步驟-3添加-css-樣式">步驟 3：添加 CSS 樣式</h3>
<p>在 <code>layouts/partials/custom_head.html</code> 中添加樣式：</p>
<details>
  <summary>點擊查看 CSS 樣式程式碼</summary>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c">/* 可折疊程式碼區塊樣式 */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">details</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mf">1.5</span><span class="kt">rem</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">border-radius</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">transition</span><span class="p">:</span> <span class="kc">all</span> <span class="mf">0.3</span><span class="kt">s</span> <span class="kc">ease</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nt">details</span><span class="p">:</span><span class="nd">hover</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.08</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="k">border-color</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.15</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nt">details</span><span class="o">[</span><span class="nt">open</span><span class="o">]</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.03</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="k">border-color</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.2</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nt">summary</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="k">cursor</span><span class="p">:</span> <span class="kc">pointer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="k">font-weight</span><span class="p">:</span> <span class="mi">600</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="k">font-size</span><span class="p">:</span> <span class="mf">0.95</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">-1</span><span class="kt">rem</span> <span class="mi">-1</span><span class="kt">rem</span> <span class="mi">0</span> <span class="mi">-1</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="k">border-radius</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span> <span class="mi">8</span><span class="kt">px</span> <span class="mi">0</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="k">transition</span><span class="p">:</span> <span class="kc">all</span> <span class="mf">0.2</span><span class="kt">s</span> <span class="kc">ease</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="k">user-select</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="k">list-style</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="nt">summary</span><span class="p">::</span><span class="nd">-webkit-details-marker</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="nt">summary</span><span class="p">::</span><span class="nd">before</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">  <span class="k">content</span><span class="p">:</span> <span class="s1">&#39;▶&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">inline-block</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">  <span class="k">margin-right</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">  <span class="k">transition</span><span class="p">:</span> <span class="k">transform</span> <span class="mf">0.3</span><span class="kt">s</span> <span class="kc">ease</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">  <span class="k">font-size</span><span class="p">:</span> <span class="mf">0.8</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">
</span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="nt">details</span><span class="o">[</span><span class="nt">open</span><span class="o">]</span> <span class="nt">summary</span><span class="p">::</span><span class="nd">before</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">  <span class="k">transform</span><span class="p">:</span> <span class="nb">rotate</span><span class="p">(</span><span class="mi">90</span><span class="kt">deg</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">
</span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="nt">summary</span><span class="p">:</span><span class="nd">hover</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">
</span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="nt">details</span><span class="o">[</span><span class="nt">open</span><span class="o">]</span> <span class="nt">summary</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">  <span class="k">margin-bottom</span><span class="p">:</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">  <span class="k">border-bottom</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">  <span class="k">border-radius</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span> <span class="mi">8</span><span class="kt">px</span> <span class="mi">0</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">
</span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="c">/* 確保 details 內的程式碼區塊樣式正常 */</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl"><span class="nt">details</span> <span class="nt">pre</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">1</span><span class="kt">rem</span> <span class="mi">0</span> <span class="mi">0</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">
</span></span><span class="line"><span class="ln">65</span><span class="cl"><span class="nt">details</span> <span class="o">&gt;</span> <span class="o">*</span><span class="p">:</span><span class="nd">not</span><span class="o">(</span><span class="nt">summary</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">  <span class="k">animation</span><span class="p">:</span> <span class="n">fadeIn</span> <span class="mf">0.3</span><span class="kt">s</span> <span class="kc">ease</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl">
</span></span><span class="line"><span class="ln">69</span><span class="cl"><span class="p">@</span><span class="k">keyframes</span> <span class="nt">fadeIn</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">70</span><span class="cl">  <span class="nt">from</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl">    <span class="k">opacity</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl">    <span class="k">transform</span><span class="p">:</span> <span class="nb">translateY</span><span class="p">(</span><span class="mi">-10</span><span class="kt">px</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">73</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">74</span><span class="cl">  <span class="nt">to</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">75</span><span class="cl">    <span class="k">opacity</span><span class="p">:</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">76</span><span class="cl">    <span class="k">transform</span><span class="p">:</span> <span class="nb">translateY</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">77</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">78</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">79</span><span class="cl">
</span></span><span class="line"><span class="ln">80</span><span class="cl"><span class="c">/* 響應式設計 */</span>
</span></span><span class="line"><span class="ln">81</span><span class="cl"><span class="p">@</span><span class="k">media</span> <span class="o">(</span><span class="nt">max-width</span><span class="o">:</span> <span class="nt">768px</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">82</span><span class="cl">  <span class="nt">details</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">83</span><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mi">1</span><span class="kt">rem</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">84</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mf">0.8</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">85</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">86</span><span class="cl">
</span></span><span class="line"><span class="ln">87</span><span class="cl">  <span class="nt">summary</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">88</span><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mf">0.9</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">89</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mf">0.4</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">90</span><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mf">-0.8</span><span class="kt">rem</span> <span class="mf">-0.8</span><span class="kt">rem</span> <span class="mi">0</span> <span class="mf">-0.8</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">91</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">92</span><span class="cl"><span class="p">}</span></span></span></code></pre></div></details>
<h2 id="進階功能">進階功能</h2>
<h3 id="自定義參數">自定義參數</h3>
<p>你可以擴展 shortcode 支援更多參數：</p>
<details>
  <summary>點擊查看進階 Shortcode 程式碼</summary>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln"> 1</span><span class="cl">{{/*
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  進階 Details shortcode
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  參數:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  - summary: 摘要文字
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  - open: 是否預設展開（true/false）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  - class: 自定義 CSS 類別
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">*/}}
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">&lt;</span><span class="nt">details</span> <span class="err">{{</span> <span class="na">if</span> <span class="err">.</span><span class="na">Get</span> <span class="err">&#34;</span><span class="na">open</span><span class="err">&#34;</span> <span class="err">}}</span><span class="na">open</span><span class="err">{{</span> <span class="na">end</span> <span class="err">}}</span> <span class="err">{{</span> <span class="na">with</span> <span class="err">.</span><span class="na">Get</span> <span class="err">&#34;</span><span class="na">class</span><span class="err">&#34;</span> <span class="err">}}</span><span class="na">class</span><span class="o">=</span><span class="s">&#34;{{ . }}&#34;</span><span class="err">{{</span> <span class="na">end</span> <span class="err">}}</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">&lt;</span><span class="nt">summary</span><span class="p">&gt;</span>{{ .Get &#34;summary&#34; | default &#34;點擊展開&#34; }}<span class="p">&lt;/</span><span class="nt">summary</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  {{ .Inner | markdownify }}
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">&lt;/</span><span class="nt">details</span><span class="p">&gt;</span></span></span></code></pre></div></details>
<p><strong>使用範例</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">

<details>
  <summary>重要提示</summary>
  
這個區塊預設是展開的

</details></span></span></code></pre></div><h3 id="巢狀使用">巢狀使用</h3>
<p>Shortcode 支援巢狀使用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">

<details>
  <summary>外層標題</summary>
  

這是外層內容



<details>
  <summary>內層標題</summary>
  
這是內層內容

</details>


</details></span></span></code></pre></div><h2 id="遷移指南">遷移指南</h2>
<h3 id="批量替換">批量替換</h3>
<p>如果你已經有很多使用 HTML 標籤的文章，可以使用以下步驟批量替換：</p>
<h4 id="步驟-1備份檔案">步驟 1：備份檔案</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git commit -am <span class="s2">&#34;備份：準備遷移到 shortcode&#34;</span></span></span></code></pre></div><h4 id="步驟-2使用-sed-批量替換macos">步驟 2：使用 sed 批量替換（macOS）</h4>
<details>
  <summary>點擊查看批量替換腳本</summary>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 替換開始標籤</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">find content -name <span class="s2">&#34;*.md&#34;</span> -type f -exec sed -i <span class="s1">&#39;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;s/&lt;details&gt;$/{{&amp;lt; details summary=&#34;點擊查看程式碼&#34; &amp;gt;}}/g&#39;</span> <span class="o">{}</span> +
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 替換帶 summary 的開始標籤</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">find content -name <span class="s2">&#34;*.md&#34;</span> -type f -exec sed -i <span class="s1">&#39;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;s/&lt;details&gt;.*&lt;summary&gt;\(.*\)&lt;\/summary&gt;/{{&amp;lt; details summary=&#34;\1&#34; &amp;gt;}}/g&#39;</span> <span class="o">{}</span> +
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 替換結束標籤</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">find content -name <span class="s2">&#34;*.md&#34;</span> -type f -exec sed -i <span class="s1">&#39;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;s/&lt;\/details&gt;/{{&amp;lt; \/details &amp;gt;}}/g&#39;</span> <span class="o">{}</span> +</span></span></code></pre></div></details>
<h4 id="步驟-3驗證結果">步驟 3：驗證結果</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 檢查是否還有 HTML 標籤</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -r <span class="s2">&#34;&lt;details&gt;&#34;</span> content/
</span></span><span class="line"><span class="ln">3</span><span class="cl">grep -r <span class="s2">&#34;&lt;/details&gt;&#34;</span> content/</span></span></code></pre></div><h4 id="步驟-4測試並提交">步驟 4：測試並提交</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hugo server -D
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 確認無誤後提交</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">git add .
</span></span><span class="line"><span class="ln">4</span><span class="cl">git commit -m <span class="s2">&#34;遷移到 shortcode：移除 HTML 標籤&#34;</span></span></span></code></pre></div><h2 id="常見問題">常見問題</h2>
<h3 id="q1-shortcode-不生效">Q1: Shortcode 不生效？</h3>
<p><strong>可能原因</strong>：</p>
<ol>
<li>檔案路徑錯誤：確認檔案在 <code>layouts/shortcodes/</code> 目錄</li>
<li>檔案名稱錯誤：檔案名稱應該是 <code>details.html</code></li>
<li>Hugo 版本過舊：確認 Hugo 版本 &gt;= 0.55</li>
</ol>
<p><strong>解決方案</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 檢查 Hugo 版本</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hugo version
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 重新啟動 Hugo server</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">hugo server -D --disableFastRender</span></span></code></pre></div><h3 id="q2-markdown-內容沒有被渲染">Q2: Markdown 內容沒有被渲染？</h3>
<p><strong>問題</strong>：shortcode 內的 Markdown 語法沒有被轉換為 HTML</p>
<p><strong>解決方案</strong>：</p>
<p>確認使用了 <code>markdownify</code> 函數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl">{{ .Inner | markdownify }}</span></span></code></pre></div><h3 id="q3-如何處理全域-gitignore-規則">Q3: 如何處理全域 gitignore 規則？</h3>
<p>如果你的專案需要追蹤 <code>.claude/settings.local.json</code>，但被全域 gitignore 排除：</p>
<h4 id="方案-1強制添加">方案 1：強制添加</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git add -f .claude/settings.local.json</span></span></code></pre></div><h4 id="方案-2在專案-gitignore-中覆蓋">方案 2：在專案 <code>.gitignore</code> 中覆蓋</h4>





<pre tabindex="0"><code class="language-gitignore" data-lang="gitignore"># 允許追蹤 .claude/settings.local.json
!.claude/settings.local.json</code></pre><h3 id="q4-css-樣式沒有生效">Q4: CSS 樣式沒有生效？</h3>
<p><strong>檢查清單</strong>：</p>
<ol>
<li>確認 CSS 是否正確載入到 <code>custom_head.html</code></li>
<li>確認瀏覽器快取是否清除（Ctrl+Shift+R 強制重新整理）</li>
<li>確認 CSS 選擇器是否正確</li>
<li>確認是否有其他 CSS 覆蓋了樣式</li>
</ol>
<h2 id="效能考量">效能考量</h2>
<h3 id="shortcode-vs-html-標籤">Shortcode vs HTML 標籤</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>HTML 標籤</th>
          <th>Shortcode</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建置時間</td>
          <td>快</td>
          <td>稍慢（需處理）</td>
      </tr>
      <tr>
          <td>執行時效能</td>
          <td>相同</td>
          <td>相同</td>
      </tr>
      <tr>
          <td>快取效果</td>
          <td>相同</td>
          <td>相同</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>高</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Hugo 部落格支援 Mermaid 流程圖完整實現指南</title><link>https://tarrragon.github.io/blog/posts/hugo-%E9%83%A8%E8%90%BD%E6%A0%BC%E6%94%AF%E6%8F%B4-mermaid-%E6%B5%81%E7%A8%8B%E5%9C%96%E5%AE%8C%E6%95%B4%E5%AF%A6%E7%8F%BE%E6%8C%87%E5%8D%97/</link><pubDate>Wed, 08 Oct 2025 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/hugo-%E9%83%A8%E8%90%BD%E6%A0%BC%E6%94%AF%E6%8F%B4-mermaid-%E6%B5%81%E7%A8%8B%E5%9C%96%E5%AE%8C%E6%95%B4%E5%AF%A6%E7%8F%BE%E6%8C%87%E5%8D%97/</guid><description>&lt;h2 id="概述">概述&lt;/h2>
&lt;p>本文詳細說明如何在 Hugo 部落格中實現 Mermaid 流程圖支援，包含：&lt;/p>
&lt;ul>
&lt;li>Mermaid.js 整合與初始化&lt;/li>
&lt;li>Markdown 語法轉換處理&lt;/li>
&lt;li>自定義樣式設計&lt;/li>
&lt;li>響應式圖表適配&lt;/li>
&lt;li>多種圖表類型支援&lt;/li>
&lt;/ul>
&lt;h2 id="1-問題分析">1. 問題分析&lt;/h2>
&lt;h3 id="11-hugo-markdown-渲染問題">1.1 Hugo Markdown 渲染問題&lt;/h3>
&lt;p>Hugo 的 Markdown 渲染器會將 Mermaid 程式碼區塊包裝在 &lt;code>&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;&lt;/code> 標籤中：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">pre&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;&lt;/span>&lt;span class="nt">code&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;language-mermaid&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">graph TD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> A[開始] --&amp;gt; B{判斷條件}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> B --&amp;gt;|是| C[執行動作]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> B --&amp;gt;|否| D[結束]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">code&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">pre&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但 Mermaid.js 需要的是 &lt;code>&amp;lt;div class=&amp;quot;mermaid&amp;quot;&amp;gt;&lt;/code> 標籤：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;mermaid&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">graph TD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> A[開始] --&amp;gt; B{判斷條件}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> B --&amp;gt;|是| C[執行動作]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> B --&amp;gt;|否| D[結束]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="12-解決方案">1.2 解決方案&lt;/h3>
&lt;p>使用 JavaScript 動態轉換 Markdown 渲染的程式碼區塊為 Mermaid 所需的格式。&lt;/p>
&lt;h2 id="2-實現步驟">2. 實現步驟&lt;/h2>
&lt;h3 id="21-引入-mermaidjs">2.1 引入 Mermaid.js&lt;/h3>
&lt;p>在 &lt;code>layouts/partials/custom_head.html&lt;/code> 中添加 Mermaid.js：&lt;/p>


&lt;details>
 &lt;summary>點擊查看引入程式碼&lt;/summary>
 

```html
&lt;!-- Mermaid.js 支援 -->
&lt;script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js">&lt;/script>
```


&lt;/details>
&lt;h3 id="22-語法轉換腳本">2.2 語法轉換腳本&lt;/h3>
&lt;p>添加 JavaScript 來轉換 Markdown 渲染的程式碼區塊：&lt;/p>


&lt;details>
 &lt;summary>點擊查看語法轉換 JavaScript 程式碼&lt;/summary>
 

```html
&lt;!-- Mermaid 初始化與樣式 -->
&lt;script>
 document.addEventListener('DOMContentLoaded', function() {
 // 轉換 mermaid 程式碼區塊
 const mermaidCodeBlocks = document.querySelectorAll('pre code.language-mermaid');
 mermaidCodeBlocks.forEach(function(block) {
 const pre = block.parentElement;
 const div = document.createElement('div');
 div.className = 'mermaid';
 div.textContent = block.textContent;
 pre.parentNode.replaceChild(div, pre);
 });
 
 // 初始化 Mermaid
 mermaid.initialize({
 startOnLoad: true,
 theme: 'default',
 securityLevel: 'loose',
 fontFamily: 'Arial, sans-serif',
 themeVariables: {
 primaryColor: '#2d3748',
 primaryTextColor: '#2d3748',
 primaryBorderColor: '#4a5568',
 lineColor: '#4a5568',
 secondaryColor: '#e2e8f0',
 tertiaryColor: '#f7fafc'
 }
 });
 });
&lt;/script>
```


&lt;/details>
&lt;h3 id="23-自定義樣式">2.3 自定義樣式&lt;/h3>
&lt;p>添加 Mermaid 圖表的 CSS 樣式：&lt;/p>


&lt;details>
 &lt;summary>點擊查看 CSS 樣式程式碼&lt;/summary>
 

```html
&lt;style>
 /* Mermaid 圖表樣式 */
 .mermaid {
 text-align: center;
 margin: 20px 0;
 }
 
 .mermaid svg {
 max-width: 100%;
 height: auto;
 }
 
 /* 響應式設計 */
 @media (max-width: 768px) {
 .mermaid {
 font-size: 12px;
 }
 }
&lt;/style>
```


&lt;/details>
&lt;h2 id="3-完整實現程式碼">3. 完整實現程式碼&lt;/h2>
&lt;h3 id="31-custom_headhtml-完整程式碼">3.1 custom_head.html 完整程式碼&lt;/h3>


&lt;details>
 &lt;summary>點擊查看完整實現程式碼&lt;/summary>
 

```html
&lt;!-- Mermaid.js 支援 -->
&lt;script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js">&lt;/script>

&lt;!-- Mermaid 初始化與樣式 -->
&lt;script>
 document.addEventListener('DOMContentLoaded', function() {
 // 轉換 mermaid 程式碼區塊
 const mermaidCodeBlocks = document.querySelectorAll('pre code.language-mermaid');
 mermaidCodeBlocks.forEach(function(block) {
 const pre = block.parentElement;
 const div = document.createElement('div');
 div.className = 'mermaid';
 div.textContent = block.textContent;
 pre.parentNode.replaceChild(div, pre);
 });
 
 // 初始化 Mermaid
 mermaid.initialize({
 startOnLoad: true,
 theme: 'default',
 securityLevel: 'loose',
 fontFamily: 'Arial, sans-serif',
 themeVariables: {
 primaryColor: '#2d3748',
 primaryTextColor: '#2d3748',
 primaryBorderColor: '#4a5568',
 lineColor: '#4a5568',
 secondaryColor: '#e2e8f0',
 tertiaryColor: '#f7fafc'
 }
 });
 });
&lt;/script>

&lt;style>
 /* Mermaid 圖表樣式 */
 .mermaid {
 text-align: center;
 margin: 20px 0;
 }
 
 .mermaid svg {
 max-width: 100%;
 height: auto;
 }
 
 /* 響應式設計 */
 @media (max-width: 768px) {
 .mermaid {
 font-size: 12px;
 }
 }
&lt;/style>
```


&lt;/details>
&lt;h2 id="4-使用方式">4. 使用方式&lt;/h2>
&lt;h3 id="41-基本語法">4.1 基本語法&lt;/h3>
&lt;p>在 Markdown 文件中使用 Mermaid 語法：&lt;/p></description><content:encoded><![CDATA[<h2 id="概述">概述</h2>
<p>本文詳細說明如何在 Hugo 部落格中實現 Mermaid 流程圖支援，包含：</p>
<ul>
<li>Mermaid.js 整合與初始化</li>
<li>Markdown 語法轉換處理</li>
<li>自定義樣式設計</li>
<li>響應式圖表適配</li>
<li>多種圖表類型支援</li>
</ul>
<h2 id="1-問題分析">1. 問題分析</h2>
<h3 id="11-hugo-markdown-渲染問題">1.1 Hugo Markdown 渲染問題</h3>
<p>Hugo 的 Markdown 渲染器會將 Mermaid 程式碼區塊包裝在 <code>&lt;pre&gt;&lt;code&gt;</code> 標籤中：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">pre</span><span class="p">&gt;&lt;</span><span class="nt">code</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;language-mermaid&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">graph TD
</span></span><span class="line"><span class="ln">3</span><span class="cl">    A[開始] --&gt; B{判斷條件}
</span></span><span class="line"><span class="ln">4</span><span class="cl">    B --&gt;|是| C[執行動作]
</span></span><span class="line"><span class="ln">5</span><span class="cl">    B --&gt;|否| D[結束]
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">&lt;/</span><span class="nt">code</span><span class="p">&gt;&lt;/</span><span class="nt">pre</span><span class="p">&gt;</span></span></span></code></pre></div><p>但 Mermaid.js 需要的是 <code>&lt;div class=&quot;mermaid&quot;&gt;</code> 標籤：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;mermaid&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">graph TD
</span></span><span class="line"><span class="ln">3</span><span class="cl">    A[開始] --&gt; B{判斷條件}
</span></span><span class="line"><span class="ln">4</span><span class="cl">    B --&gt;|是| C[執行動作]
</span></span><span class="line"><span class="ln">5</span><span class="cl">    B --&gt;|否| D[結束]
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><h3 id="12-解決方案">1.2 解決方案</h3>
<p>使用 JavaScript 動態轉換 Markdown 渲染的程式碼區塊為 Mermaid 所需的格式。</p>
<h2 id="2-實現步驟">2. 實現步驟</h2>
<h3 id="21-引入-mermaidjs">2.1 引入 Mermaid.js</h3>
<p>在 <code>layouts/partials/custom_head.html</code> 中添加 Mermaid.js：</p>


<details>
  <summary>點擊查看引入程式碼</summary>
  

```html
<!-- Mermaid.js 支援 -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
```


</details>
<h3 id="22-語法轉換腳本">2.2 語法轉換腳本</h3>
<p>添加 JavaScript 來轉換 Markdown 渲染的程式碼區塊：</p>


<details>
  <summary>點擊查看語法轉換 JavaScript 程式碼</summary>
  

```html
<!-- Mermaid 初始化與樣式 -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // 轉換 mermaid 程式碼區塊
    const mermaidCodeBlocks = document.querySelectorAll('pre code.language-mermaid');
    mermaidCodeBlocks.forEach(function(block) {
      const pre = block.parentElement;
      const div = document.createElement('div');
      div.className = 'mermaid';
      div.textContent = block.textContent;
      pre.parentNode.replaceChild(div, pre);
    });
    
    // 初始化 Mermaid
    mermaid.initialize({
      startOnLoad: true,
      theme: 'default',
      securityLevel: 'loose',
      fontFamily: 'Arial, sans-serif',
      themeVariables: {
        primaryColor: '#2d3748',
        primaryTextColor: '#2d3748',
        primaryBorderColor: '#4a5568',
        lineColor: '#4a5568',
        secondaryColor: '#e2e8f0',
        tertiaryColor: '#f7fafc'
      }
    });
  });
</script>
```


</details>
<h3 id="23-自定義樣式">2.3 自定義樣式</h3>
<p>添加 Mermaid 圖表的 CSS 樣式：</p>


<details>
  <summary>點擊查看 CSS 樣式程式碼</summary>
  

```html
<style>
  /* Mermaid 圖表樣式 */
  .mermaid {
    text-align: center;
    margin: 20px 0;
  }
  
  .mermaid svg {
    max-width: 100%;
    height: auto;
  }
  
  /* 響應式設計 */
  @media (max-width: 768px) {
    .mermaid {
      font-size: 12px;
    }
  }
</style>
```


</details>
<h2 id="3-完整實現程式碼">3. 完整實現程式碼</h2>
<h3 id="31-custom_headhtml-完整程式碼">3.1 custom_head.html 完整程式碼</h3>


<details>
  <summary>點擊查看完整實現程式碼</summary>
  

```html
<!-- Mermaid.js 支援 -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>

<!-- Mermaid 初始化與樣式 -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // 轉換 mermaid 程式碼區塊
    const mermaidCodeBlocks = document.querySelectorAll('pre code.language-mermaid');
    mermaidCodeBlocks.forEach(function(block) {
      const pre = block.parentElement;
      const div = document.createElement('div');
      div.className = 'mermaid';
      div.textContent = block.textContent;
      pre.parentNode.replaceChild(div, pre);
    });
    
    // 初始化 Mermaid
    mermaid.initialize({
      startOnLoad: true,
      theme: 'default',
      securityLevel: 'loose',
      fontFamily: 'Arial, sans-serif',
      themeVariables: {
        primaryColor: '#2d3748',
        primaryTextColor: '#2d3748',
        primaryBorderColor: '#4a5568',
        lineColor: '#4a5568',
        secondaryColor: '#e2e8f0',
        tertiaryColor: '#f7fafc'
      }
    });
  });
</script>

<style>
  /* Mermaid 圖表樣式 */
  .mermaid {
    text-align: center;
    margin: 20px 0;
  }
  
  .mermaid svg {
    max-width: 100%;
    height: auto;
  }
  
  /* 響應式設計 */
  @media (max-width: 768px) {
    .mermaid {
      font-size: 12px;
    }
  }
</style>
```


</details>
<h2 id="4-使用方式">4. 使用方式</h2>
<h3 id="41-基本語法">4.1 基本語法</h3>
<p>在 Markdown 文件中使用 Mermaid 語法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="s">```mermaid
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s"></span>graph TD
</span></span><span class="line"><span class="ln">3</span><span class="cl">    A[開始] --&gt; B{判斷條件}
</span></span><span class="line"><span class="ln">4</span><span class="cl">    B --&gt;|是| C[執行動作]
</span></span><span class="line"><span class="ln">5</span><span class="cl">    B --&gt;|否| D[結束]
</span></span><span class="line"><span class="ln">6</span><span class="cl">    C --&gt; D
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">```</span></span></span></code></pre></div><h3 id="42-支援的圖表類型">4.2 支援的圖表類型</h3>
<h4 id="421-流程圖-flowchart">4.2.1 流程圖 (Flowchart)</h4>





<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">graph TD
    A[開始] --&gt; B{判斷條件}
    B --&gt;|是| C[執行動作]
    B --&gt;|否| D[結束]
    C --&gt; D</code></pre><h4 id="422-時序圖-sequence-diagram">4.2.2 時序圖 (Sequence Diagram)</h4>





<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">sequenceDiagram
    participant A as 用戶
    participant B as 系統
    participant C as 資料庫
    
    A-&gt;&gt;B: 發送請求
    B-&gt;&gt;C: 查詢資料
    C--&gt;&gt;B: 返回結果
    B--&gt;&gt;A: 顯示結果</code></pre><h4 id="423-甘特圖-gantt-chart">4.2.3 甘特圖 (Gantt Chart)</h4>





<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">gantt
    title 專案時程規劃
    dateFormat  YYYY-MM-DD
    section 第一階段
    需求分析           :a1, 2024-01-01, 30d
    系統設計           :a2, after a1, 20d
    section 第二階段
    程式開發           :a3, after a2, 40d
    測試驗證           :a4, after a3, 15d</code></pre><h4 id="424-類別圖-class-diagram">4.2.4 類別圖 (Class Diagram)</h4>





<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">classDiagram
    class User {
        +String name
        +String email
        +login()
        +logout()
    }
    
    class Admin {
        +String role
        +manageUsers()
    }
    
    User &lt;|-- Admin</code></pre><h4 id="425-狀態圖-state-diagram">4.2.5 狀態圖 (State Diagram)</h4>





<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">stateDiagram-v2
    [*] --&gt; 待機
    待機 --&gt; 執行中 : 開始任務
    執行中 --&gt; 完成 : 任務完成
    執行中 --&gt; 錯誤 : 發生錯誤
    錯誤 --&gt; 待機 : 重新開始
    完成 --&gt; [*]</code></pre><h2 id="5-自定義配置">5. 自定義配置</h2>
<h3 id="51-主題設定">5.1 主題設定</h3>
<p>Mermaid 支援多種主題，可以在初始化時設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">mermaid</span><span class="p">.</span><span class="nx">initialize</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;default&#39;</span><span class="p">,</span> <span class="c1">// 可選: default, dark, forest, neutral
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// ... 其他設定
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h3 id="52-自定義顏色">5.2 自定義顏色</h3>
<p>通過 <code>themeVariables</code> 自定義顏色：</p>


<details>
  <summary>點擊查看自定義顏色程式碼</summary>
  

```javascript
mermaid.initialize({
  themeVariables: {
    primaryColor: '#2d3748',        // 主要顏色
    primaryTextColor: '#2d3748',    // 主要文字顏色
    primaryBorderColor: '#4a5568',  // 主要邊框顏色
    lineColor: '#4a5568',           // 線條顏色
    secondaryColor: '#e2e8f0',      // 次要顏色
    tertiaryColor: '#f7fafc'        // 第三級顏色
  }
});
```


</details>
<h3 id="53-字體設定">5.3 字體設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">mermaid</span><span class="p">.</span><span class="nx">initialize</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">fontFamily</span><span class="o">:</span> <span class="s1">&#39;Arial, sans-serif&#39;</span><span class="p">,</span> <span class="c1">// 字體家族
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// ... 其他設定
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h2 id="6-響應式設計">6. 響應式設計</h2>
<h3 id="61-桌面版樣式">6.1 桌面版樣式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">mermaid</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">text-align</span><span class="p">:</span> <span class="kc">center</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">.</span><span class="nc">mermaid</span> <span class="nt">svg</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">max-width</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="62-手機版適配">6.2 手機版適配</h3>


<details>
  <summary>點擊查看手機版適配 CSS</summary>
  

```css
@media (max-width: 768px) {
  .mermaid {
    font-size: 12px;
    margin: 15px 0;
  }
}
```


</details>
<h2 id="7-進階功能">7. 進階功能</h2>
<h3 id="71-互動式圖表">7.1 互動式圖表</h3>
<p>Mermaid 支援點擊事件和互動功能：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">mermaid</span><span class="p">.</span><span class="nx">initialize</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">startOnLoad</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">securityLevel</span><span class="o">:</span> <span class="s1">&#39;loose&#39;</span><span class="p">,</span> <span class="c1">// 允許互動功能
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="c1">// ... 其他設定
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h3 id="72-自定義樣式">7.2 自定義樣式</h3>
<p>可以通過 CSS 進一步自定義圖表外觀：</p>


<details>
  <summary>點擊查看進階自定義樣式 CSS</summary>
  

```css
.mermaid .node rect {
  fill: #f9f9f9;
  stroke: #333;
  stroke-width: 2px;
}

.mermaid .edgePath .path {
  stroke: #333;
  stroke-width: 2px;
}

.mermaid .edgeLabel {
  background-color: #e8e8e8;
}
```


</details>
<h2 id="8-常見問題與解決方案">8. 常見問題與解決方案</h2>
<h3 id="81-圖表不顯示">8.1 圖表不顯示</h3>
<p><strong>問題</strong>：Mermaid 圖表沒有渲染出來</p>
<p><strong>解決方案</strong>：</p>
<ol>
<li>檢查 JavaScript 是否正確載入</li>
<li>確認 Markdown 語法是否正確</li>
<li>檢查瀏覽器控制台是否有錯誤訊息</li>
</ol>
<h3 id="82-樣式問題">8.2 樣式問題</h3>
<p><strong>問題</strong>：圖表樣式不符合預期</p>
<p><strong>解決方案</strong>：</p>
<ol>
<li>檢查 CSS 樣式是否正確載入</li>
<li>確認 Mermaid 初始化設定</li>
<li>檢查是否有其他 CSS 衝突</li>
</ol>
<h3 id="83-響應式問題">8.3 響應式問題</h3>
<p><strong>問題</strong>：在手機版圖表顯示異常</p>
<p><strong>解決方案</strong>：</p>
<ol>
<li>檢查響應式 CSS 設定</li>
<li>調整字體大小和邊距</li>
<li>測試不同螢幕尺寸</li>
</ol>
<h2 id="9-效能優化">9. 效能優化</h2>
<h3 id="91-延遲載入">9.1 延遲載入</h3>
<p>對於包含大量圖表的頁面，可以考慮延遲載入：</p>


<details>
  <summary>點擊查看延遲載入程式碼</summary>
  

```javascript
// 只在圖表進入視窗時才初始化
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 初始化 Mermaid
      mermaid.init(undefined, entry.target);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('.mermaid').forEach(el => {
  observer.observe(el);
});
```


</details>
<h3 id="92-快取優化">9.2 快取優化</h3>
<p>使用 CDN 快取 Mermaid.js：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js&#34;</span> 
</span></span><span class="line"><span class="ln">2</span><span class="cl">        <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;sha384-...&#34;</span> 
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="na">crossorigin</span><span class="o">=</span><span class="s">&#34;anonymous&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div><h2 id="10-總結">10. 總結</h2>
<p>通過這個實現方案：</p>
<ol>
<li>支援 Mermaid 語法</li>
<li>處理 Hugo Markdown 渲染的格式問題</li>
<li>響應式設計</li>
<li>支援流程圖、時序圖、甘特圖等</li>
<li>可以根據網站主題調整外觀</li>
</ol>
]]></content:encoded></item><item><title>在部落格中設置文章資訊與tag</title><link>https://tarrragon.github.io/blog/posts/%E5%9C%A8%E9%83%A8%E8%90%BD%E6%A0%BC%E4%B8%AD%E8%A8%AD%E7%BD%AE%E6%96%87%E7%AB%A0%E8%B3%87%E8%A8%8A%E8%88%87tag/</link><pubDate>Tue, 09 Sep 2025 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E5%9C%A8%E9%83%A8%E8%90%BD%E6%A0%BC%E4%B8%AD%E8%A8%AD%E7%BD%AE%E6%96%87%E7%AB%A0%E8%B3%87%E8%A8%8A%E8%88%87tag/</guid><description>&lt;p>使用YAML格式撰寫以下資訊放在文章開頭，title部分取代＃大標題&lt;/p></description><content:encoded><![CDATA[<p>使用YAML格式撰寫以下資訊放在文章開頭，title部分取代＃大標題</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-YAML" data-lang="YAML"><span class="line"><span class="ln">1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Flutter 可以使用的 ToggleButton 樣式&#34;</span><span class="w"> </span><span class="c">#設訂文章標題後不需再使用#大標題</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2025-09-09</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="nt">draft</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span><span class="c">#是否設為草稿</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;Markdown&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;blog心得&#34;</span><span class="p">,]</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="nn">---</span></span></span></code></pre></div>]]></content:encoded></item></channel></rss>