<?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>文章列表 on Tarragon</title><link>https://tarrragon.github.io/blog/posts/</link><description>Recent content in 文章列表 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/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>驗證導向的 CLI 工具文章：官方 docs 查核放過的落差類型</title><link>https://tarrragon.github.io/blog/posts/%E9%A9%97%E8%AD%89%E5%B0%8E%E5%90%91%E7%9A%84-cli-%E5%B7%A5%E5%85%B7%E6%96%87%E7%AB%A0%E5%AE%98%E6%96%B9-docs-%E6%9F%A5%E6%A0%B8%E6%94%BE%E9%81%8E%E7%9A%84%E8%90%BD%E5%B7%AE%E9%A1%9E%E5%9E%8B/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E9%A9%97%E8%AD%89%E5%B0%8E%E5%90%91%E7%9A%84-cli-%E5%B7%A5%E5%85%B7%E6%96%87%E7%AB%A0%E5%AE%98%E6%96%B9-docs-%E6%9F%A5%E6%A0%B8%E6%94%BE%E9%81%8E%E7%9A%84%E8%90%BD%E5%B7%AE%E9%A1%9E%E5%9E%8B/</guid><description>&lt;p>本文記錄驗證導向生產流程背後的 evidence — 為什麼官方文件查核不夠、實機驗證抓到了什麼。操作步驟維護在 &lt;code>.claude/skills/verification-driven-cli/&lt;/code>。&lt;/p>
&lt;h2 id="官方文件查核放過的五類落差">官方文件查核放過的五類落差&lt;/h2>
&lt;p>&lt;code>content/cli/&lt;/code> 五類終端機工具文章（監控 / 圖表 / 多工器 / 檔案管理 / SQL 客戶端）在實機驗證時抓到、純靠 docs 查核會放過的落差：&lt;/p>
&lt;h3 id="1-旗標改名">1. 旗標改名&lt;/h3>
&lt;p>&lt;code>zellij web&lt;/code> 文件寫有 &lt;code>--bind&lt;/code>，實際 0.43.1 是分開的 &lt;code>--ip&lt;/code> 與 &lt;code>--port&lt;/code>。讀者照文件下指令會得到 unknown flag error、但不知道正確旗標是什麼。&lt;/p>
&lt;h3 id="2-設定鍵-migrate">2. 設定鍵 migrate&lt;/h3>
&lt;p>&lt;code>lazygit&lt;/code> 的 pager 設定文件寫 &lt;code>git.paging.pager&lt;/code>，新版 0.62.2 改成 &lt;code>git.pagers&lt;/code>（list）。舊鍵啟動時會被自動 migrate、改寫設定檔 — 讀者照舊文件設定後發現設定檔被工具自己改掉。&lt;/p>
&lt;h3 id="3-隱含-schema-prefix">3. 隱含 schema prefix&lt;/h3>
&lt;p>&lt;code>dblab&lt;/code> 的查詢編輯器要 schema 限定（&lt;code>SELECT * FROM public.products&lt;/code>），裸 &lt;code>products&lt;/code> 會報 relation 不存在。原因是編輯器連線的 search_path 不含 public — 文件沒提。&lt;/p>
&lt;h3 id="4-平台特定-segfault">4. 平台特定 segfault&lt;/h3>
&lt;p>&lt;code>nvtop&lt;/code> 在 Apple Silicon mac 裝得起來，但 snapshot 模式直接 segfault。GPU 後端不穩。裝成功不代表能用 — 文件只說「支援 macOS」。&lt;/p>
&lt;h3 id="5-driver-差異">5. Driver 差異&lt;/h3>
&lt;p>同一個 Postgres，&lt;code>lazysql&lt;/code>（Go pq driver）連無 SSL 的 DB 要 &lt;code>?sslmode=disable&lt;/code>，&lt;code>pgcli&lt;/code> / &lt;code>harlequin&lt;/code>（Python psycopg）不用。同樣的連線字串在不同工具會有不同行為、文件各自不提對方。&lt;/p>
&lt;h2 id="共通模式">共通模式&lt;/h2>
&lt;p>這五類落差有個共通點：讀者照文件走會撞牆、卻在文件裡找不到答案。實機跑一次就現形，而且現形的正是文章最該寫的內容 — gotcha 段落省下讀者各自撞一次的時間。&lt;/p>
&lt;p>官方文件的 fact-check 只能驗證「文件說的是否正確」，驗不了「文件沒說的是否存在」。實機驗證補的是後者。&lt;/p>
&lt;h2 id="相關連結">相關連結&lt;/h2>
&lt;ul>
&lt;li>Verification-driven CLI skill（&lt;code>.claude/skills/verification-driven-cli/&lt;/code>）— 操作步驟&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a> — 用此流程生產的工具文章&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能&lt;/a> — 本文精簡的依據&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本文記錄驗證導向生產流程背後的 evidence — 為什麼官方文件查核不夠、實機驗證抓到了什麼。操作步驟維護在 <code>.claude/skills/verification-driven-cli/</code>。</p>
<h2 id="官方文件查核放過的五類落差">官方文件查核放過的五類落差</h2>
<p><code>content/cli/</code> 五類終端機工具文章（監控 / 圖表 / 多工器 / 檔案管理 / SQL 客戶端）在實機驗證時抓到、純靠 docs 查核會放過的落差：</p>
<h3 id="1-旗標改名">1. 旗標改名</h3>
<p><code>zellij web</code> 文件寫有 <code>--bind</code>，實際 0.43.1 是分開的 <code>--ip</code> 與 <code>--port</code>。讀者照文件下指令會得到 unknown flag error、但不知道正確旗標是什麼。</p>
<h3 id="2-設定鍵-migrate">2. 設定鍵 migrate</h3>
<p><code>lazygit</code> 的 pager 設定文件寫 <code>git.paging.pager</code>，新版 0.62.2 改成 <code>git.pagers</code>（list）。舊鍵啟動時會被自動 migrate、改寫設定檔 — 讀者照舊文件設定後發現設定檔被工具自己改掉。</p>
<h3 id="3-隱含-schema-prefix">3. 隱含 schema prefix</h3>
<p><code>dblab</code> 的查詢編輯器要 schema 限定（<code>SELECT * FROM public.products</code>），裸 <code>products</code> 會報 relation 不存在。原因是編輯器連線的 search_path 不含 public — 文件沒提。</p>
<h3 id="4-平台特定-segfault">4. 平台特定 segfault</h3>
<p><code>nvtop</code> 在 Apple Silicon mac 裝得起來，但 snapshot 模式直接 segfault。GPU 後端不穩。裝成功不代表能用 — 文件只說「支援 macOS」。</p>
<h3 id="5-driver-差異">5. Driver 差異</h3>
<p>同一個 Postgres，<code>lazysql</code>（Go pq driver）連無 SSL 的 DB 要 <code>?sslmode=disable</code>，<code>pgcli</code> / <code>harlequin</code>（Python psycopg）不用。同樣的連線字串在不同工具會有不同行為、文件各自不提對方。</p>
<h2 id="共通模式">共通模式</h2>
<p>這五類落差有個共通點：讀者照文件走會撞牆、卻在文件裡找不到答案。實機跑一次就現形，而且現形的正是文章最該寫的內容 — gotcha 段落省下讀者各自撞一次的時間。</p>
<p>官方文件的 fact-check 只能驗證「文件說的是否正確」，驗不了「文件沒說的是否存在」。實機驗證補的是後者。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Verification-driven CLI skill（<code>.claude/skills/verification-driven-cli/</code>）— 操作步驟</li>
<li><a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a> — 用此流程生產的工具文章</li>
<li><a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a> — 本文精簡的依據</li>
</ul>
]]></content:encoded></item><item><title>Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%</title><link>https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/</guid><description>&lt;p>本文記錄 migration-playbook-methodology 這套寫作方法論前三輪 batch dogfood（實際寫文章驗證方法論）的演化過程（skill 已累積到六輪、本文記錄前三輪）。操作步驟維護在 &lt;code>.claude/skills/migration-playbook-methodology/&lt;/code>，本文只保留 retrospective — 每一輪跑出來學到什麼、哪些假設被推翻。&lt;/p>
&lt;h2 id="為什麼-migration-playbook-需要自己的方法論">為什麼 migration playbook 需要自己的方法論&lt;/h2>
&lt;p>Migration playbook 跟 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">single feature deep article&lt;/a> 是不同 content category：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Deep article&lt;/th>
 &lt;th>Migration playbook&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>主題形狀&lt;/td>
 &lt;td>Single feature（pgBouncer / Vault dynamic credential）&lt;/td>
 &lt;td>Cross-vendor process（Splunk → Elastic）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結構&lt;/td>
 &lt;td>6-section（problem → concept → config → failure → capacity → integration）&lt;/td>
 &lt;td>6 種不同 type、各對應不同結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重點章節&lt;/td>
 &lt;td>Step-by-step 配置 + 故障演練&lt;/td>
 &lt;td>視 type 不同：phased flow / parallel streams / hybrid&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫作週期 / 篇&lt;/td>
 &lt;td>1-2 小時&lt;/td>
 &lt;td>2-3 小時（diff dimension audit + 結構選擇 + 寫作）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨篇 cadence 風險&lt;/td>
 &lt;td>中（章節 1 entry 容易 collapse）&lt;/td>
 &lt;td>高（migration 主題本質相似、主題語意 attractor「為什麼遷」明顯）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵差異：deep article 是 single direction implementation、migration playbook 是 bidirectional comparison + process。第一輪寫了 5 篇後發現結構完全不同；嘗試套 deep article 的固定結構都只對 1 種情境適用，於是用 diff dimension audit（寫前評估 source/target 在哪些維度差異最大）選對應的結構模板（Type A-F，依主導差異維度決定）。&lt;/p>
&lt;h2 id="第一輪-batch5-篇type-a-e-浮現--cadence-collapse-35">第一輪 batch（5 篇）：Type A-E 浮現 + cadence collapse 3/5&lt;/h2>
&lt;p>第一輪寫了 5 篇跨 vendor migration playbook，每篇自然對映到一種 type（結構模板）：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security&lt;/a> — Type A phased translation&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> — Type B drop-in&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &amp;#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora&lt;/a> — Type C operational hybrid&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack&lt;/a> — Type D parallel streams&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&amp;#39;migration&amp;#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &amp;#43; 混合架構">Kafka ↔ NATS&lt;/a> — Type E paradigm shift&lt;/li>
&lt;/ul>
&lt;h3 id="cadence-collapse前-3-篇被動寫作全部同質化">Cadence collapse：前 3 篇被動寫作全部同質化&lt;/h3>
&lt;p>Cadence collapse 指批量寫作時、多篇文章的開場句型不自覺重複同一模式。&lt;/p></description><content:encoded><![CDATA[<p>本文記錄 migration-playbook-methodology 這套寫作方法論前三輪 batch dogfood（實際寫文章驗證方法論）的演化過程（skill 已累積到六輪、本文記錄前三輪）。操作步驟維護在 <code>.claude/skills/migration-playbook-methodology/</code>，本文只保留 retrospective — 每一輪跑出來學到什麼、哪些假設被推翻。</p>
<h2 id="為什麼-migration-playbook-需要自己的方法論">為什麼 migration playbook 需要自己的方法論</h2>
<p>Migration playbook 跟 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">single feature deep article</a> 是不同 content category：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主題形狀</td>
          <td>Single feature（pgBouncer / Vault dynamic credential）</td>
          <td>Cross-vendor process（Splunk → Elastic）</td>
      </tr>
      <tr>
          <td>結構</td>
          <td>6-section（problem → concept → config → failure → capacity → integration）</td>
          <td>6 種不同 type、各對應不同結構</td>
      </tr>
      <tr>
          <td>重點章節</td>
          <td>Step-by-step 配置 + 故障演練</td>
          <td>視 type 不同：phased flow / parallel streams / hybrid</td>
      </tr>
      <tr>
          <td>寫作週期 / 篇</td>
          <td>1-2 小時</td>
          <td>2-3 小時（diff dimension audit + 結構選擇 + 寫作）</td>
      </tr>
      <tr>
          <td>跨篇 cadence 風險</td>
          <td>中（章節 1 entry 容易 collapse）</td>
          <td>高（migration 主題本質相似、主題語意 attractor「為什麼遷」明顯）</td>
      </tr>
  </tbody>
</table>
<p>關鍵差異：deep article 是 single direction implementation、migration playbook 是 bidirectional comparison + process。第一輪寫了 5 篇後發現結構完全不同；嘗試套 deep article 的固定結構都只對 1 種情境適用，於是用 diff dimension audit（寫前評估 source/target 在哪些維度差異最大）選對應的結構模板（Type A-F，依主導差異維度決定）。</p>
<h2 id="第一輪-batch5-篇type-a-e-浮現--cadence-collapse-35">第一輪 batch（5 篇）：Type A-E 浮現 + cadence collapse 3/5</h2>
<p>第一輪寫了 5 篇跨 vendor migration playbook，每篇自然對映到一種 type（結構模板）：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> — Type A phased translation</li>
<li><a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> — Type B drop-in</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> — Type C operational hybrid</li>
<li><a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a> — Type D parallel streams</li>
<li><a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> — Type E paradigm shift</li>
</ul>
<h3 id="cadence-collapse前-3-篇被動寫作全部同質化">Cadence collapse：前 3 篇被動寫作全部同質化</h3>
<p>Cadence collapse 指批量寫作時、多篇文章的開場句型不自覺重複同一模式。</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Variant 規劃</th>
          <th>章節 1 entry framing</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 Splunk → Elastic</td>
          <td>被動</td>
          <td>「為什麼遷：cost / multi-vendor / cloud-native」</td>
      </tr>
      <tr>
          <td>2 Redis → DragonflyDB</td>
          <td>被動</td>
          <td>「為什麼遷：cost / single-thread / multi-tenancy」</td>
      </tr>
      <tr>
          <td>3 Postgres → Aurora</td>
          <td>被動</td>
          <td>「為什麼遷：operational cost / HA / DR」</td>
      </tr>
      <tr>
          <td>4 Datadog → Grafana</td>
          <td>主動</td>
          <td>「$50K/month bill 拆解」</td>
      </tr>
      <tr>
          <td>5 Kafka ↔ NATS</td>
          <td>主動</td>
          <td>「『Kafka → NATS migration』字面上不成立」</td>
      </tr>
  </tbody>
</table>
<p>3/5 collapse — 主題語意 attractor「為什麼遷：X / Y / Z driver」在前 3 篇被動寫作下浮現。寫第 4 篇前發現問題、後 2 篇主動換 entry variant。</p>
<p>前 3 篇的 collapse 是 Stage 0 variant 規劃成為硬需求的直接證據。</p>
<h3 id="type-a-e-怎麼浮現">Type A-E 怎麼浮現</h3>
<p>5 篇寫完後比對結構、發現 5 篇結構完全不同，但都可以用「主導差異維度」解釋：schema 差為主 → phased translation、全 Low → drop-in、operational 差為主 → hybrid。Type A-E 從這 5 篇的歸納中浮現，第二輪 dogfood 再加上 Type F（topology re-layout）。</p>
<h2 id="第二輪-batch5-篇漏類驗證--多軸-high-實證">第二輪 batch（5 篇）：漏類驗證 + 多軸 High 實證</h2>
<p>第二輪刻意選漏類場景驗證 self-aware limitation：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL major version upgrade (14 → 17)</a> — 漏類驗證（同 vendor）</li>
<li><a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis cluster re-sharding</a> — 漏類驗證（topology 重劃）→ Type F 浮現</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB</a> — 三維 High multi-axis 驗證</li>
<li><a href="/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/" data-link-title="MySQL → PostgreSQL：從 SQL dialect diff 跑出來的 Type A 6-phase migration" data-link-desc="MySQL → PostgreSQL 是 Type A 高 schema 差 migration 的標準形態 — SQL dialect / collation / case sensitivity / replication 模型差異主導；用 pgloader / AWS DMS / 自管 dual-write 三條 path、5 個 production 踩雷（auto_increment vs SERIAL / charset 跟 collation / case sensitivity / index syntax / triggers）">MySQL → PostgreSQL</a> — Type A 標準形態（263 行）</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a> — Type C 標準形態（349 行）</li>
</ul>
<p>Stage 0 variant 規劃從第二輪開始全面啟用，cadence collapse 從 3/5 降到 0/5。</p>
<h3 id="驗證成立的-4-項預測">驗證成立的 4 項預測</h3>
<ol>
<li><strong>5 type 漏類確認</strong>：major version upgrade + re-sharding 結構跟 5 type 完全不同</li>
<li><strong>多重歸類 + tie-breaking 規則成立</strong>：PostgreSQL → CockroachDB 三維皆 High、按主導維度走 Type E + 高維度獨立段</li>
<li><strong>Type A / Type C 標準形態仍適用</strong>：MySQL → PostgreSQL + MongoDB → Atlas 走標準模板</li>
<li><strong>Stage 0 variant 規劃硬需求</strong>：第二輪 5 篇全主動 variant、collapse 0/5</li>
</ol>
<h3 id="浮現的-3-項新議題">浮現的 3 項新議題</h3>
<ol>
<li><strong>新 audit 維度（data topology）</strong>：re-sharding 揭露 5 維度沒「topology」軸 → 擴到 6 維</li>
<li><strong>「為什麼這篇不套」是漏類文章標準 frame</strong>：major-version-upgrade + cluster-resharding 都用這個 frame 開頭</li>
<li><strong>「高維度獨立段」升級為 multi-axis migration 標準結構元素</strong></li>
</ol>
<h2 id="第三輪-batch5-篇type-f-dogfood--候選軸驗證">第三輪 batch（5 篇）：Type F dogfood + 候選軸驗證</h2>
<p>第三輪驗證 data topology audit dimension 的 self-aware limitation 4 條 tripwire：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/partition-redesign/" data-link-title="PostgreSQL Partition Redesign：當 monthly partition 越跑越慢" data-link-desc="PostgreSQL partition redesign 是 Type F「topology re-layout」第 2 個 dogfood — 從 monthly partition 改 daily / 從 range 改 list / 從單軸改 sub-partition；6 維 audit 皆 Low &#43; topology 軸 High；涵蓋 partition 不平衡偵測、ATTACH/DETACH 線上重劃、5 個 production 踩雷、跟 partition_pruning &#43; autovacuum 整合">PostgreSQL partition redesign</a>（246 行）— Type F dogfood #2</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/" data-link-title="MongoDB Shard Expansion &#43; Multi-DC：Type F「不需要 parallel run」的 multi-region 例外" data-link-desc="MongoDB sharded cluster 加 shard &#43; 跨 DC expansion 是 Type F「topology re-layout」第 3 個 dogfood — 同時改 sharding &#43; replication topology &#43; region distribution；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 第 3 點「Type F 不需要 parallel run」claim 的例外（multi-region rollout 必須 parallel run &#43; 切流量）；涵蓋 chunk migration / replica set add member / cross-DC routing">MongoDB shard + multi-DC expansion</a>（291 行）— Type F dogfood #3 + parallel run 例外實證</li>
<li><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/" data-link-title="Vault → AWS Secrets Manager：「secret」不是「secret」、identity model 才是核心差異" data-link-desc="Vault → AWS Secrets Manager migration 表面是 secret store 替換、實際核心是 identity model 對位（Vault token &#43; policy vs AWS IAM &#43; resource policy）；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 identity axis 候選 — identity 是否獨立 audit 軸；5 個 production 踩雷（IAM principal 對位 / dynamic credential 對等失敗 / lease lifecycle 模型不同 / audit log 結構差 / 計費模型反轉）">Vault → AWS Secrets Manager</a>（272 行）— Identity axis 候選（45% 工作量）</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">DynamoDB consistency model optimization</a>（249 行）— Consistency axis 候選（85% 工作量）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/multi-region-gdpr-rollout/" data-link-title="PostgreSQL Multi-Region GDPR Rollout：政策驅動的 migration 屬本 methodology 嗎" data-link-desc="PostgreSQL 單 region → multi-region 同時滿足 GDPR EU residency 是 *政策驅動* 兼 *topology 變動* 兼 *operational redesign* 的多軸 migration；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 residency axis 候選 — residency 是 driver 還是獨立 audit 軸；涵蓋 logical replication 配 GDPR / 5 個 production 踩雷 / cross-region cost">PostgreSQL multi-region GDPR rollout</a>（238 行）— Residency axis 候選（40% 工作量）</li>
</ul>
<p>第三輪維持 collapse 0/5，但 Type F 分裂出 sub-type（F-cluster vs F-multi-region），框架仍在演化。</p>
<h3 id="累積-evidence">累積 evidence</h3>
<ul>
<li><strong>Type F sub-type 浮現</strong>：F-cluster（單 cluster 內、不需 parallel run）vs F-multi-region（跨 region、需 parallel run）</li>
<li><strong>3 軸候選確認可獨立</strong>：identity / consistency / residency 各帶 30-85% 獨立工作量；累積到 3-5 case / 軸後考慮升 audit 7-9 維</li>
<li><strong>Residency 是 cross-cutting constraint</strong>：不只是 driver、反向約束 topology + operational + application</li>
</ul>
<h2 id="三輪對照方法論的演化軌跡">三輪對照：方法論的演化軌跡</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>第一輪（5 篇）</th>
          <th>第二輪（5 篇）</th>
          <th>第三輪（5 篇）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type 集合</td>
          <td>A-E（5 type）</td>
          <td>A-F（+Type F）</td>
          <td>A-F + sub-type</td>
      </tr>
      <tr>
          <td>Audit 維度</td>
          <td>5 維</td>
          <td>6 維（+topology）</td>
          <td>6 維 + 3 候選軸</td>
      </tr>
      <tr>
          <td>Cadence collapse</td>
          <td>3/5 (60%)</td>
          <td>0/5 (0%)</td>
          <td>0/5 (0%)</td>
      </tr>
      <tr>
          <td>Variant 規劃</td>
          <td>被動 → 主動</td>
          <td>全主動</td>
          <td>全主動</td>
      </tr>
      <tr>
          <td>總行數</td>
          <td>~1,200</td>
          <td>1,389</td>
          <td>1,292</td>
      </tr>
      <tr>
          <td>單篇行數</td>
          <td>200-300</td>
          <td>263-349</td>
          <td>238-288</td>
      </tr>
  </tbody>
</table>
<p>關鍵轉折是第一輪到第二輪：後續批次未再觀察到 collapse。</p>
<h2 id="self-aware-limitation">Self-aware limitation</h2>
<p>本 methodology 從 15 篇 migration playbook dogfood 抽出 6 type；已知 limitation：</p>
<ul>
<li><strong>6 type 非窮盡</strong>：major version upgrade / merger consolidation 等情境不在 6 type 內</li>
<li><strong>多重歸類常見</strong>：實際 source/target 配對很少完美對映單一 type</li>
<li><strong>「主導維度」需 judgment</strong>：優先序是 audience-dependent heuristic、不是 universal 規則</li>
<li><strong>Collapse 歸因有共變因素</strong>：第二輪以後 collapse 消失，但同時作者已有第一輪經驗、且知道自己在測量 cadence（Hawthorne effect）。Stage 0 variant 規劃是介入手段之一，無法完全隔離歸因。N=5 的二項信賴區間也無法排除偶然</li>
<li><strong>候選軸未 commit</strong>：identity / consistency / residency 各 N=1、累積到 3-5 case / 軸後才考慮升維</li>
</ul>
<p>本 methodology 接受 evolution、不假裝穩定。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Migration Playbook Methodology skill（<code>.claude/skills/migration-playbook-methodology/</code>）— 操作步驟（6 維 audit、6 type、Stage 0 variant、4-reviewer）</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor deep article methodology</a> — sibling、處理 single feature implementation</li>
<li><a href="/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">Case-first Agent Team Review Workflow</a> — 教學模組級批次寫作流程</li>
<li><a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a> — 本文精簡的依據</li>
</ul>
]]></content:encoded></item><item><title>Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證</title><link>https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/</guid><description>&lt;p>Vendor overview 寫完後、往下寫單一功能深度文章時，選題與結構需要不同的方法論。操作步驟維護在 &lt;code>.claude/skills/vendor-deep-article/&lt;/code>，本文記錄這套方法論從兩輪 batch 中演化出來的過程，重點是 cadence collapse（批量寫作時開場句型同質化重複）怎麼被寫前的 variant 規劃（每篇預先指定不同開場 framing）解決。&lt;/p>
&lt;h2 id="背景">背景&lt;/h2>
&lt;p>本 blog 的 backend 教學模組已完成多個 vendor overview。overview 層飽和後、自然的下一步是 overview 頁尾「預計實作話題」backlog 的深度文章。&lt;/p>
&lt;p>寫了 deep article + migration playbook 後、確認 deep article 跟 overview 是不同產品、需要自己的方法論。差異見 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook 方法論演化紀錄&lt;/a>。&lt;/p>
&lt;h2 id="第一輪-batch5-篇跨-vendor5-種-entry-framing">第一輪 batch（5 篇）：跨 vendor、5 種 entry framing&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>篇&lt;/th>
 &lt;th>Variant&lt;/th>
 &lt;th>章節 1 entry framing&lt;/th>
 &lt;th>行數&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &amp;#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置&lt;/a>&lt;/td>
 &lt;td>A 標準&lt;/td>
 &lt;td>標準「問題情境」&lt;/td>
 &lt;td>263&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault dynamic credential&lt;/a>&lt;/td>
 &lt;td>A 標準&lt;/td>
 &lt;td>標準「問題情境」&lt;/td>
 &lt;td>222&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/" data-link-title="Kubernetes Graceful Shutdown：termination 序列跟你以為的不一樣" data-link-desc="K8s pod termination 五步序列、preStop / SIGTERM / terminationGracePeriodSeconds 的真實時序、5 個 production 踩雷（500 期間 502、connection drain race、init container 重啟、StatefulSet 串行終止、Job 不 graceful）、跟 service mesh / readiness probe 整合">K8s graceful shutdown&lt;/a>&lt;/td>
 &lt;td>B 痛點&lt;/td>
 &lt;td>痛點宣告「沒做對、每次 deploy 都吃 502」&lt;/td>
 &lt;td>213&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &amp;#43; case management 整合">Splunk RBA&lt;/a>&lt;/td>
 &lt;td>C 反向&lt;/td>
 &lt;td>概念反向定義「alert fatigue 是 detection 天花板」&lt;/td>
 &lt;td>193&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/page-shield-csp-sri/" data-link-title="Cloudflare Page Shield：用 CSP &amp;#43; SRI &amp;#43; script monitoring 防 client-side supply chain" data-link-desc="Page Shield 三層防禦（CSP / SRI / script monitoring）對應 Magecart / formjacking / skimmer / 第三方 SDK 注入的不同 attack pattern、Cloudflare dashboard &amp;#43; API 配置、四個 production 踩雷（inline script 漏 / dynamic loader / CSP report 噪音 / SRI hash mismatch）、跟 dev workflow &amp;#43; WAF 整合">Cloudflare Page Shield&lt;/a>&lt;/td>
 &lt;td>D 對照表&lt;/td>
 &lt;td>對照表驅動「Attack pattern x Defense mechanism」&lt;/td>
 &lt;td>214&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一輪確認了結構 framework 成立、且章節名可隨主題調整。&lt;/p></description><content:encoded><![CDATA[<p>Vendor overview 寫完後、往下寫單一功能深度文章時，選題與結構需要不同的方法論。操作步驟維護在 <code>.claude/skills/vendor-deep-article/</code>，本文記錄這套方法論從兩輪 batch 中演化出來的過程，重點是 cadence collapse（批量寫作時開場句型同質化重複）怎麼被寫前的 variant 規劃（每篇預先指定不同開場 framing）解決。</p>
<h2 id="背景">背景</h2>
<p>本 blog 的 backend 教學模組已完成多個 vendor overview。overview 層飽和後、自然的下一步是 overview 頁尾「預計實作話題」backlog 的深度文章。</p>
<p>寫了 deep article + migration playbook 後、確認 deep article 跟 overview 是不同產品、需要自己的方法論。差異見 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook 方法論演化紀錄</a>。</p>
<h2 id="第一輪-batch5-篇跨-vendor5-種-entry-framing">第一輪 batch（5 篇）：跨 vendor、5 種 entry framing</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Variant</th>
          <th>章節 1 entry framing</th>
          <th>行數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a></td>
          <td>A 標準</td>
          <td>標準「問題情境」</td>
          <td>263</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault dynamic credential</a></td>
          <td>A 標準</td>
          <td>標準「問題情境」</td>
          <td>222</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/" data-link-title="Kubernetes Graceful Shutdown：termination 序列跟你以為的不一樣" data-link-desc="K8s pod termination 五步序列、preStop / SIGTERM / terminationGracePeriodSeconds 的真實時序、5 個 production 踩雷（500 期間 502、connection drain race、init container 重啟、StatefulSet 串行終止、Job 不 graceful）、跟 service mesh / readiness probe 整合">K8s graceful shutdown</a></td>
          <td>B 痛點</td>
          <td>痛點宣告「沒做對、每次 deploy 都吃 502」</td>
          <td>213</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &#43; case management 整合">Splunk RBA</a></td>
          <td>C 反向</td>
          <td>概念反向定義「alert fatigue 是 detection 天花板」</td>
          <td>193</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/page-shield-csp-sri/" data-link-title="Cloudflare Page Shield：用 CSP &#43; SRI &#43; script monitoring 防 client-side supply chain" data-link-desc="Page Shield 三層防禦（CSP / SRI / script monitoring）對應 Magecart / formjacking / skimmer / 第三方 SDK 注入的不同 attack pattern、Cloudflare dashboard &#43; API 配置、四個 production 踩雷（inline script 漏 / dynamic loader / CSP report 噪音 / SRI hash mismatch）、跟 dev workflow &#43; WAF 整合">Cloudflare Page Shield</a></td>
          <td>D 對照表</td>
          <td>對照表驅動「Attack pattern x Defense mechanism」</td>
          <td>214</td>
      </tr>
  </tbody>
</table>
<p>第一輪確認了結構 framework 成立、且章節名可隨主題調整。</p>
<h3 id="6-段-framework-成立但章節名可變">6 段 framework 成立但章節名可變</h3>
<p>6 段內容指引（問題情境 → 概念 → 配置 → 演練 → 容量 → 整合）在 5 篇都成立。但章節 1 的 framing 因主題本質不同自然分化 — 5 種 entry framing 都成立、章節 1 不必死守「問題情境」標題。</p>
<p>據此小修方法論：6 段 framework 是內容指引、不是章節標題模板。</p>
<h3 id="cadence-collapse-0--主動-variant-有效">Cadence collapse 0% — 主動 variant 有效</h3>
<p>後 4 篇寫作前主動規劃 4 種 framing variant。跟 backend/07 的 51 vendor batch 對照：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>backend/07 51 vendor</th>
          <th>deep article 後 4 篇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cadence「任一缺失」族重複</td>
          <td>51/51 (100%)</td>
          <td>0/4 (0%)</td>
      </tr>
      <tr>
          <td>章節 1 entry framing 種類</td>
          <td>1 種</td>
          <td>4 種</td>
      </tr>
  </tbody>
</table>
<h3 id="reviewer-單人足夠">Reviewer 單人足夠</h3>
<p>deep article 焦點窄（單一 feature）、跨章 frame 重複風險低、case 引用密度低（1-2 個對照）。5 篇都採單一 reviewer 流程、未出現需要 multi-axis review 的盲點。</p>
<h2 id="第二輪-batch5-篇同-vendor-sub-tool-系列最高-collapse-風險">第二輪 batch（5 篇）：同 vendor sub-tool 系列、最高 collapse 風險</h2>
<p>第二輪刻意選 cadence collapse 最高風險場景：5 篇 PostgreSQL sub-tool deep article、同 vendor / 同 article type / 同 audience / 同 6-section framework。</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Variant</th>
          <th>章節 1 entry framing</th>
          <th>行數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a></td>
          <td>E lifecycle-driven</td>
          <td>「Failover lifecycle 5 段不是一條曲線」</td>
          <td>243</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum tuning</a></td>
          <td>B pain-driven</td>
          <td>「你的 autovacuum 永遠追不上 bloat — 為什麼」</td>
          <td>202</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">declarative partitioning</a></td>
          <td>C concept-reversed</td>
          <td>「Partition 不是『把大表切小』、是『讓 planner pruning + 縮小 maintenance scope』」</td>
          <td>244</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">logical replication + Debezium</a></td>
          <td>D table-driven</td>
          <td>「Replication slot x Failure x Recovery 對照」</td>
          <td>227</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL archiving</a></td>
          <td>A standard 6-section</td>
          <td>「問題情境」</td>
          <td>273</td>
      </tr>
  </tbody>
</table>
<p>第二輪在最高風險場景（同 vendor sub-tool）仍維持 collapse 0%，且新增第五種 variant（lifecycle-driven）。</p>
<h3 id="跨兩輪對照">跨兩輪對照</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>第一輪 N=4（跨 vendor）</th>
          <th>第二輪 N=5（同 vendor sub-tool）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Variant 種類</td>
          <td>4（A / B / C / D）</td>
          <td>5（A / B / C / D / E）</td>
      </tr>
      <tr>
          <td>Cadence collapse</td>
          <td>0/4 (0%)</td>
          <td>0/5 (0%)</td>
      </tr>
      <tr>
          <td>章節 1 entry framing 種類</td>
          <td>4</td>
          <td>5</td>
      </tr>
      <tr>
          <td>共同 context</td>
          <td>6-section framework</td>
          <td>6-section + 同 vendor + 同讀者</td>
      </tr>
  </tbody>
</table>
<p>關鍵驗證：</p>
<ol>
<li><strong>N=5 仍 0% collapse</strong>：5 種 variant 在最高風險場景（同 vendor sub-tool）仍完全錯開</li>
<li><strong>5 variant 不耗盡</strong>：5 種變體（lifecycle / pain / reverse / table / standard）對應主題自然進入方式、不是強制配對</li>
<li><strong>cadence audit 最佳位置是進度 60-80%</strong>：進度 10-20% 只有 1 樣本訊號弱、60-80% 有 4 樣本對照訊號強</li>
</ol>
<h2 id="方法論演化小結">方法論演化小結</h2>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>修改</th>
          <th>驅動來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>v0</td>
          <td>直覺套 overview 11 章節</td>
          <td>第一篇 deep article 不合用</td>
      </tr>
      <tr>
          <td>v1</td>
          <td>6 段結構 + 200-400 行 sweet spot</td>
          <td>第一輪 5 篇 dogfood</td>
      </tr>
      <tr>
          <td>v1.1</td>
          <td>6 段是內容指引、不是章節標題模板</td>
          <td>章節 1 framing 自然分化</td>
      </tr>
      <tr>
          <td>v1.2</td>
          <td>寫作時間預估 2-4hr → 1-2hr</td>
          <td>overview 已建立 context</td>
      </tr>
      <tr>
          <td>v1.3</td>
          <td>cadence audit 抽樣位置 10-20% → 60-80%</td>
          <td>第二輪 N=5 驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Vendor Deep Article skill（<code>.claude/skills/vendor-deep-article/</code>）— 操作步驟</li>
<li><a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 方法論演化紀錄</a> — sibling、處理 cross-vendor process</li>
<li><a href="/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">Case-First Agent Team Review Workflow</a> — 教學模組級批次寫作流程</li>
<li><a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a> — 本文精簡的依據</li>
</ul>
]]></content:encoded></item><item><title>Case-First + Agent Team Review：教學內容的生產流程</title><link>https://tarrragon.github.io/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/</guid><description>&lt;h2 id="這篇要說什麼">這篇要說什麼&lt;/h2>
&lt;p>寫教學文章時、純靠 LLM 自生內容會踩到兩個系統性盲點：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Scope 盲點&lt;/strong>：內容停在「教科書級」結構、漏掉真實事故才會浮現的失敗模式跟設計取捨。&lt;/li>
&lt;li>&lt;strong>準確性盲點&lt;/strong>：把通用 best practice 包裝成「[case] 揭露」、把案例沒講的細節寫成案例事實。&lt;/li>
&lt;/ol>
&lt;p>本文整理在 backend/01 至 backend/07 batch 1 七個模組撰寫過程中浮現的五階段流程：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>完整閱讀案例庫、抽 findings&lt;/strong> — 用案例驅動「該寫什麼」、不只是 LLM 自生&lt;/li>
&lt;li>&lt;strong>基於 findings 建立內容&lt;/strong> — findings 分布到章節、避免硬塞模板&lt;/li>
&lt;li>&lt;strong>Agent team 平行多輪審查&lt;/strong> — 用 3 個專責 reviewer 補 LLM 自盲點&lt;/li>
&lt;li>&lt;strong>修正循環&lt;/strong> — 按檔案批次修 high + 重要 medium、reviewer 抓出問題各章節對應修&lt;/li>
&lt;li>&lt;strong>Polish pass&lt;/strong> — 跨檔系統性 pattern 集中處理（負向骨架掃描、編號漂移、用語不一、cross-link 補漏）&lt;/li>
&lt;/ol>
&lt;p>實作數據：7 個模組（backend/01-07 batch 1）、~45 章 / 385 個 review issue、case fidelity 落在 70-93% 區間、修正後品質升至 0 critical 編造、cross-link 全綠、規範違反 polish pass 後降到單位數低 issue。06 模組後方法論工具化為可觸發 skill、stage 1-5 流程跟 reviewer prompt template、self-scan regex 都封裝成可重用元件。07 模組驗證下「章節已有 routing layer skeleton」的特殊處理（在現有結構內補 case-driven 深化段、不擴成厚重章節）。&lt;/p>
&lt;h2 id="問題llm-自生內容的兩個盲點">問題：LLM 自生內容的兩個盲點&lt;/h2>
&lt;p>純靠 LLM 寫教學章節、容易產出兩種品質風險：&lt;/p>
&lt;p>&lt;strong>Scope 盲點&lt;/strong>：LLM 從訓練資料抽出的內容偏 &lt;em>普遍性&lt;/em>、是「教科書 + 部落格 + 文件」的綜合。但真實工程議題的判讀條件常常來自 &lt;em>特定事故揭露&lt;/em>、不是普遍知識。例：&lt;/p>
&lt;ul>
&lt;li>「DynamoDB GSI 在 backfill 完成前查不到完整資料」這種具體陷阱&lt;/li>
&lt;li>「Super Bowl +50% no sweat 的工程意義是 headroom 提前預留、不是 vendor 神奇」這種反直覺判讀&lt;/li>
&lt;li>「99.99% → 99.999% 是指數成本、遠超直覺的 10x 線性想像」這種規模對照&lt;/li>
&lt;/ul>
&lt;p>純技術知識推導不出來、要看真實案例才會浮現。&lt;/p>
&lt;p>&lt;strong>準確性盲點&lt;/strong>：LLM 寫到「對應 [case]」時、容易把通用 best practice 包裝成案例事實、或把案例沒提到的細節擴寫成「案例揭露」。例（從本文討論的實作中抓出的真實 issue）：&lt;/p>
&lt;ul>
&lt;li>Snowflake 案例描述「異常查詢偵測維度（query 體積 / IP / 跨 schema scan）」、LLM 自生內容寫成「query 體積從 1MB / 天跳到 10GB / 天、來源 IP 從 office network 變 unknown VPS」— 具體數字是 LLM 加上去的、案例沒寫&lt;/li>
&lt;li>Tixcraft 案例策略段建議「composite key」、LLM 自生內容寫成「Tixcraft 用 user_id 分散、不是 event_id」— 案例沒揭露 Tixcraft 實際 partition key 設計&lt;/li>
&lt;/ul>
&lt;p>這兩類盲點都不容易在 self-review 時抓到、因為 LLM 看不出自己內容是否真的對應案例。&lt;/p>
&lt;h2 id="階段-1完整閱讀案例庫抽-findings">階段 1：完整閱讀案例庫、抽 findings&lt;/h2>
&lt;h3 id="為什麼要完整閱讀不能只看-title--description">為什麼要完整閱讀、不能只看 title + description&lt;/h3>
&lt;p>只看 title + description 能做 &lt;em>承接&lt;/em>（建立 link）、但無法做 &lt;em>scope 擴展&lt;/em>（揭露 LLM 不會自生的議題）。case 的 findings 通常埋在 body 的「判讀」段、不在 description 裡。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要說什麼">這篇要說什麼</h2>
<p>寫教學文章時、純靠 LLM 自生內容會踩到兩個系統性盲點：</p>
<ol>
<li><strong>Scope 盲點</strong>：內容停在「教科書級」結構、漏掉真實事故才會浮現的失敗模式跟設計取捨。</li>
<li><strong>準確性盲點</strong>：把通用 best practice 包裝成「[case] 揭露」、把案例沒講的細節寫成案例事實。</li>
</ol>
<p>本文整理在 backend/01 至 backend/07 batch 1 七個模組撰寫過程中浮現的五階段流程：</p>
<ol>
<li><strong>完整閱讀案例庫、抽 findings</strong> — 用案例驅動「該寫什麼」、不只是 LLM 自生</li>
<li><strong>基於 findings 建立內容</strong> — findings 分布到章節、避免硬塞模板</li>
<li><strong>Agent team 平行多輪審查</strong> — 用 3 個專責 reviewer 補 LLM 自盲點</li>
<li><strong>修正循環</strong> — 按檔案批次修 high + 重要 medium、reviewer 抓出問題各章節對應修</li>
<li><strong>Polish pass</strong> — 跨檔系統性 pattern 集中處理（負向骨架掃描、編號漂移、用語不一、cross-link 補漏）</li>
</ol>
<p>實作數據：7 個模組（backend/01-07 batch 1）、~45 章 / 385 個 review issue、case fidelity 落在 70-93% 區間、修正後品質升至 0 critical 編造、cross-link 全綠、規範違反 polish pass 後降到單位數低 issue。06 模組後方法論工具化為可觸發 skill、stage 1-5 流程跟 reviewer prompt template、self-scan regex 都封裝成可重用元件。07 模組驗證下「章節已有 routing layer skeleton」的特殊處理（在現有結構內補 case-driven 深化段、不擴成厚重章節）。</p>
<h2 id="問題llm-自生內容的兩個盲點">問題：LLM 自生內容的兩個盲點</h2>
<p>純靠 LLM 寫教學章節、容易產出兩種品質風險：</p>
<p><strong>Scope 盲點</strong>：LLM 從訓練資料抽出的內容偏 <em>普遍性</em>、是「教科書 + 部落格 + 文件」的綜合。但真實工程議題的判讀條件常常來自 <em>特定事故揭露</em>、不是普遍知識。例：</p>
<ul>
<li>「DynamoDB GSI 在 backfill 完成前查不到完整資料」這種具體陷阱</li>
<li>「Super Bowl +50% no sweat 的工程意義是 headroom 提前預留、不是 vendor 神奇」這種反直覺判讀</li>
<li>「99.99% → 99.999% 是指數成本、遠超直覺的 10x 線性想像」這種規模對照</li>
</ul>
<p>純技術知識推導不出來、要看真實案例才會浮現。</p>
<p><strong>準確性盲點</strong>：LLM 寫到「對應 [case]」時、容易把通用 best practice 包裝成案例事實、或把案例沒提到的細節擴寫成「案例揭露」。例（從本文討論的實作中抓出的真實 issue）：</p>
<ul>
<li>Snowflake 案例描述「異常查詢偵測維度（query 體積 / IP / 跨 schema scan）」、LLM 自生內容寫成「query 體積從 1MB / 天跳到 10GB / 天、來源 IP 從 office network 變 unknown VPS」— 具體數字是 LLM 加上去的、案例沒寫</li>
<li>Tixcraft 案例策略段建議「composite key」、LLM 自生內容寫成「Tixcraft 用 user_id 分散、不是 event_id」— 案例沒揭露 Tixcraft 實際 partition key 設計</li>
</ul>
<p>這兩類盲點都不容易在 self-review 時抓到、因為 LLM 看不出自己內容是否真的對應案例。</p>
<h2 id="階段-1完整閱讀案例庫抽-findings">階段 1：完整閱讀案例庫、抽 findings</h2>
<h3 id="為什麼要完整閱讀不能只看-title--description">為什麼要完整閱讀、不能只看 title + description</h3>
<p>只看 title + description 能做 <em>承接</em>（建立 link）、但無法做 <em>scope 擴展</em>（揭露 LLM 不會自生的議題）。case 的 findings 通常埋在 body 的「判讀」段、不在 description 裡。</p>
<p>實作中的對照：第一輪 audit 6 個 case、每 case 平均揭露 2.3 個 finding；其中約 7 成是 description 跟 title 看不到、要讀完整 body 才能抽出。例如 DraftKings 案例的「讀寫雙峰錯位」（比賽中讀爆量、payout 時寫爆量）— description 只說「financial ledger」、要讀「核心負載形狀」段才看到雙峰結構。</p>
<h3 id="邊際遞減的判斷">邊際遞減的判斷</h3>
<p>不是所有 case 都要讀。實作中觀察到的遞減曲線：</p>
<table>
  <thead>
      <tr>
          <th>輪次</th>
          <th>讀案例數</th>
          <th>揭露 findings</th>
          <th>平均 / case</th>
          <th>純新議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第一輪</td>
          <td>6</td>
          <td>14</td>
          <td>2.3</td>
          <td>~95%</td>
      </tr>
      <tr>
          <td>第二輪</td>
          <td>5</td>
          <td>15</td>
          <td>3.0</td>
          <td>~85%</td>
      </tr>
      <tr>
          <td>第三輪</td>
          <td>5</td>
          <td>13</td>
          <td>2.6</td>
          <td>~60%</td>
      </tr>
  </tbody>
</table>
<p>第三輪開始 <em>純新議題</em> 比例下降、重複 frame 出現（vendor dogfood 在 3 個 case 都揭露、benchmark 對照基準在 3 個 case 都揭露）。這是停止 audit 的訊號。</p>
<p>判讀條件：</p>
<ul>
<li><strong>繼續 audit</strong>：每 case 至少 1.5 個純新議題、且重複 frame 不超過 30%</li>
<li><strong>停止 audit</strong>：純新議題 &lt; 1 個 / case、重複 frame &gt; 50%、累積 finding 數已涵蓋目標章節主要議題</li>
</ul>
<p>實作中 11/94 cases（~12%）時邊際遞減訊號明顯、16/94 cases（~17%）時停止 audit、抽出 ~42 個 unique findings、足以支撐 6 個章節的 scope 擴展。</p>
<h3 id="findings-抽取方法">Findings 抽取方法</h3>
<p>讀 case 時、把每個段落看成可能的 finding 來源、問三個問題：</p>
<ol>
<li><strong>這段揭露什麼判讀條件</strong>？（是不是純技術推導不易浮現的議題）</li>
<li><strong>這段揭露什麼數字 / 設計細節</strong>？（規模、percentile、partition key 數量、replication lag 量級）</li>
<li><strong>這段揭露什麼失敗模式</strong>？（事故當下會出什麼問題、有什麼反直覺結論）</li>
</ol>
<p>寫進 findings 列表時、要附上 <em>case 來源</em> 跟 <em>該對應到哪個章節</em>。例：</p>
<blockquote>
<p>Finding: 線性擴展是 OLTP 設計最高目標、coordinator 是傳統 OLTP 的擴展瓶頸
來源: 9.C10 Spanner 案例「2 nodes → 45K reads/sec, 4 nodes → 90K reads/sec」段
章節: 1.11 全球分散式 OLTP</p></blockquote>
<p>不寫來源跟章節定位、findings 會變成抽象列表、寫稿時用不上。</p>
<h3 id="case-類型的承接策略">Case 類型的承接策略</h3>
<p>不同 case 類型適合不同承接深度、誤判類型會引發 <em>over-extrapolation</em> 問題。實作中觀察到的兩類 case：</p>
<p><strong>Rich case</strong>（典型：09/07 案例庫中含具體數字、設計細節、遷移路徑的長篇 case）：</p>
<ul>
<li>內容深度：50-200 行、含具體數字、業務情境、引用源</li>
<li>承接方式：可直接引用為事實、case 揭露的具體數字（RPS、延遲、TPS、stale window）可放進章節</li>
<li>例：9.C5 Amazon Ads「90M RPS + 5M writes/sec + 99.999%」可直接寫進 1.10 KV 章節</li>
<li>例：9.C6 Tinder「4700 萬 MAU 配對引擎、cache 是主要服務面」可直接做為 2.1 high-concurrency 的判讀依據</li>
</ul>
<p><strong>Medium case</strong>（06 模組新發現的類別、典型：模組內部 case 庫中含結構化「決策機制」+「可觀測訊號」表、但無具體數字的中篇 case）：</p>
<ul>
<li>內容深度：30-50 行、結構化 5 段（問題場景 / 決策機制 / 可觀測訊號 / 常見陷阱 / 下一步路由）、含 mechanism + 訊號名稱、但不給具體數字</li>
<li>承接方式：用 case 直接列出的 <em>mechanism 名稱</em> 精準引用、比 skeleton 精準、但比 rich 保守</li>
<li>承接句型：「對應 [case]：揭露 N 個機制 — A、B、C、D」</li>
<li>例：6.C1 Amazon Shuffle Sharding 揭露 cell boundary / shuffle sharding / static stability / constant work 四機制、可直接引用機制名稱、但不擴寫到「具體 shard 數量」「具體 cell 大小」等 case 沒提的實作細節</li>
</ul>
<p><strong>Skeleton case</strong>（典型：模組內部 N.Cx 案例庫中只有 frame、無具體數字的短篇 case）：</p>
<ul>
<li>內容深度：10-30 行、只給方向、無具體數字 / taxonomy</li>
<li>承接方式：作為「視角 / 方向」、可引用為「case 揭露 X 議題」、但不引用為「case 揭露 X 具體場景數量」</li>
<li>例：2.C1 Meta Cache Consistency 只有「promotion、shard move、故障恢復」三個方向、不引用為「具體 inconsistency window 數字」</li>
<li>例：3.C9 反例只給「依賴特定 offset / 重試節奏 / idempotency」三個方向、不引用為「4 個具體誤配場景」</li>
</ul>
<p><strong>判讀條件</strong>：</p>
<ul>
<li>看 case 行數 + 內容密度判斷類型</li>
<li>skeleton case 的 finding 寫成「對應 [case] — 揭露 X 方向、以下展開基於通用工程知識補充」</li>
<li>medium case 的 finding 寫成「對應 [case]：揭露 N 個機制 — A、B、C、D」、用 mechanism 名稱精準引用</li>
<li>rich case 的 finding 可寫「對應 [case] — XXX 具體數字 / 設計」</li>
</ul>
<p>實作中（01/02/03 三個模組驗證）、skeleton case 寫成 rich case 對應是 case fidelity reviewer 抓出 over-extrapolation 的主要來源（02 / 03 各 3-4 個 critical 編造都來自此陷阱）。誤判類型 → 編造 case 沒寫的細節 → reviewer 抓出 → 修正成本高。stage 1 抽 findings 時就要 <em>標明 case 類型</em>、stage 2 寫作時依類型決定承接深度。</p>
<p><strong>Rich case 引用的反向風險（04/05 模組新發現）</strong>：rich case 雖然可以引用具體數字、但 case 內常含「觀察層」（具體 fact）跟「判讀層」（作者推論）兩段、引用時要分開處理。05 模組驗證時 case fidelity reviewer 抓出 4 個 high issue 都來自把「判讀層作者推論」寫成「case 揭露的 fact」：</p>
<ul>
<li>9.C12 Riot Games：5.2 寫「揭露 35ms latency 反推 region 部署」、實際 case 的「35ms」是觀察層、「反推 region 部署」是作者判讀層</li>
<li>9.C34 GCP 130K：5.2 寫「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」、實際 case 用更保守的「control plane 極限取決於 storage backend、GCP 用 Spanner 替換 etcd」分兩個點寫</li>
<li>9.C12 Riot：5.2 引用「single-tenant per game 的多 cluster 策略」、漏掉 case 揭露的關鍵歷史轉折「從 multi-tenant cluster 模型改成 single-tenant per game」</li>
</ul>
<p><strong>修法</strong>：rich case 引用時、用「揭露 X 觀察 + 作者判讀 Y」分層標明、避免把推論寫成 fact。或在引用後補一句「（case 中 X 屬作者判讀層、本章引用此推論）」明示分層。</p>
<p>兩類 case 的引用紀律可總結成一個 <em>fact vs derive</em> 分層原則：</p>
<ul>
<li><strong>Skeleton case</strong>：絕大多數內容是 derive（方向 / 議題）、引用時不擴寫成 fact</li>
<li><strong>Rich case</strong>：含 fact（具體數字 / 設計）跟 derive（作者判讀）、引用時分層標明、避免把 derive 升級成 fact</li>
</ul>
<h2 id="階段-2基於-findings-建立內容">階段 2：基於 findings 建立內容</h2>
<h3 id="findings-分布到章節">Findings 分布到章節</h3>
<p>抽完 findings 後、按章節主題分類、看哪個章節缺口最大、哪個 finding 該寫去哪。實作中的分布：</p>
<ul>
<li>1.1 高併發：7 findings</li>
<li>1.5 紅隊：8 findings</li>
<li>1.9 reconciliation：4 findings</li>
<li>1.10 KV：6 findings</li>
<li>1.11 全球分散式：10 findings（最大缺口）</li>
<li>1.6+1.12 migration：5 findings</li>
</ul>
<p>涉及多軸取捨的章節（1.11 一致性 / 可用性 / 成本 / 延遲）暴露最多缺口、純流程章節（1.9）暴露最少。這是 <em>章節結構性質</em> 的差異、不是寫得好壞。</p>
<h3 id="stage-2-寫作前先定-ssot-對應">Stage 2 寫作前先定 SSoT 對應</h3>
<p>當同一 finding 或 frame 在 <em>多個章節</em> 都有用、要在開始寫之前 <em>先定 SSoT 對應</em>、否則 case-driven 擴章必然出現 frame 重複展開。</p>
<p>實作中觀察到的反例（02 / 03 模組都遇到過）：</p>
<ul>
<li><strong>02 cache</strong>：「cache 角色變化」frame 在 2.1 主寫但實際屬模組層級、應在 <code>_index</code>；Tubi 案例在 2.1 / 2.2 / 2.8 三章各自展開 mini-finding；Snap KeyDB 在 2.1 / 2.7 / 2.8 三章重複</li>
<li><strong>03 message-queue</strong>（最嚴重）：「三層語意（delivery / processing / recovery）」在 3.4 / 3.6 / 3.8 三章各自定義；「Slack Kafka+Redis 拓樸」在 3.4 跟 3.8 兩章逐字重複；「規模對照（小 / 中 / 大型）」在 3.4 / 3.6 / 3.8 三章拆用、結論散落讀者拼不出總圖</li>
</ul>
<p><strong>SSoT 對應的判讀順序</strong>：</p>
<ol>
<li>列出所有 cross-chapter findings（出現在多章的 frame）</li>
<li>每個 frame 指定 <em>一個</em> 主寫章節（SSoT）</li>
<li>其他章節 <em>只 link</em>、不展開</li>
<li>SSoT 章節要有完整論述、被引用章節保留簡述跟 cross-link</li>
</ol>
<p><strong>SSoT 選擇標準</strong>：</p>
<ul>
<li>frame 涉及 <em>跨模組層級概念</em> → 寫進 <code>_index.md</code></li>
<li>frame 涉及 <em>單章核心責任</em> → SSoT 為該章</li>
<li>frame 涉及 <em>跨章交接點</em> → 選最相關章節為 SSoT、其他章節 link</li>
</ul>
<p>漏掉這步、reviewer 跨章一致性會抓出 5-10 個 frame 重複 issue、修正成本高（要把已展開內容收斂回 SSoT）。Stage 2 前花 30 分鐘做 SSoT 對應、能省下 Stage 3 數小時的重構工。</p>
<h3 id="避免硬塞模板">避免硬塞模板</h3>
<p>最大的反模式是把多個 findings 硬塞成同一個 table、每 row 一短語、失去情境敘事。</p>
<p>實作中的反例：1.9 章新增「Dual-track IC 5 個角色表」、本來想用表格整齊呈現、但 reviewer 抓出「5 角色平鋪、責任只一行、未展開每角色在真實事故的決策樣態」。修正後拆成：</p>
<ul>
<li>主表格（5 個角色快速對照）</li>
<li>Overall IC 跟 Tech IC 的差異獨立段（300 字）</li>
<li>Data IC 的特殊角色獨立段（300 字、含「為什麼不能讓 Tech IC 兼任」的失誤對照）</li>
<li>事先準備 4 項各自延伸（不只列項目、解釋失效樣態）</li>
</ul>
<p>這樣 <em>每個項目都是情境</em> 而非 <em>硬塞的欄位</em>、符合 AGENTS.md「表格不是終點」原則。</p>
<h3 id="情境敘事的判讀條件">情境敘事的判讀條件</h3>
<p>每段內容寫完後、問三個檢查問題：</p>
<ol>
<li><strong>首句是不是核心原則</strong>？（不是「某 case 揭露 X」、是「X 是什麼、承擔什麼責任」）</li>
<li><strong>是不是用否定句主導</strong>？（「不是 X」「不只 X」開段要回到正向陳述）</li>
<li><strong>這個 finding 在不同情境下是否會變義</strong>？（一個 finding 套到多個情境、要分情境寫、不是套同模板）</li>
</ol>
<h3 id="案例引用的準確性">案例引用的準確性</h3>
<p>寫「對應 [case] — XXX」時、要回 case 原文驗證 XXX 是否真的出現。實作中常見的失分：</p>
<ul>
<li>把 case 沒提到的數字補進去（「30-90 天 baseline」、「1MB→10GB / 天」）</li>
<li>把通用 best practice 寫成案例事實（「Snowflake 之後改為預設強制 MFA」— case 只說「資料平台應預設強制 MFA」、不是描述後續行動）</li>
<li>公開事實但 case 沒寫（「MOVEit 跨上百家客戶」、「LastPass master password 弱可被離線爆破」）</li>
</ul>
<p>寫稿當下不容易抓、要靠階段 3 的 case fidelity reviewer 對照。</p>
<h2 id="階段-3agent-team-平行多輪審查">階段 3：Agent team 平行多輪審查</h2>
<h3 id="為什麼要-agent-team不能交給單一-reviewer">為什麼要 agent team、不能交給單一 reviewer</h3>
<p>單一 reviewer 有兩個限制：</p>
<ol>
<li><strong>維度盲點</strong>：一個 reviewer 同時看寫作規範、案例準確性、跨章一致性、容易 <em>維度互相干擾</em>、最後每個維度都看不深</li>
<li><strong>Context 污染</strong>：reviewer 讀完整 commit + 所有案例 + 所有章節後、自身 context 就被佔滿、給的建議會 <em>對應主 context 也跟著沉重</em></li>
</ol>
<p>解法是用 3 個專責 reviewer、平行 background 跑、各自獨立報告、主 context 只看精煉摘要。</p>
<h3 id="三個維度-reviewer-分工">三個維度 reviewer 分工</h3>
<p>實作中使用的三個 reviewer：</p>
<h4 id="reviewer-a寫作規範審查agentsmd-核心原則">Reviewer A：寫作規範審查（AGENTS.md 核心原則）</h4>
<ul>
<li>對照核心原則先行、正向陳述優先、商業邏輯先於 case、表格不是終點、情境優先於模板、可操作判準等八原則</li>
<li>找首句用否定句切入、表格 / bullet 平鋪沒延伸、表格項硬塞模板等</li>
<li>實作中抓出 25 個 issue</li>
</ul>
<h4 id="reviewer-b案例引用準確性">Reviewer B：案例引用準確性</h4>
<ul>
<li>對照原始 case 內容、驗證「對應 [case] — XXX」斷言是否真的來自案例</li>
<li>識別編造數字、過度推論、把通用 best practice 寫成案例事實</li>
<li>實作中抓出 9 個 issue、包含 3 個 critical 編造</li>
</ul>
<h4 id="reviewer-c跨章一致性">Reviewer C：跨章一致性</h4>
<ul>
<li>跨多章找重複 frame、矛盾說法、失效 cross-link、章節邊界錯位</li>
<li>識別「該在 A 章卻寫在 B 章」、「frame 重複展開沒整併」</li>
<li>實作中抓出 13 個 issue</li>
</ul>
<h3 id="平行-background-跑不佔主-context">平行 background 跑、不佔主 context</h3>
<p>關鍵設計是 3 個 reviewer 並行、各自 background、各自寫 output file、不污染主 context：</p>
<ul>
<li>主 context 只看到「啟動 reviewer」跟「reviewer 完成的彙整報告」</li>
<li>Raw output 跟 reviewer 的 deep dive 留在 output file、需要時 SendMessage 繼續對話</li>
<li>3 個 reviewer 完成時間 ~5-15 分鐘、可以同時跑、不必等</li>
</ul>
<p>實作中 3 個 reviewer 平均 2-3 分鐘完成、主 context 增量 ~3K tokens（彙整 + 47 issue 清單）、相比把所有案例跟章節塞進主 context 做 review 節省 ~80% context。</p>
<h3 id="reviewer-issue-數量的-baseline">Reviewer issue 數量的 baseline</h3>
<p>7 個模組（01 / 02 / 03 / 04 / 05 / 06 / 07 batch 1）驗證後、每模組 reviewer 抓到的 issue 數量在 standards reviewer 抓 pattern 越來越細的趨勢下持續擴大、可作為流程預期：</p>
<table>
  <thead>
      <tr>
          <th>Reviewer 維度</th>
          <th>01</th>
          <th>02</th>
          <th>03</th>
          <th>04</th>
          <th>05</th>
          <th>06</th>
          <th>07 b1</th>
          <th>baseline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Standards reviewer</td>
          <td>25</td>
          <td>20</td>
          <td>20</td>
          <td>31</td>
          <td>28</td>
          <td>45</td>
          <td>31</td>
          <td>20-45 issue</td>
      </tr>
      <tr>
          <td>Case fidelity reviewer</td>
          <td>9 (88%)</td>
          <td>20 (78%)</td>
          <td>15 (70%)</td>
          <td>6 (92.9%)</td>
          <td>13 (80%)</td>
          <td>11 (88%)</td>
          <td>8 (81%)</td>
          <td>6-20 issue</td>
      </tr>
      <tr>
          <td>Consistency reviewer</td>
          <td>13</td>
          <td>15</td>
          <td>15</td>
          <td>14</td>
          <td>18</td>
          <td>15</td>
          <td>13</td>
          <td>13-18 issue</td>
      </tr>
      <tr>
          <td><strong>總計</strong></td>
          <td><strong>47</strong></td>
          <td><strong>55</strong></td>
          <td><strong>50</strong></td>
          <td><strong>51</strong></td>
          <td><strong>59</strong></td>
          <td><strong>71</strong></td>
          <td><strong>52</strong></td>
          <td><strong>47-71 issue</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>模式觀察</strong>：</p>
<ul>
<li><strong>每模組 issue 數隨 standards reviewer 抓 pattern 越來越細而擴大</strong>：01-03 穩定在 47-55、04/05 推到 51-59、06 推到 71、07 batch 1 回到 52（章節已有 routing skeleton、擴章規模小）。趨勢來自 standards reviewer 抓的 pattern 越來越廣（從負向骨架 → 「核心責任不是」變體 → 「沒有 X 會 Y」鏈式 → 「case 引用段首」框架 → 「case 引用句構同質化」）。</li>
<li><strong>Case fidelity 準確率分布更廣</strong>：04 的 92.9% 來自 skeleton case 嚴守「揭露方向、通用補充」紀律；05 的 80% 因引用 09 rich case 加入「fact vs derive 分層」新失分模式；06 的 88% 屬 medium case 紀律首次套用、揭露「實作層擴寫過頭」失分；07 batch 1 的 81% 揭露「跨 case 合成 frame」新失分類型（reviewer B 2 high 都屬此類）</li>
<li><strong>Consistency reviewer 抓到的 frame 重複跟章節數成正比</strong>：02 / 03 / 04 都有 ~13-18 個一致性 issue、05/06 跨模組 cross-link 密度高仍維持在 baseline 內、07 batch 1 因 7 章規模、issue 13 個落在 baseline 下緣</li>
</ul>
<p><strong>Stage 3 修正成本估算</strong>：</p>
<ul>
<li>Critical（編造、矛盾）：~每個 5-10 分鐘修正、佔 0-5 個（04/05 都 0 critical、紀律已成熟）</li>
<li>High（重複 frame、章節邊界、判讀層 vs fact）：~每個 10-20 分鐘修正、佔 5-14 個</li>
<li>Medium / Low（規範細節、cross-link 補）：~每個 2-5 分鐘修正、佔 35-45 個</li>
<li><strong>總計 ~1.5-2.5 小時 / 模組</strong></li>
</ul>
<p><strong>Stage 4 修正後仍會有 ~30-40% issue 殘留</strong>（low / medium 的 cross-link、編號漂移、用語不一）、屬於系統性 pattern、適合在 Stage 5 polish pass 集中處理（見後段）。</p>
<h3 id="為何要多輪-review不是一次到位">為何要多輪 review、不是一次到位</h3>
<p>第一輪 review 的目的是 <em>找問題</em>、不是 <em>修問題</em>。問題清單列出後、要做兩件事：</p>
<ol>
<li><strong>分類優先序</strong>：critical / high / medium / low、按嚴重度跟修改成本排序</li>
<li><strong>修正循環</strong>：批次修正、避免一個一個改散開、修完再跑驗證</li>
</ol>
<p>修正後可選擇性做第二輪 review、檢查：</p>
<ul>
<li>修正本身有沒有引入新問題</li>
<li>之前 reviewer 漏掉的維度（例：教學性、讀者路徑、實作可行性）</li>
<li>跨 commit 一致性</li>
</ul>
<p>實作中第一輪足夠處理 47 個 issue、第二輪沒進行、留到未來模組（02 cache、03 message queue）累積經驗後再評估是否必要。</p>
<h2 id="修正循環的執行原則">修正循環的執行原則</h2>
<p>47 個 issue 分布到 6 個章節、修正時 <em>按檔案批次</em>、不是按 issue 編號順序。每個檔案一次修完所有相關 issue、減少切換成本：</p>
<ul>
<li>1.5 紅隊章（12 issue）：含 2 個 critical 編造、優先處理</li>
<li>1.10 KV（7 issue）：含 1 個 critical 編造</li>
<li>1.11 全球分散式（5 issue）</li>
<li>1.12 大規模遷移（10 issue）：表格密度最高、最多延伸</li>
<li>1.1 高併發（4 issue）</li>
<li>1.9 reconciliation（5 issue）</li>
</ul>
<p>每個檔案修完後跑一次 <code>mdtools fmt --fix</code> + <code>mdtools cards</code> + <code>mdtools lint</code>、確認該檔內部一致、再進下一檔。最後跑一次跨檔驗證、確認 cross-link 全部對齊。</p>
<h2 id="階段-5polish-pass0405-模組後新增">階段 5：Polish pass（04/05 模組後新增）</h2>
<p>Stage 4 修完 high + 重要 medium 後、仍有 ~30-40% 的 low / medium 殘留、屬於系統性 pattern（負向骨架、編號漂移、cross-link 缺漏、模板化）。這些 issue 不適合按章節批次修、適合用「跨檔系統性掃描」處理 — 這是 polish pass 的核心責任。</p>
<h3 id="polish-pass-的觸發條件">Polish pass 的觸發條件</h3>
<p>Stage 4 後出現以下任一訊號、就該排 polish pass：</p>
<ul>
<li>Standards reviewer 抓出的「不是 X、而是 Y」段首結構超過 5 處（屬寫作習慣、單章修改無效率）</li>
<li>Consistency reviewer 抓出「編號漂移」「失效 link」「用語不一」多處（屬跨檔規範問題）</li>
<li>自掃描漏掉的 pattern 出現在 reviewer report（例：04 自掃描說 pass、reviewer A 抓出 31 個 issue、暴露自掃描 regex 不夠寬）</li>
</ul>
<h3 id="polish-pass-不該做的事">Polish pass 不該做的事</h3>
<ul>
<li><strong>不重寫章節結構</strong>：polish pass 是把現有內容修得更貼合規範、不是重新組織。重寫的觸發條件應該回到 stage 2、不是 polish pass。</li>
<li><strong>不擴大 scope</strong>：原本 4.20 / 5.4 等不在擴充範圍的章節、polish pass 也不動。Polish pass 邊界 = stage 4 修改過的章節集合。</li>
<li><strong>不追求 0 issue</strong>：reviewer 抓的 ~15 個 low 通常可保留為下次擴章節時自然處理。Polish pass 處理「系統性 pattern」、不處理「孤立 issue」。</li>
</ul>
<h3 id="polish-pass-的標準工序">Polish pass 的標準工序</h3>
<p>按系統性 pattern 分批處理、每批跑一次自掃描確認：</p>
<ol>
<li><strong>負向骨架掃描修正</strong>：用更寬泛的 regex <code>不是 |而不是|沒有.*[，、]會</code> 掃描、把「不是 X、而是 Y」「而不是 X」改成正向陳述 + 後置邊界提醒。技術約束敘述（「多人共用 IP 無法區分」）保留。</li>
<li><strong>編號漂移統一</strong>：把 <code>04.X</code> 風格 plain text 改成 <code>[4.X title](url)</code> markdown link、跟 _index 對齊。</li>
<li><strong>表格延伸段補強（關鍵段）</strong>：選 2-3 個最高 impact 表格（判讀訊號表的爭議列、Buffer / Sampling 等選型表）補延伸子段、不全部補（避免擴展超出 scope）。</li>
<li><strong>模板化拆敘事（代表性段）</strong>：選 1-2 個最明顯的「四步驟模板套不同情境」段、拆成情境化敘事、其他保留為下次。</li>
<li><strong>Cross-link 補漏 + ownership 邊界補強</strong>：reviewer C 報告的所有 cross-link 缺漏一次補完、用同一個批次跑 mdtools 驗證。</li>
<li><strong>用語不一統一 + 失效 link 修正</strong>：簡轉繁、<code>/knowledge-cards/</code> vs <code>/section/</code> URL 統一、失效 link 改規劃中或正確路徑。</li>
<li><strong>最終驗證 + commit</strong>：跑 <code>mdtools fmt --fix &amp;&amp; mdtools cards &amp;&amp; mdtools lint</code>、確認全綠、commit。</li>
</ol>
<h3 id="polish-pass-的實作成本">Polish pass 的實作成本</h3>
<p>實作中（04 / 05 polish pass 合併 commit <code>1072087</code>）：</p>
<ul>
<li>處理範圍：11 個檔案、+44 / -29 行</li>
<li>修正項目：~35 個 issue（10 個負向骨架、2 個模板化、3 個編號漂移、3 個表格延伸段、3 個 cross-link、1 個 case 引用結構）</li>
<li>時間：~30-45 分鐘（不重寫、只 pattern match）</li>
<li>剩餘 ~15 個 low 保留下次</li>
</ul>
<p>Polish pass 的 ROI 來自「系統性 pattern 一次處理 vs 散在各章一個個改」的效率差異。每個 pattern 在多章重複出現時、用 grep / rg 跨檔修一輪比每章單獨修快 3-5 倍。</p>
<h3 id="自掃描盲點更新">自掃描盲點更新</h3>
<p>04 流程暴露了一個 self-scan 盲點：原 regex <code>不行|不可以|不要|無法|不能</code> 漏掉「核心責任不是 X、而是 Y」這個變體段首。修正建議：</p>
<ul>
<li>加 <code>^[^|].*責任(不是|並非)</code> 抓「核心責任不是 X」變體</li>
<li>加 <code>^[^|].*[，,]而是</code> 抓「X、而是 Y」結構（已是正常陳述、但段首位置仍是負向骨架）</li>
<li>加 <code>^[^|].*[，,]不是</code> 抓「X、不是 Y」結構</li>
</ul>
<p>把自掃描 regex 視為持續演進的工具、每個 reviewer 抓出新 pattern 就更新一次、避免在下個模組重蹈覆轍。</p>
<h2 id="適用情境跟限制">適用情境跟限制</h2>
<h3 id="適用情境">適用情境</h3>
<ul>
<li><strong>長期累積的教學模組</strong>：6+ 章、跨章引用密集、規範遵循重要</li>
<li><strong>有現成 case 庫</strong>：07/09 累積的 100+ 案例是這套流程的前提、沒案例庫做不到 case-first</li>
<li><strong>品質高於速度</strong>：完整三階段約 3-4 小時 / 模組（stage 2 寫作 ~1.5-2hr + reviewer ~15 分鐘 + stage 3 修正 ~1.5-2hr）、適合長期累積的內容、不適合 one-off 文章</li>
<li><strong>主 context 容量敏感</strong>：reviewer 平行 background 是節省 context 的關鍵設計</li>
</ul>
<h3 id="不適用情境">不適用情境</h3>
<ul>
<li><strong>新主題沒案例庫</strong>：要先建案例庫、不能直接套這流程</li>
<li><strong>單篇短文</strong>：流程的固定成本（讀案例 + 跑 reviewer）對短文 ROI 低</li>
<li><strong>快速迭代原型</strong>：流程偏向 <em>寫一次寫好</em>、不是 <em>快速修改</em></li>
<li><strong>Routing layer / 導讀性質章節</strong>：已含完整 threat scope + 引用標準 + 問題節點表、case 庫不對應或缺位、應跳過本流程、用標準引用 + 通用工程知識補充承接（07 LLM / 治理章節驗證）</li>
<li><strong>Standard framework 比 case 庫成熟的領域</strong>：見下段「Standard-driven 取代 case-driven」</li>
</ul>
<h3 id="standard-driven-取代-case-driven07-llm-章節驗證">Standard-driven 取代 case-driven（07 LLM 章節驗證）</h3>
<p>在標準框架比 case 庫成熟的領域、case-driven 不是預設選擇。LLM 安全章節跑完 5 章驗證後浮現一個 finding：當該領域的 <em>標準框架</em>（如 OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS）已涵蓋 threat 分類、且 case 維護半衰期短於 standard、章節應 <em>用 standard-driven 取代 case-driven</em>。Standard-driven 跟 case-driven 是平行選項、依領域特性選用 — 兩者沒有退化 / 進階關係。</p>
<p><strong>判斷該用哪種策略的四維度</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Case-driven 適用</th>
          <th>Standard-driven 適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>議題穩定度</td>
          <td>高（5+ 年穩定）</td>
          <td>低（&lt; 1 年快速演進）</td>
      </tr>
      <tr>
          <td>Case 公開度</td>
          <td>高（充分的事故公告）</td>
          <td>中或低（vendor disclosure 偏 marketing）</td>
      </tr>
      <tr>
          <td>Standard 成熟度</td>
          <td>中（多用 case 而非 standard）</td>
          <td>高（standard framework 已成型）</td>
      </tr>
      <tr>
          <td>維護半衰期</td>
          <td>長</td>
          <td>短（6 個月過時）</td>
      </tr>
  </tbody>
</table>
<p><strong>典型對照</strong>：</p>
<ul>
<li><em>Case-driven 領域</em>：分散式系統 / 安全控制面 / 可靠性 / 訊息佇列（backend/01-07 batch 1 都屬此類、案例公開充分、半衰期 5+ 年）</li>
<li><em>Standard-driven 領域</em>：LLM 安全（OWASP LLM Top 10 / MITRE ATLAS 已成型、案例 6 個月過時）、新興 compliance（NIST AI RMF）、cloud-native 標準（CNCF baseline）</li>
</ul>
<p><strong>Standard-driven 章節的寫作策略</strong>：</p>
<ol>
<li><strong>章節對齊 standard framework 分類</strong>：用 framework 章節 ID 標明（如 OWASP LLM01 / NIST AI-1.1）取代「對應 [case] —」斷言</li>
<li><strong>加 Last reviewed cadence</strong>：每 quarter 重評估 standard 版本跟章節對應、寫進 frontmatter</li>
<li><strong>「案例觸發參考」段標明「公開案例累積中、值得追蹤的方向」</strong>：不寫「對應 [case] 揭露」斷言、避免引用源不穩定</li>
<li><strong>引用標準時用版本號</strong>：OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS continuous — framework 改版要 trigger 章節重審</li>
</ol>
<p><strong>實證</strong>：07 LLM 章節 5 章已套用 standard-driven 策略：</p>
<ul>
<li>章節 113-137 行、含完整 threat scope + 問題節點表 + 風險邊界</li>
<li>引用 OWASP LLM Top 10 + NIST AI RMF + MITRE ATLAS 取代個別 case 引用</li>
<li>加 <code>Last reviewed: 2026-05-12</code> cadence</li>
<li>「案例觸發參考」段寫「公開案例累積中、值得追蹤的方向」+「事實查核註」</li>
<li>完全不寫「對應 [case] —」斷言、不存在 case fidelity reviewer 該抓的準確性問題</li>
</ul>
<p>對照 backend/01-07 batch 1 的 case-driven 章節、LLM 章節是 <em>用不同方法達到同樣品質</em> — scope 涵蓋真實 production 議題（KV cache 跨租戶、shared prefix optimization、batch 推論順序敏感）、不停在教科書級內容。</p>
<p><strong>何時要從 standard-driven 轉回 case-driven</strong>：</p>
<ul>
<li>該領域累積 5+ 個高可信度 case（vendor disclosure + academic paper + CVE 三來源交叉）</li>
<li>跨章 frame 重複出現、case-driven mechanism 深化能解 SSoT 衝突</li>
<li>出現「等級類似 SolarWinds」的 incident、案例本身夠重、單一 case 即可支撐章節擴章</li>
<li>讀者反饋章節太抽象、需要具體 case 才能理解 mechanism</li>
</ul>
<p>不滿足任一條件時、繼續走 standard-driven、不勉強建 case 庫。</p>
<p><strong>對 case-first-module-workflow skill 的補強</strong>：</p>
<p>skill 之前的「不適用情境」寫「沒 case 庫的新主題（要先建 case 庫）」— 這暗示缺 case 庫一定要先補。07 LLM 章節驗證了第三條路：<em>用 standard-driven 取代</em>、適用 standard framework 比 case 庫成熟的領域。這個 finding 已補進 skill 的「不適用情境」段。</p>
<h3 id="限制">限制</h3>
<ul>
<li><strong>Reviewer 維度有限</strong>：當前 3 個 reviewer 沒覆蓋「教學性」「讀者路徑」「實作可行性」、若主題需要這些維度、要加 reviewer</li>
<li><strong>修正可能引入新 issue</strong>：第一輪 review 後修正、修正本身可能違反規範、若大量修正最好做第二輪</li>
<li><strong>Case 庫品質決定 findings 品質</strong>：case 寫得淺、findings 也淺；case fidelity reviewer 也只能驗證「跟 case 一致」、不能驗證「case 本身對不對」</li>
<li><strong>依賴 LLM agent 平台能力</strong>：流程預設可平行跑 background agent、不是所有 LLM 平台都支援</li>
</ul>
<h2 id="7-個模組驗證後的反覆陷阱">7 個模組驗證後的反覆陷阱</h2>
<p>01 / 02 / 03 / 04 / 05 / 06 / 07 七個模組執行下來、以下陷阱在 <em>多數模組都重複出現</em>、屬於 LLM case-driven 寫作的系統性失分點。本流程下次套用前要 <em>主動防範</em>、不能依賴 stage 3 reviewer 補救（雖然 reviewer 都會抓到、但修正成本高）。</p>
<h3 id="陷阱-1skeleton-case-擴寫成-case-事實">陷阱 1：Skeleton case 擴寫成 case 事實</h3>
<p>當 case 內容簡短（10-30 行、只有 frame 沒有具體數字 / taxonomy）、LLM 寫作時容易把通用知識（具體數字、攻擊向量列表、設計細節）寫成「對應 [case] —」斷言。實際 case 沒寫的。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01 紅隊：Snowflake「30-90 天 baseline」編造、Tixcraft「partition key 用 user_id」編造</li>
<li>02 cache：Tubi 三層 cache 具體 latency（L1 &lt; 1ms、L2 &lt; 10ms、L3 10-100ms）編造、Redis「100K-200K ops/sec」無來源、KeyDB「5-10x throughput」其實是 case 判讀段非引用源</li>
<li>03 messaging：PayPay「broker 寫入 3K msg/sec」實際 case 寫的是「DynamoDB 寫入 3K msg/sec」（PayPay 用 DynamoDB 不是傳統 broker）、3.C9 case 三個方向被擴寫成「4 個誤配場景」、3.C10 case 「大型服務 DLQ 是診斷入口」完全編造</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 1 抽 findings 時 <em>標明 case 類型</em>（rich vs skeleton）</li>
<li>Stage 2 寫 skeleton case finding 時、用「對應 [case] — 揭露 X 方向、以下展開基於通用工程知識補充」這種 <em>fact vs derive</em> 標記</li>
<li>不要為了「整齊的 4 個攻擊面」「3 個攻擊向量」「5 個誤配場景」這種數字感、把 case 沒寫的 taxonomy 寫成 case 揭露</li>
</ul>
<h3 id="陷阱-2frame-重複展開ssot-不清">陷阱 2：Frame 重複展開（SSoT 不清）</h3>
<p>同一概念在多章 case-driven 擴章時各自展開、形成 frame 重複。讀者跨章讀會踩到重述、結論散落拼不出總圖。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01：容量三口徑 frame 在 1.1 跟 1.12 重複展開、storage / compute 分離 frame 在 1.1 跟 1.11 重複</li>
<li>02：cache 角色變化 frame 在 2.1 主寫但屬模組層級、應在 _index；Tubi 案例在 2.1 / 2.2 / 2.8 三章 mini-展開</li>
<li>03（最嚴重）：三層語意（delivery / processing / recovery）在 3.4 / 3.6 / 3.8 三章各自定義；Slack Kafka+Redis 拓樸在 3.4 跟 3.8 兩章逐字重複；規模對照在 3.4 / 3.6 / 3.8 三章拆用</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 2 寫作前花 30 分鐘做 SSoT 對應（見前面「Stage 2 寫作前先定 SSoT 對應」段）</li>
<li>列出 cross-chapter frames、指定唯一主寫章節、其他章節只 link</li>
<li>寫每章前問「這個 frame 主寫在哪？我現在寫的是主寫還是 link？」</li>
</ul>
<h3 id="陷阱-3負向陳述--模板化規範系統性失分">陷阱 3：負向陳述 + 模板化（規範系統性失分）</h3>
<p>「不是 X、是 Y」推進論證、L1/L2/L3 三層平鋪、三選一表格、四步驟流程。這兩個原則違反在每模組都重複出現、是 LLM 寫作的反覆模式、stage 3 standards reviewer 每模組會抓 10-20 處。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01 規範 violation：表格不延伸（7 處）、負向陳述（5 處）、首句結構（4 處）</li>
<li>02 規範 violation：原則 8 模板化（6 處）、原則 2 負向陳述（6 處）、原則 4 表格不延伸（4 處）</li>
<li>03 規範 violation：原則 2 負向陳述（12 處最嚴重）、原則 1 首句結構（5 處）、原則 6 用語節制（2 處）</li>
<li>04 規範 violation：原則 2 負向陳述（12 處最嚴重、含「核心責任不是 X、而是 Y」變體段首）、原則 1 首句結構（9 處）、原則 4 表格不延伸（9 處）</li>
<li>05 規範 violation：原則 2「不是 X、而是 Y」+「沒有 X、會 Y」（10 處）、原則 8 四步驟 / 四層並列模板（7 處）、原則 3 case 引用框架取代商業邏輯先行（6 處）</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 2 寫完後 <em>寫稿端就跑掃描</em>、不等 reviewer：
<ul>
<li><code>rg -n &quot;不行|不可以|不要|無法|不能&quot; &lt;module-path&gt;</code> 找負向骨架（技術約束敘述例外）</li>
<li><code>rg -n &quot;^[^|].*責任(不是|並非)&quot; &lt;module-path&gt;</code> 找「核心責任不是 X」變體段首（04 模組新發現的 pattern）</li>
<li><code>rg -n &quot;^[^|].*[，,]而是|^[^|].*[，,]不是&quot; &lt;module-path&gt;</code> 找對比骨架開段</li>
<li>自查表格：每個 bullet 是否有後文延伸？</li>
<li>自查首句：是否「核心原則先行」而非「對應 [case] 揭露」</li>
</ul>
</li>
<li>模板化（L1/L2/L3、三選一）出現時、先問「這三項是真的對等？還是業務情境不同？」— 不同情境的話拆敘事段、不用表格</li>
</ul>
<h3 id="陷阱-4rich-case-判讀層被當-case-fact-引用0405-模組新發現">陷阱 4：Rich case 判讀層被當 case fact 引用（04/05 模組新發現）</h3>
<p>引用 09 / 07 等 rich case 時、case 內常含「觀察層」（具體 fact）跟「判讀層」（作者推論）兩段。LLM 寫作時容易把兩層壓縮成「揭露 X」、把作者判讀升級為 case fact。</p>
<p>跟陷阱 1（skeleton case 擴寫成 case 事實）的差別：</p>
<ul>
<li><strong>陷阱 1</strong>：case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露</li>
<li><strong>陷阱 4</strong>：case 有提、但屬作者判讀層的內容被寫成 case fact</li>
</ul>
<p><strong>實證</strong>：</p>
<ul>
<li>05 / 9.C12 Riot：5.2 寫「揭露 35ms latency 反推 region 部署」、實際 case 的「35ms」是觀察層、「反推 region 部署」是作者判讀層</li>
<li>05 / 9.C34 GCP：5.2 寫「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」、實際 case 用更保守的「control plane 極限取決於 storage backend、GCP 用 Spanner 替換 etcd」分兩個點寫、章節壓縮 + 強化成硬性結論</li>
<li>05 / 9.C12 Riot：漏掉 case 揭露的關鍵歷史轉折「從 multi-tenant cluster 模型改成 single-tenant per game」</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>引用 rich case 前、先把 case 內的「觀察段」跟「判讀段」分開讀、抽 finding 時各自標明來源層</li>
<li>引用時用「揭露 X 觀察 + 作者判讀 Y」分層寫、或在引用後補一句「（case 中 X 屬作者判讀層、本章引用此推論）」</li>
<li>避免使用「才是 / 必須 / 一定」這類強化詞、保留 case 原文的條件性表述</li>
<li>Stage 3 case fidelity reviewer 的 prompt 要特別點出「判讀層 vs 觀察層」的分界、把這當作 high 級 issue 抓取</li>
</ul>
<h3 id="陷阱-5自掃描盲點累積040506-模組持續顯現">陷阱 5：自掃描盲點累積（04/05/06 模組持續顯現）</h3>
<p>自掃描的 regex 跟 reviewer 抓的 pattern 會逐漸脫節。每個模組 reviewer 會發現新 pattern、self-scan regex 跟著演進、但 reviewer 仍會發現下一個。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>04 自掃描用 <code>不行|不可以|不要|無法|不能</code> 跟「不是 X、是 Y」掃描通過、但 reviewer A 抓出「核心責任不是 X、而是 Y」變體段首（佔 12 處）</li>
<li>05 自掃描通過、但 reviewer A 仍抓出「沒有 X、會 Y」鏈式負向句構 + 「四步驟模板」+ 「case 引用框架取代商業邏輯先行」三類新 pattern</li>
<li>06 self-scan 加了「不是 X、而是 Y」變體 + 「沒有 X 會 Y」、仍漏掉「對應 [case]：揭露 N 個機制」段首取代核心概念句的 pattern（reviewer A 抓 45 issue、其中 11/12 新段都犯這個錯）</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>每個模組 reviewer 抓出新 pattern 後、回頭更新 self-scan regex</li>
<li>把 self-scan 視為持續演進的工具、不是固定 checklist</li>
<li>Stage 5 polish pass 是處理自掃描盲點累積的標準入口（見前段）</li>
<li>06 模組後 self-scan 加 <code>rg -n &quot;^對應 \[&quot; &lt;module-paths&gt;</code> 抓段首 case 引用框架</li>
</ul>
<h3 id="陷阱-6case-引用段首取代核心概念句06-模組新發現">陷阱 6：Case 引用段首取代核心概念句（06 模組新發現）</h3>
<p>LLM 從 case 反推內容時、容易把 case 揭露當概念出發點、寫成「對應 [case]：揭露 N 個機制 — &hellip;」段首結構。讀者尚未理解概念就被丟入案例細節、且跨章讀同句構會感同質。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>06 模組 12 個新段中 11 個用「對應 [case]：揭露 N 個機制」相同句構作為 section 第二段</li>
<li>概念定義句被推到第二段或更後、商業邏輯先於 case 的原則被推翻</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>把 case 引用視為「三段式」結構：概念定義句 → case 引用 → 通用展開</li>
<li>寫每段時、先確認段首是「該概念是什麼、承擔什麼責任」、case 引用退到第二位置</li>
<li>Case 引用句構應變化：寫多章時刻意避免同句構連續超過 3 次</li>
<li>詳見 skill 內部原則卡 <code>principles/case-citation-three-part</code>（對應檔案 <code>.claude/skills/case-first-module-workflow/references/principles/case-citation-three-part.md</code>、屬 skill 內部 reference、不對外暴露）</li>
</ul>
<h3 id="陷阱-7medium-case-實作層擴寫過頭06-模組新發現">陷阱 7：Medium case 實作層擴寫過頭（06 模組新發現）</h3>
<p>Medium case（30-50 行、結構化但無具體數字）首次套用時、容易把 case 沒提的具體實作層擴寫進章節、把通用工程知識掛到 case 名下。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>06 模組 6.12 idempotency-replay 從 S1「key 設計要跟業務邊界一致」一條方向擴寫成「key 來源 / TTL / fallback / 偽造防護 / 5 個 observability 欄位」5 條實作判讀、case 沒提這些細節</li>
<li>06 模組 6.14 dependency-reliability-budget 從 M1 region failover 擴寫成「thundering herd」機制名 + 「先恢復核心 region 最小集合」具體步驟、case 沒提這兩個</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Medium case 引用用 <em>mechanism 名稱</em> 精準引用、不擴寫到 case 沒提的具體實作細節</li>
<li>引用後若要展開實作層、用「以下實作層判讀屬通用工程知識展開、case 本身只給 X 方向」明示分層</li>
<li>Case fidelity reviewer 的 prompt 要特別點出 medium case 的「實作層擴寫」失分類型</li>
</ul>
<h3 id="陷阱-8跨-case-合成-frame-升級成-case-揭露07-模組新發現">陷阱 8：跨 case 合成 frame 升級成 case 揭露（07 模組新發現）</h3>
<p>當段落把多個 case 的失效訊號抽象為更高層 frame（如「跨工具回查壓力」「平台責任切分」）、LLM 會把章節合成的 frame 包裝成 case 揭露。讀者回查 case 時會發現章節說的「case 揭露 X」實際是章節 derive、不是 case 原文框架。</p>
<p>跟陷阱 1（skeleton case 擴寫成 case 事實）跟陷阱 4（rich case 判讀層當 fact）的差別：</p>
<ul>
<li><strong>陷阱 1</strong>：case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露</li>
<li><strong>陷阱 4</strong>：case 有提、但屬作者判讀層的內容被寫成 case fact</li>
<li><strong>陷阱 8</strong>：case <em>單獨</em> 寫的訊號被章節 <em>跨 case 合成</em> 抽象為更高層 frame、frame 本身不在任一 case 原文</li>
</ul>
<p><strong>實證</strong>（07 batch 1 reviewer B 抓的 2 個 high issue）：</p>
<ul>
<li>7.7 跨工具回查壓力：Uber 失效控制面寫「告警串接不足」、Slack 寫「訊號未匯流」— 都是單工具內訊號、章節合成「跨工具回查」axis</li>
<li>7.7 平台責任切分：SolarWinds 失效控制面寫「更新來源信任過於單點」「行為監測難以區分合法元件」— 都是供應鏈信任議題、章節合成「平台 vs 產品 audit 責任分離」frame</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>段落把多 case 抽象為更高層 frame 時、要 explicit 標明「frame 是本章合成、case 原文沒有此 frame」</li>
<li>修法範例：「兩個案例分別在 X 層揭露同類失效訊號 — A case 標明 B、C case 標明 D。本章把兩者抽象為『XXX』是 YYY 視角的合成 frame、非 case 原文框架。」</li>
<li>Stage 3 reviewer B prompt 要明示「跨 case 合成 frame 必須標為本章合成」是 high 級 issue 抓取項</li>
</ul>
<h3 id="陷阱-9case-引用句構同質化07-模組新發現">陷阱 9：Case 引用句構同質化（07 模組新發現）</h3>
<p>即使遵守 case 引用三段式紀律、跨章節 case 引用仍會出現句構同質化。13 處 case 引用 11 處用同一句構「揭露 N 層失效控制面 — A、B、C。案例『可落地檢查點』標明 mechanism 為 X、前提是 Y」。讀者跨章連讀時、會把 case 引用當儀式而非論證。</p>
<p><strong>實證</strong>：07 batch 1 reviewer A 抓出 systemic medium issue (Issue 8.1)、13 段 case 引用 11 段用相同句構。Stage 5 polish pass 主動分流 4 處後狀況改善。</p>
<p><strong>防範</strong>：</p>
<ul>
<li>句構選擇要 <em>跟著 case 類型走</em>、不是隨機變化（case 直接列 N mechanism → 「揭露 N 層」；case 揭露單一壓力 → 「補的失效訊號是 X」；case 揭露對比 → 「揭露兩個層次的對照」）</li>
<li>Stage 5 polish pass 加句構分流為標準工序之一（跟負向骨架同層級）</li>
<li>自掃描 regex <code>^對應 \[</code> 抓不到此類問題（這是符合三段式的引用、只是句構單一）、要靠 stage 5 主動 scan：<code>rg -c &quot;揭露[^。]*失效控制面&quot; &lt;module-paths&gt;</code> 看同句構出現次數、超過 5 處要分流</li>
</ul>
<h3 id="章節已有-routing-skeleton的特殊處理07-模組新發現">「章節已有 routing skeleton」的特殊處理（07 模組新發現）</h3>
<p>07 模組跟 06 / 09 不同之處：章節在 stage 2 前已有完整 routing layer 結構（threat scope / 從本章到實作 / 問題節點表 / 風險邊界 / 案例觸發 / 路由）— stage 2 是在現有結構內補 case-driven 深化段，而非空白擴章。</p>
<p>這個情境下：</p>
<ul>
<li><strong>SSoT 衝突更容易發生</strong>：新段落要跟既有章節結構協調、不只是新增內容。07 batch 1 三個 H issue（C-H1/H2/H3）都是 frame 跟既有章節 / 其他章節新增段衝突</li>
<li><strong>章節寫作邊界要先確認</strong>：補強段聚焦在「現有問題節點表的 mechanism 深化」、不擴成厚重 case-driven 章節（避免章節結構失衡）</li>
<li><strong>Cross-link 密度顯著上升</strong>：補強段要明示「本節聚焦 X 視角、canonical 在 Y 章」、否則 reviewer C 會抓 frame 重複展開</li>
</ul>
<p>判讀條件：</p>
<ul>
<li>章節已有 threat scope / 問題節點表 / 案例觸發段 → 走「補強段」策略、不空白擴章</li>
<li>章節是 routing layer / 導讀性質、不適合 case-driven 深化 → 跳過本流程</li>
<li>章節有 case 庫但 case 主要是 skeleton 型（30 行 frame） → 補強段嚴守「揭露 X 方向、通用補充」紀律、不擴寫實作層</li>
</ul>
<h3 id="衍生-insightreviewer-維度沒覆蓋的部分">衍生 insight：reviewer 維度沒覆蓋的部分</h3>
<p>3 個模組跑下來、發現現有 3 reviewer 維度（規範 / 案例準確性 / 跨章一致性）有未覆蓋的問題：</p>
<ul>
<li><strong>教學性 / 讀者路徑</strong>：章節之間的閱讀順序是否合理？讀者讀完 A 章能不能銜接 B 章？目前沒 reviewer 檢查</li>
<li><strong>判讀條件可操作性</strong>：寫了判讀訊號、但實際工程師能不能用這些訊號做決策？沒 reviewer 驗證</li>
<li><strong>實作可行性</strong>：建議的設計是否真的能落地？跨團隊協調是否現實？需要懂業務的 reviewer</li>
</ul>
<p>未來 6 / 7 / 8 模組執行時、可以考慮加第 4 個 reviewer 維度（教學性 + 實作可行性）。</p>
<h2 id="跟其他寫作流程的差異">跟其他寫作流程的差異</h2>
<p>跟「LLM 自生 + 人工 review」比、本流程的差異：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>LLM 自生 + 人工 review</th>
          <th>Case-first + Agent team</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope 來源</td>
          <td>訓練資料 + 提示詞</td>
          <td>真實案例 findings</td>
      </tr>
      <tr>
          <td>準確性檢查</td>
          <td>人工讀完對比</td>
          <td>Case fidelity reviewer 自動對照</td>
      </tr>
      <tr>
          <td>規範遵循</td>
          <td>人工 checklist</td>
          <td>Standards reviewer 自動掃描</td>
      </tr>
      <tr>
          <td>跨章一致性</td>
          <td>人工跨檔 grep</td>
          <td>Consistency reviewer 自動檢查</td>
      </tr>
      <tr>
          <td>Context 成本</td>
          <td>低（人工不佔 LLM context）</td>
          <td>中（reviewer 各自佔自己 context、主 context 輕）</td>
      </tr>
      <tr>
          <td>時間成本</td>
          <td>高（人工逐段讀）</td>
          <td>中（reviewer 平行）</td>
      </tr>
      <tr>
          <td>真實事故揭露</td>
          <td>受限於 reviewer 經驗</td>
          <td>受限於案例庫覆蓋</td>
      </tr>
  </tbody>
</table>
<p>跟「LLM 自生 + 自我 review」比：</p>
<ul>
<li>自我 review 抓不到自生內容的盲點（self-blindness）</li>
<li>Agent team 是 <em>不同 instance</em>、不共享 context、能扮演獨立 reviewer</li>
</ul>
<h2 id="下一步">下一步</h2>
<p>本流程在 backend/01 至 backend/07 batch 1 七個模組驗證後（共 ~45 章 / 385 review issue / case fidelity 70-93% 區間）、方法論已工具化為 <code>case-first-module-workflow</code> skill（內部檔 <code>.claude/skills/case-first-module-workflow/</code>、含 stage 1-5 流程、reviewer prompt template、self-scan regex 跟 5 個原則卡）、後續套用到：</p>
<ul>
<li>backend/07 batch 2 LLM 安全：case 庫缺位（OWASP LLM Top 10 + agent injection 公開事件未累積成模組 case）、要先建 LLM case 庫再走 case-first</li>
<li>backend/07 batch 3 治理章節：routing 層 / 導讀性質、case-driven 深化適用度低、做標準 polish pass 即可</li>
<li>backend/08 incident response：跟 04 / 06 / 07 cross-link 密度最高、SSoT 對應規劃壓力最大</li>
<li>其他模組依此類推</li>
</ul>
<p>06 模組是首次套用工具化 skill 的模組、驗證 skill 對 stage 1-2 加速有效、但 reviewer A 仍抓出 45 issue（高於 05 之前 baseline 20-30、推動 v1.2 把 standards reviewer baseline 擴大到 20-45）— 揭露 skill 改進方向（self-scan regex 需要持續演進、case 引用段首結構是 LLM 系統性傾向）。</p>
<p>07 batch 1 驗證下「章節已有 routing skeleton」情境的處理策略：補強段不擴成厚重 case-driven 章節、聚焦 mechanism 深化 + cross-link 對齊。揭露兩個新陷阱（跨 case 合成 frame 升級成 case 揭露、case 引用句構同質化）、補進 skill 跟方法論。</p>
<p>流程本身會在每個模組後 retrospective、看 reviewer 維度是否該調整、findings 抽取方法是否該強化、polish pass 處理 pattern 是否該擴充。目前已知改進方向：</p>
<ul>
<li>加 reviewer：教學性審查（讀者路徑是否清楚、判讀順序是否合理）</li>
<li>強化 findings 抽取：標註 finding 的 <em>泛化程度</em>、避免把 case-specific 細節推為通用結論</li>
<li>Rich / Medium case 引用紀律：把「fact vs derive」分層 + 「mechanism 名稱精準引用」寫進 stage 1 抽 findings 模板、stage 3 case fidelity reviewer prompt 也明示此分界</li>
<li>自掃描 regex 持續演進：每個模組 reviewer 抓出新 pattern 後、回頭加進 self-scan 工具、避免在下個模組重蹈覆轍。06 模組後加 <code>^對應 \[</code> 抓段首 case 引用框架。07 模組後標明 <code>^對應 \[</code> 在三段分離結構下會 false positive、要靠 awk 看 prev line context</li>
<li>Case 引用三段式：把「概念定義 → case 引用 → 通用展開」當段落結構紀律、避免段首被 case 引用取代（06 模組最大宗 systemic 違規）</li>
<li>Case 引用句構分流：07 模組後 stage 5 polish pass 加句構分流為標準工序、避免跨章 13+ 段同句構讀感儀式化</li>
<li>跨 case 合成 frame 紀律：07 模組後 reviewer B prompt 明示「跨 case 合成 frame 必須標為本章合成」是 high 級 issue</li>
<li>加修正後自動 lint：修完不只跑 mdtools、加跑「找首句否定句」「找表格沒延伸」「找模板化並列點」「找段首 case 引用」的自動掃描</li>
</ul>
<p>跟其他寫作協議的整合：本流程跟 <code>compositional-writing</code> skill 互補（後者管 <em>單篇</em> 寫作的原子化跟意圖、本流程管 <em>跨章模組</em> 的 scope 跟一致性）、跟 <code>requirement-protocol</code> skill 互補（後者管 <em>對話協議</em>、本流程管 <em>內容生產</em>）。</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>Mermaid gitGraph：自訂 commit type 顏色不渲染的配置補洞</title><link>https://tarrragon.github.io/blog/posts/mermaid_gitgraph_type_color_config/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/mermaid_gitgraph_type_color_config/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>背景&lt;/strong>：本文是「Hugo 部落格支援 Mermaid 流程圖完整實現指南」的補洞紀錄。原指南建立了 Mermaid 整合的基礎、但 gitGraph 的自訂 commit type 顏色設定沒包含進去、文章用到才發現渲染失效。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>部落格用 Hugo + Mermaid 10.6.1 畫 gitGraph，文章裡寫了 &lt;code>type: HIGHLIGHT&lt;/code> 和 &lt;code>type: REVERSE&lt;/code> 想標出特定 commit、但渲染出來全部是預設灰色、type 標記沒生效。&lt;/p>





&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">gitGraph
 commit id: &amp;#34;A&amp;#34; type: HIGHLIGHT
 commit id: &amp;#34;C&amp;#34; type: REVERSE&lt;/code>&lt;/pre>&lt;p>期望：HIGHLIGHT 綠色、REVERSE 紅色
實際：兩個都跟普通 commit 一樣灰&lt;/p>
&lt;hr>
&lt;h2 id="根本原因">根本原因&lt;/h2>
&lt;p>&lt;code>layouts/partials/custom_head.html&lt;/code> 的 &lt;code>mermaid.initialize()&lt;/code> 裡 &lt;code>themeVariables&lt;/code> 只設了通用顏色（&lt;code>primaryColor&lt;/code>、&lt;code>secondaryColor&lt;/code> 等）、沒給 gitGraph 專用的顏色變數。Mermaid 找不到 HIGHLIGHT / REVERSE 對應的顏色就 fallback 到預設值。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 原本的配置（不完整）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">themeVariables&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">primaryColor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#2d3748&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">primaryTextColor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#2d3748&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">primaryBorderColor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#4a5568&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">lineColor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#4a5568&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">secondaryColor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#e2e8f0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">tertiaryColor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#f7fafc&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 缺：git0 / git1 / git2 等 gitGraph 變數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mermaid 的 themeVariables 對不同圖表類型有不同的命名空間 — flowchart 用 &lt;code>primaryColor&lt;/code>、gitGraph 用 &lt;code>git0&lt;/code> / &lt;code>git1&lt;/code> / &lt;code>git2&lt;/code>。原本的配置只覆蓋到通用 / flowchart 的命名空間。&lt;/p>
&lt;hr>
&lt;h2 id="解法">解法&lt;/h2>
&lt;p>兩層補洞：JS 層補 themeVariables、CSS 層補 selector 規則做雙保險。&lt;/p>
&lt;h3 id="1-themevariables-加-gitgraph-顏色">1. themeVariables 加 gitGraph 顏色&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">themeVariables&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 原本的通用顏色保留
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 補 gitGraph 顏色
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">git0&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#90ee90&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// HIGHLIGHT
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">git1&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#ffb6c6&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// REVERSE
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">git2&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#4a5568&amp;#39;&lt;/span> &lt;span class="c1">// 其他
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-css-selector-補強">2. CSS selector 補強&lt;/h3>
&lt;p>光靠 themeVariables 在某些 Mermaid 版本仍不穩定（命名規則隨版本改變、見下方注意事項）、加 CSS 直接針對渲染後的 SVG 元素：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">mermaid&lt;/span> &lt;span class="nt">svg&lt;/span> &lt;span class="o">[&lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="o">$=&lt;/span>&lt;span class="s2">&amp;#34;_HIGHLIGHT&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="nt">circle&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">fill&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#90ee90&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">stroke&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#2d7a2d&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&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 class="p">.&lt;/span>&lt;span class="nc">mermaid&lt;/span> &lt;span class="nt">svg&lt;/span> &lt;span class="o">[&lt;/span>&lt;span class="nt">id&lt;/span>&lt;span class="o">$=&lt;/span>&lt;span class="s2">&amp;#34;_REVERSE&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="nt">circle&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="n">fill&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#ffb6c6&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="n">stroke&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#d32f2f&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mermaid 渲染 gitGraph 時、會在每個 commit 的 SVG node 加上 &lt;code>id=&amp;quot;..._HIGHLIGHT&amp;quot;&lt;/code> / &lt;code>id=&amp;quot;..._REVERSE&amp;quot;&lt;/code>、用 attribute selector &lt;code>[id$=&amp;quot;_TYPENAME&amp;quot;]&lt;/code> 命中。&lt;/p>
&lt;hr>
&lt;h2 id="注意事項">注意事項&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>顏色變數命名隨 Mermaid 版本變動&lt;/strong>：10.6.1 用 &lt;code>git0&lt;/code> / &lt;code>git1&lt;/code> / &lt;code>git2&lt;/code>、更早版本可能是 &lt;code>gitInv0&lt;/code> / &lt;code>gitInv1&lt;/code>。升級 Mermaid 版本時要驗證一次顏色設定還生效。&lt;/li>
&lt;li>&lt;strong>CSS selector 是防禦性的&lt;/strong>：themeVariables 配對的話 CSS 不會生效、但 themeVariables 失靈時 CSS 接住。雙保險、不重複設值。&lt;/li>
&lt;li>&lt;strong>這篇只解決「顏色沒出來」這個視覺問題&lt;/strong>：寫文章引用 gitGraph 的過程中還發現另一個議題 —「用 emoji 圖例區分 HIGHLIGHT / REVERSE」本身是語意混淆、不是視覺問題、修 CSS 解不了。那個議題見 &lt;a href="https://tarrragon.github.io/blog/report/visual-tool-error-layer-alignment/" data-link-title="視覺手段對齊錯誤層次：CSS / emoji 修不到語意 / 邏輯問題" data-link-desc="修視覺問題的工具（CSS、emoji、顏色、排版）只能擋視覺層、不能修語意 / 邏輯層。把語意 / 邏輯問題當成視覺問題修 = 蓋住症狀根因不動 &amp;#43; false confidence、跟 #82 用 hook 蓋行為錯誤同骨。三層優先序：邏輯 → 語意 → 視覺、修法從深層往淺層走、不從症狀往回推。本卡是 #82 在「呈現層」的具體實例、是 #83 multi-pass review 缺的 vertical 軸。">report #92 視覺手段對齊錯誤層次&lt;/a>。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="驗證">驗證&lt;/h2>
&lt;p>修改後在本地 Hugo dev server 預覽包含 gitGraph 的文章、確認：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>背景</strong>：本文是「Hugo 部落格支援 Mermaid 流程圖完整實現指南」的補洞紀錄。原指南建立了 Mermaid 整合的基礎、但 gitGraph 的自訂 commit type 顏色設定沒包含進去、文章用到才發現渲染失效。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>部落格用 Hugo + Mermaid 10.6.1 畫 gitGraph，文章裡寫了 <code>type: HIGHLIGHT</code> 和 <code>type: REVERSE</code> 想標出特定 commit、但渲染出來全部是預設灰色、type 標記沒生效。</p>





<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">gitGraph
   commit id: &#34;A&#34; type: HIGHLIGHT
   commit id: &#34;C&#34; type: REVERSE</code></pre><p>期望：HIGHLIGHT 綠色、REVERSE 紅色
實際：兩個都跟普通 commit 一樣灰</p>
<hr>
<h2 id="根本原因">根本原因</h2>
<p><code>layouts/partials/custom_head.html</code> 的 <code>mermaid.initialize()</code> 裡 <code>themeVariables</code> 只設了通用顏色（<code>primaryColor</code>、<code>secondaryColor</code> 等）、沒給 gitGraph 專用的顏色變數。Mermaid 找不到 HIGHLIGHT / REVERSE 對應的顏色就 fallback 到預設值。</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="c1">// 原本的配置（不完整）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">themeVariables</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">primaryColor</span><span class="o">:</span> <span class="s1">&#39;#2d3748&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">primaryTextColor</span><span class="o">:</span> <span class="s1">&#39;#2d3748&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">primaryBorderColor</span><span class="o">:</span> <span class="s1">&#39;#4a5568&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nx">lineColor</span><span class="o">:</span> <span class="s1">&#39;#4a5568&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nx">secondaryColor</span><span class="o">:</span> <span class="s1">&#39;#e2e8f0&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">tertiaryColor</span><span class="o">:</span> <span class="s1">&#39;#f7fafc&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// 缺：git0 / git1 / git2 等 gitGraph 變數
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>Mermaid 的 themeVariables 對不同圖表類型有不同的命名空間 — flowchart 用 <code>primaryColor</code>、gitGraph 用 <code>git0</code> / <code>git1</code> / <code>git2</code>。原本的配置只覆蓋到通用 / flowchart 的命名空間。</p>
<hr>
<h2 id="解法">解法</h2>
<p>兩層補洞：JS 層補 themeVariables、CSS 層補 selector 規則做雙保險。</p>
<h3 id="1-themevariables-加-gitgraph-顏色">1. themeVariables 加 gitGraph 顏色</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">themeVariables</span><span class="o">:</span> <span class="p">{</span>
</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"><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></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// 補 gitGraph 顏色
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">git0</span><span class="o">:</span> <span class="s1">&#39;#90ee90&#39;</span><span class="p">,</span>    <span class="c1">// HIGHLIGHT
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nx">git1</span><span class="o">:</span> <span class="s1">&#39;#ffb6c6&#39;</span><span class="p">,</span>    <span class="c1">// REVERSE
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>  <span class="nx">git2</span><span class="o">:</span> <span class="s1">&#39;#4a5568&#39;</span>     <span class="c1">// 其他
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><h3 id="2-css-selector-補強">2. CSS selector 補強</h3>
<p>光靠 themeVariables 在某些 Mermaid 版本仍不穩定（命名規則隨版本改變、見下方注意事項）、加 CSS 直接針對渲染後的 SVG 元素：</p>





<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="nt">svg</span> <span class="o">[</span><span class="nt">id</span><span class="o">$=</span><span class="s2">&#34;_HIGHLIGHT&#34;</span><span class="o">]</span> <span class="nt">circle</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">fill</span><span class="p">:</span> <span class="mh">#90ee90</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">stroke</span><span class="p">:</span> <span class="mh">#2d7a2d</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="o">[</span><span class="nt">id</span><span class="o">$=</span><span class="s2">&#34;_REVERSE&#34;</span><span class="o">]</span> <span class="nt">circle</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="n">fill</span><span class="p">:</span> <span class="mh">#ffb6c6</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="n">stroke</span><span class="p">:</span> <span class="mh">#d32f2f</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><p>Mermaid 渲染 gitGraph 時、會在每個 commit 的 SVG node 加上 <code>id=&quot;..._HIGHLIGHT&quot;</code> / <code>id=&quot;..._REVERSE&quot;</code>、用 attribute selector <code>[id$=&quot;_TYPENAME&quot;]</code> 命中。</p>
<hr>
<h2 id="注意事項">注意事項</h2>
<ul>
<li><strong>顏色變數命名隨 Mermaid 版本變動</strong>：10.6.1 用 <code>git0</code> / <code>git1</code> / <code>git2</code>、更早版本可能是 <code>gitInv0</code> / <code>gitInv1</code>。升級 Mermaid 版本時要驗證一次顏色設定還生效。</li>
<li><strong>CSS selector 是防禦性的</strong>：themeVariables 配對的話 CSS 不會生效、但 themeVariables 失靈時 CSS 接住。雙保險、不重複設值。</li>
<li><strong>這篇只解決「顏色沒出來」這個視覺問題</strong>：寫文章引用 gitGraph 的過程中還發現另一個議題 —「用 emoji 圖例區分 HIGHLIGHT / REVERSE」本身是語意混淆、不是視覺問題、修 CSS 解不了。那個議題見 <a href="/blog/report/visual-tool-error-layer-alignment/" data-link-title="視覺手段對齊錯誤層次：CSS / emoji 修不到語意 / 邏輯問題" data-link-desc="修視覺問題的工具（CSS、emoji、顏色、排版）只能擋視覺層、不能修語意 / 邏輯層。把語意 / 邏輯問題當成視覺問題修 = 蓋住症狀根因不動 &#43; false confidence、跟 #82 用 hook 蓋行為錯誤同骨。三層優先序：邏輯 → 語意 → 視覺、修法從深層往淺層走、不從症狀往回推。本卡是 #82 在「呈現層」的具體實例、是 #83 multi-pass review 缺的 vertical 軸。">report #92 視覺手段對齊錯誤層次</a>。</li>
</ul>
<hr>
<h2 id="驗證">驗證</h2>
<p>修改後在本地 Hugo dev server 預覽包含 gitGraph 的文章、確認：</p>
<ul>
<li>HIGHLIGHT 的 commit circle 顯示綠色</li>
<li>REVERSE 的 commit circle 顯示紅色</li>
<li>沒有 type 的 commit 維持預設灰色</li>
</ul>
]]></content:encoded></item><item><title>Cards-Skills 系統的活案例：從一個 search bug 到 14 張新卡的閉環</title><link>https://tarrragon.github.io/blog/posts/cards-skills-%E7%B3%BB%E7%B5%B1%E7%9A%84%E6%B4%BB%E6%A1%88%E4%BE%8B%E5%BE%9E%E4%B8%80%E5%80%8B-search-bug-%E5%88%B0-14-%E5%BC%B5%E6%96%B0%E5%8D%A1%E7%9A%84%E9%96%89%E7%92%B0/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/cards-skills-%E7%B3%BB%E7%B5%B1%E7%9A%84%E6%B4%BB%E6%A1%88%E4%BE%8B%E5%BE%9E%E4%B8%80%E5%80%8B-search-bug-%E5%88%B0-14-%E5%BC%B5%E6%96%B0%E5%8D%A1%E7%9A%84%E9%96%89%E7%92%B0/</guid><description>&lt;h2 id="這篇要說什麼">這篇要說什麼&lt;/h2>
&lt;p>&lt;code>content/report/&lt;/code> 累積了 70+ 張原子化事後檢討卡片、&lt;code>.claude/skills/&lt;/code> 收錄三個 protocol skill。這些是用來指導下一輪實作、又會被下一輪實作的學習回流修正的活基礎建設。&lt;/p>
&lt;p>本文把這套系統實際跑一輪的歷程紀錄下來、當未來「想用這套系統的人」的 onboarding case study。主軸是修一個 search filter bug — 看似一週工作、實際走完八輪迭代、產出 14 張新卡片 + 兩個 skill 的 v0.2 + 4 個 CI test、過程中還抓到自己的 dogfooding 失敗、回頭修一次。&lt;/p>
&lt;hr>
&lt;h2 id="起點使用者問題">起點：使用者問題&lt;/h2>
&lt;p>&amp;ldquo;我們搜尋頁的 標題/內文篩選功能現在雖然做出來了、但是還是有一個很嚴重的 BUG&amp;rdquo;&lt;/p>
&lt;p>具體：Pagefind 分批 load、view 層 post-filter；切到 title-only 後、第二批 load more 的 8 筆全部 title 不含 query → 全 hidden、畫面閃但內容沒變、使用者看到「load more 沒效果」silent 失敗。&lt;/p>
&lt;p>User 還明確補了一句：「&lt;strong>所以除了用 JS 取巧解決畫面、但是實際功能面上怎麼配合跟實作 我們並沒有解決&lt;/strong>」— 這已經點到核心：問題不在畫面、在抽象層。&lt;/p>
&lt;hr>
&lt;h2 id="第一輪拆卡片之前先想清楚">第一輪：拆卡片之前先想清楚&lt;/h2>
&lt;p>直接修 bug 是可選但不是 user 要的。User 強調：「&lt;strong>先思考我的需求、然後思考各種狀況的邊界&lt;/strong>」。&lt;/p>
&lt;p>依當時的兩個 skill — &lt;code>requirement-protocol&lt;/code>（對話協議）跟 &lt;code>frontend-with-playwright&lt;/code>（前端執行協議）— 把問題分解：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Bug 的結構性根因&lt;/strong>：filter 寫在視覺層、source 在資料層分批、兩層的「一筆」定義不一致 → silent 缺口&lt;/li>
&lt;li>&lt;strong>解法策略空間&lt;/strong>：5 個合理選項（推進 query / 自動續抓 / 多 index / 誠實 UX / 明示縮小）— 每個機會成本不同&lt;/li>
&lt;li>&lt;strong>跨領域通用性&lt;/strong>：這結構不只前端有 — 後端 middleware filter、map-reduce、SQL view 都同模式&lt;/li>
&lt;/ol>
&lt;p>User 的關鍵回應：「&lt;strong>這部份可以補充 SKILL 中演算法不足的原因 &amp;hellip; 卡片是經過多次迭代、擴充、然後分拆、再擴充、最後做連結&lt;/strong>」。&lt;/p>
&lt;p>明確了協作方式：先建卡片、再灌進 skill、最後才修。卡片本身要走原子化拆解 → 補充 → 反向擴充 → 連結的多輪迭代。&lt;/p>
&lt;hr>
&lt;h2 id="14-張卡片的拆解第一冷啟">14 張卡片的拆解（第一冷啟）&lt;/h2>
&lt;p>依 user 對 atomic 的標準（一卡一議題、一個議題多面向 OK、議題太多就拆），列出 10 張卡片提案：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分組&lt;/th>
 &lt;th>卡片&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>問題分析&lt;/td>
 &lt;td>#55 層錯位 / #56 視覺完成 ≠ 功能完成 / #57 三狀態區分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>指令澄清&lt;/td>
 &lt;td>#58 篩選類指令的澄清時機&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>解法策略&lt;/td>
 &lt;td>#59 五策略對照 + #60-62 三張 pattern 卡（自動續抓 / 推進 query / 誠實 UX）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>抽象原則&lt;/td>
 &lt;td>#63 資料源形狀 / #64 同層合成&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>冷啟版本一次寫完不求完美 — 約 1700 行、各卡 self-contained。&lt;/p>
&lt;hr>
&lt;h2 id="七輪迭代">七輪迭代&lt;/h2>
&lt;h3 id="迭代-1抽-pattern--瘦身">迭代 1：抽 Pattern + 瘦身&lt;/h3>
&lt;p>寫完 #59 五策略後、發現 A/B/C/D/E 中 C（多 index）、E（明示縮小）沒對應 pattern 卡。抽出 #65 / #66 補完 pattern 卡組。同時瘦身 #59 → 純路由（細節留 pattern 卡）、#55 + #57 移除跟 #63 重複的「四類資料源」段。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要說什麼">這篇要說什麼</h2>
<p><code>content/report/</code> 累積了 70+ 張原子化事後檢討卡片、<code>.claude/skills/</code> 收錄三個 protocol skill。這些是用來指導下一輪實作、又會被下一輪實作的學習回流修正的活基礎建設。</p>
<p>本文把這套系統實際跑一輪的歷程紀錄下來、當未來「想用這套系統的人」的 onboarding case study。主軸是修一個 search filter bug — 看似一週工作、實際走完八輪迭代、產出 14 張新卡片 + 兩個 skill 的 v0.2 + 4 個 CI test、過程中還抓到自己的 dogfooding 失敗、回頭修一次。</p>
<hr>
<h2 id="起點使用者問題">起點：使用者問題</h2>
<p>&ldquo;我們搜尋頁的 標題/內文篩選功能現在雖然做出來了、但是還是有一個很嚴重的 BUG&rdquo;</p>
<p>具體：Pagefind 分批 load、view 層 post-filter；切到 title-only 後、第二批 load more 的 8 筆全部 title 不含 query → 全 hidden、畫面閃但內容沒變、使用者看到「load more 沒效果」silent 失敗。</p>
<p>User 還明確補了一句：「<strong>所以除了用 JS 取巧解決畫面、但是實際功能面上怎麼配合跟實作 我們並沒有解決</strong>」— 這已經點到核心：問題不在畫面、在抽象層。</p>
<hr>
<h2 id="第一輪拆卡片之前先想清楚">第一輪：拆卡片之前先想清楚</h2>
<p>直接修 bug 是可選但不是 user 要的。User 強調：「<strong>先思考我的需求、然後思考各種狀況的邊界</strong>」。</p>
<p>依當時的兩個 skill — <code>requirement-protocol</code>（對話協議）跟 <code>frontend-with-playwright</code>（前端執行協議）— 把問題分解：</p>
<ol>
<li><strong>Bug 的結構性根因</strong>：filter 寫在視覺層、source 在資料層分批、兩層的「一筆」定義不一致 → silent 缺口</li>
<li><strong>解法策略空間</strong>：5 個合理選項（推進 query / 自動續抓 / 多 index / 誠實 UX / 明示縮小）— 每個機會成本不同</li>
<li><strong>跨領域通用性</strong>：這結構不只前端有 — 後端 middleware filter、map-reduce、SQL view 都同模式</li>
</ol>
<p>User 的關鍵回應：「<strong>這部份可以補充 SKILL 中演算法不足的原因 &hellip; 卡片是經過多次迭代、擴充、然後分拆、再擴充、最後做連結</strong>」。</p>
<p>明確了協作方式：先建卡片、再灌進 skill、最後才修。卡片本身要走原子化拆解 → 補充 → 反向擴充 → 連結的多輪迭代。</p>
<hr>
<h2 id="14-張卡片的拆解第一冷啟">14 張卡片的拆解（第一冷啟）</h2>
<p>依 user 對 atomic 的標準（一卡一議題、一個議題多面向 OK、議題太多就拆），列出 10 張卡片提案：</p>
<table>
  <thead>
      <tr>
          <th>分組</th>
          <th>卡片</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>問題分析</td>
          <td>#55 層錯位 / #56 視覺完成 ≠ 功能完成 / #57 三狀態區分</td>
      </tr>
      <tr>
          <td>指令澄清</td>
          <td>#58 篩選類指令的澄清時機</td>
      </tr>
      <tr>
          <td>解法策略</td>
          <td>#59 五策略對照 + #60-62 三張 pattern 卡（自動續抓 / 推進 query / 誠實 UX）</td>
      </tr>
      <tr>
          <td>抽象原則</td>
          <td>#63 資料源形狀 / #64 同層合成</td>
      </tr>
  </tbody>
</table>
<p>冷啟版本一次寫完不求完美 — 約 1700 行、各卡 self-contained。</p>
<hr>
<h2 id="七輪迭代">七輪迭代</h2>
<h3 id="迭代-1抽-pattern--瘦身">迭代 1：抽 Pattern + 瘦身</h3>
<p>寫完 #59 五策略後、發現 A/B/C/D/E 中 C（多 index）、E（明示縮小）沒對應 pattern 卡。抽出 #65 / #66 補完 pattern 卡組。同時瘦身 #59 → 純路由（細節留 pattern 卡）、#55 + #57 移除跟 #63 重複的「四類資料源」段。</p>
<h3 id="迭代-2補概念深度">迭代 2：補概念深度</h3>
<p>回頭讀 #56 / #63 / #64、補抽象層的「為什麼」：</p>
<ul>
<li>#56 加「驗收的時間軸：四個 checkpoint」概念</li>
<li>#63 加「形狀識別 protocol」+「形狀混合」+「形狀的可改造性」</li>
<li>#64 加「跨領域通用的本質 = 資訊可見範圍」+「上推代價」</li>
</ul>
<h3 id="迭代-3跨卡連結">迭代 3：跨卡連結</h3>
<p>新卡跟 #1-#54 既有卡互相補連結。例如 #55 ↔ #11 playwright、#57 ↔ #38 aria-live、#58 ↔ #21 decide-vs-confirm、#64 ↔ #43 minimum-scope + #44 SSOT。整個 collection 從兩個獨立輪次變一張互連網。</p>
<h3 id="迭代-4抽更高層原則">迭代 4：抽更高層原則</h3>
<p>重讀新卡發現兩個議題夠 abstract、值得抽獨立卡：</p>
<ul>
<li><strong>#67 寫作便利度跟意圖對齊反相關</strong> — 從「為什麼層錯位 bug 容易寫出來」抽出。發現它是 #43 / #44 / #45 / #64 的共同上位原則：<strong>便利位置 vs 對齊位置永遠反相關</strong></li>
<li><strong>#68 驗收的時間軸：四個 checkpoint</strong> — 從 #56 抽出獨立成卡</li>
</ul>
<h3 id="迭代-5跨輪共骨">迭代 5：跨輪共骨</h3>
<p>系統性掃 #1-#54 找跟新系列共骨的、加連結。例：#6 filter-order ↔ #58 / #59、#10 placeholder ↔ #68、#15 layout-test ↔ #68、#14 selector / #20 failure / #28 class-toggle ↔ #67。</p>
<h3 id="迭代-66768-加深">迭代 6：#67/#68 加深</h3>
<p>再讀兩張抽象卡、補「為什麼人會違反這條規則」的結構性解釋：</p>
<ul>
<li>#67 加「便利度的時間維度：當下便利 vs 未來便利反向」+「我等下會 refactor 是個謊言」</li>
<li>#68 加「為什麼 Ship 前 checkpoint 最常被跳過」（沒便利路徑）+「瀑布原則：漏一層代價指數放大」</li>
</ul>
<p>從「規則陳述」進到「結構性解釋」 — 不只說「該怎麼做」、也說「為什麼人會違反」。</p>
<h3 id="迭代-7compositional-writing-規範稽核">迭代 7：compositional-writing 規範稽核</h3>
<p>User 提醒「再做一次 compositional-writing 的檢查」。發現兩類違規：</p>
<ol>
<li><strong>Rule 7 違規</strong>：26 處「X 才合理的情境：實務上幾乎不存在」假反模式 — 改成「X 是反模式：理由」格式</li>
<li><strong>結構違規</strong>：#67/#68 是抽象層原則卡、不該寫設計取捨 ABCD（情境檢討卡的格式）— 改成「不該套用本原則的情境」（適用邊界）</li>
</ol>
<p>修完 31 張卡片（含既有 #1-#54）。整個 collection 對齊 v0.6 規範。</p>
<hr>
<h2 id="灌進-skills">灌進 Skills</h2>
<p>把 #55-#68 系列接進兩個 skill：</p>
<ul>
<li><strong>requirement-protocol v0.2</strong>：clarifying-ambiguous-instructions 加第 5 類「篩選類」+ 三問模板（呼應 #58）；SKILL.md 加「相關抽象層原則」段路由 #42-45 + #67-68</li>
<li><strong>frontend-with-playwright v0.2</strong>：新增第 7 份 reference <code>data-flow-and-filter-composition</code>（涵蓋 #55-#66 跨領域範例）；強調「不只前端、適用後端 / 演算法 / DB」</li>
</ul>
<p>Skill 的角色 = 路由器、Reports = 深度內容 — 兩層分工不重述。</p>
<hr>
<h2 id="實作策略-c--phase-1-4">實作：策略 C + Phase 1-4</h2>
<p>依 #59 + Pagefind 1.5.2 capabilities：</p>
<ul>
<li><strong>A 推進 query</strong>：不可行（Pagefind 無 native title filter API）</li>
<li><strong>C 多 index</strong>：採用（最對齊意圖）</li>
<li>B / D / E 是 fallback</li>
</ul>
<p>Phase 1-4：</p>
<ol>
<li>Makefile 跑 3 輪 pagefind（all / title / content）</li>
<li>single.html <code>&lt;content&gt;</code> → <code>&lt;div class=&quot;article-body&quot; data-pagefind-body&gt;</code></li>
<li>search.html 移除 view 層 post-filter、改 destroy + new PagefindUI(bundlePath)</li>
<li>4 個 Playwright tests 固化</li>
</ol>
<p>跑出來：<code>make site</code> 三 index 成功、<code>make test</code> 4/4 PASS、live 驗證 sparse case 顯示 explicit empty。<strong>看起來完工</strong>。</p>
<hr>
<h2 id="user-抓到-dogfooding-失敗--第-8-輪">User 抓到 dogfooding 失敗 — 第 8 輪</h2>
<p>User 問：「<strong>剛剛的過程我不確定、你開始修改之前有先寫測試確保符合預測狀態、然後才調整嗎？</strong>」</p>
<p>沒有。流程是：先修 → 才補測試 → 4/4 GREEN。<strong>沒走 RED</strong>。</p>
<p>這是 #67「便利驅動」+ #68「Checkpoint 2/3 內部協議」的 dogfooding 失敗。我寫了 #67/#68 教這些原則、自己卻違反。</p>
<p>依 user 規範：先建卡片再修。抽 <strong>#69 Test-First：先看到 RED 才相信 GREEN</strong>：</p>
<ul>
<li>測試本身是程式、會有 bug（5 種失敗模式）</li>
<li>沒看過 RED = 不知道測試有沒有 catch 能力</li>
<li>RED → GREEN 兩個訊號都看到 = 測試 + 修復都被驗證</li>
</ul>
<p>retrospective 補驗證流程：checkout pre-fix commit → cherry-pick test → build → run（看 RED）→ restore → run（看 GREEN）。</p>
<p>跑下去 — 結果震撼：<strong>4 個測試只有 1 個真的 catch 到 bug、其他 3 個對 buggy code 也 PASS</strong>（placebo）。如果不做 retrospective、會帶著 3/4 placebo 測試 ship。</p>
<p>強化測試（network-level + structural assertion 替換弱 invariant）：buggy code 1 PASS / 3 FAIL、fixed code 4 PASS。RED-GREEN 真的 catch 到 bug + 真的解掉。</p>
<hr>
<h2 id="user-抓到第二個-dogfooding-失敗--checkpoint-1">User 抓到第二個 dogfooding 失敗 — Checkpoint 1</h2>
<p>我問 user 還有什麼該迭代。User 列了 7 項、選 1+2：</p>
<ol>
<li>補 Checkpoint 1（列使用者意圖完整集）</li>
<li>跟 user 確認 known limitations</li>
</ol>
<p>跑 Checkpoint 1 retrospective — 用 Playwright MCP 系統性測 5 維度（data / interaction / URL / a11y / performance）。發現 3 個 silent 缺口：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>漏掉的 case</th>
          <th>結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>URL state</td>
          <td><code>?q=X&amp;scope=Y</code> 持久化</td>
          <td>完全沒實作</td>
      </tr>
      <tr>
          <td>A11y</td>
          <td>Tab order: scope 在 search input 之前</td>
          <td>反 mental model</td>
      </tr>
      <tr>
          <td>Filter UX</td>
          <td>type/tag filter 在 sub-mode 完全消失</td>
          <td>Silent 限制</td>
      </tr>
  </tbody>
</table>
<p>依 user 規範：<strong>先建卡片再修</strong>。抽：</p>
<ul>
<li><strong>#70 URL 是 stateful UI 的儲存層</strong> — 5 個儲存層特性對照 + 三問判準</li>
<li><strong>#71 Tab Order = DOM Order = Mental Model 三者對齊</strong> — DOM 順序 = tab 順序、不對齊時優先重排 DOM</li>
<li>更新 #68 加「為什麼 Checkpoint 1 也常被跳過」段、用本次任務當 self-case</li>
</ul>
<p>然後實作 — 依 #69 RED-GREEN 順序：</p>
<ol>
<li>寫 4 個 RED tests</li>
<li>跑 → 4 個 fail（confirms RED）</li>
<li>修 search.html（URL persist + DOM reorder + UI hint）</li>
<li>跑 → 8/8 GREEN</li>
</ol>
<hr>
<h2 id="ci--自動化">CI + 自動化</h2>
<p>最後補 CI 防護：</p>
<ul>
<li><strong><code>.github/workflows/playwright.yml</code></strong> — push / PR 自動跑 8 個 tests</li>
<li><strong><code>deploy.yml</code> 修 critical bug</strong> — production 一直只 build 單 index、現在 build 三份對齊本地</li>
<li><strong><code>make test</code> + <code>make verify-red-green PRE_FIX=&lt;sha&gt;</code></strong> — codify retrospective 流程、不需手動 stash / checkout / restore</li>
</ul>
<hr>
<h2 id="數字總結">數字總結</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commits</td>
          <td>30+</td>
      </tr>
      <tr>
          <td>新卡片</td>
          <td>17（#55-#71）</td>
      </tr>
      <tr>
          <td>既有卡修改</td>
          <td>31 張（rule 7 稽核）</td>
      </tr>
      <tr>
          <td>新 skill reference</td>
          <td>1（data-flow-and-filter-composition）</td>
      </tr>
      <tr>
          <td>Skill 版本</td>
          <td>requirement-protocol v0.1 → v0.2、frontend-with-playwright v0.1 → v0.2</td>
      </tr>
      <tr>
          <td>Playwright tests</td>
          <td>8</td>
      </tr>
      <tr>
          <td>RED-GREEN cycles</td>
          <td>2（初版測試 + 強化版）</td>
      </tr>
      <tr>
          <td>CI workflows 加 / 修</td>
          <td>2（新增 playwright + 修 deploy multi-index）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="學到什麼">學到什麼</h2>
<h3 id="1-cards-skills-系統是雙向的">1. Cards-skills 系統是雙向的</h3>
<p>不是「先寫卡片、再用卡片」。是「卡片指導實作、實作問題回流卡片」。每一輪迭代都把學到的東西反饋。本次 14 張新卡有 8 張是修過程中實際遇到的問題抽出來的、不是預先想的。</p>
<h3 id="2-user-提問是外部觸發">2. User 提問是「外部觸發」</h3>
<p>我自己跑 #67 / #68 / Checkpoint 1 的機率低 — 因為這些都是「沒便利路徑」的工作。User 的兩次提問（「有先寫測試嗎」+「需求確認最重要功能」）剛好對應 #69 + Checkpoint 1 的觸發。<strong>結構性偏差需要外部觸發來修正、不能靠自我提醒</strong>。</p>
<h3 id="3-test-過--對齊使用者意圖">3. Test 過 ≠ 對齊使用者意圖</h3>
<p>第一輪修完、跑 4/4 GREEN、看起來完工。實際漏了：</p>
<ul>
<li>3 個測試是 placebo（沒做 RED 不知道）</li>
<li>3 個 silent 缺口（沒做 Checkpoint 1 不知道）</li>
</ul>
<p>任何「跑得通就 OK」的訊號都低資訊量。Real 訊號 = 對照「使用者意圖完整集合」逐一驗收。</p>
<h3 id="4-一個-bug-修完--一個-case-study-起點">4. 一個 bug 修完 = 一個 case study 起點</h3>
<p>如果停在「bug 修了、test 過了」、這次任務 5 個 commits 結束。User 的兩次提問把它變成 30+ 個 commits 的 case study、產出 17 張新卡 + 兩個 skill 升級 + CI 補強。<strong>修 bug 是 trigger、不是終點</strong>。</p>
<hr>
<h2 id="適合-reuse-這個流程的條件">適合 reuse 這個流程的條件</h2>
<p>不是每個 bug 都該走這套。適合的訊號：</p>
<ul>
<li>Bug 修法不直觀、會碰到多種策略選項（→ 需要 #59 類取捨架構）</li>
<li>修法可能影響其他 feature 或產生新案例（→ 需要 Checkpoint 1）</li>
<li>需要長期 regression 防護（→ 需要 #69 RED-GREEN 驗證）</li>
<li>修的過程中發現新原則（→ 抽卡片）</li>
</ul>
<p>不適合：純 typo / config / build 失敗 — 直接修。</p>
<hr>
<h2 id="對未來想用這套系統的人">對未來想用這套系統的人</h2>
<p>進入點：</p>
<ol>
<li>讀 <code>content/skills/_index.md</code> — 三個 skill 的 routing table</li>
<li>從你的問題情境找對應 skill：
<ul>
<li>不確定怎麼跟 user 溝通 → <code>requirement-protocol</code></li>
<li>前端 / 資料流實作 → <code>frontend-with-playwright</code></li>
<li>寫文件 / 註解 / log → <code>compositional-writing</code></li>
</ul>
</li>
<li>Skill 路由你到 specific reference、reference 路由你到 <code>content/report/</code> 深度卡片</li>
<li>修問題過程中發現新原則 → 抽卡片回流</li>
</ol>
<p>「卡片不是在實作之前一次寫完、是在實作之中持續累積」 — 這套系統的 leverage 在於「下一個類似問題能直接用、不用重新發明」。</p>
<hr>
<h2 id="結語">結語</h2>
<p><code>content/report/</code> 從 54 張長到 71 張、<code>.claude/skills/</code> 從 v0.1 進到 v0.2、CI 從假 pass 變真防護、search bug 從 silent 失敗變到 8/8 regression test 守護。</p>
<p>過程不是線性。是「先做 → 抓到 dogfooding 失敗 → 抽卡片 → 回頭修 → 再被抓失敗 → 再抽卡片 → 再修」。每一輪都讓系統往對齊使用者意圖的方向多走一點。</p>
<p>User 的角色關鍵：兩次提問都不在「指出 bug」、是在「指出我跳過的 checkpoint」。這是純執行者看不到的盲點 — 自己的 dogfooding 失敗。<strong>外部 reviewer 是 cards-skills 系統的必要組件、不是 optional</strong>。</p>
<p>下次有類似情境的人 — 不需要把這條路再走一遍、直接用 #55-#71 + 三個 skill 起步。如果發現新 case、抽新卡回流。系統的價值在每次使用都會變強。</p>
]]></content:encoded></item><item><title>決策對話協議的浮現：從 #74 到 #81 的多層迭代</title><link>https://tarrragon.github.io/blog/posts/%E6%B1%BA%E7%AD%96%E5%B0%8D%E8%A9%B1%E5%8D%94%E8%AD%B0%E7%9A%84%E6%B5%AE%E7%8F%BE%E5%BE%9E-%2374-%E5%88%B0-%2381-%E7%9A%84%E5%A4%9A%E5%B1%A4%E8%BF%AD%E4%BB%A3/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E6%B1%BA%E7%AD%96%E5%B0%8D%E8%A9%B1%E5%8D%94%E8%AD%B0%E7%9A%84%E6%B5%AE%E7%8F%BE%E5%BE%9E-%2374-%E5%88%B0-%2381-%E7%9A%84%E5%A4%9A%E5%B1%A4%E8%BF%AD%E4%BB%A3/</guid><description>&lt;h2 id="這篇要說什麼">這篇要說什麼&lt;/h2>
&lt;p>&lt;a href="../cards-skills-system-case-study/">前一篇 case study&lt;/a> 紀錄的是「實作驅動」的閉環 — 從一個 bug 出發、逼出新卡片。&lt;/p>
&lt;p>本篇紀錄的是 &lt;strong>「對話驅動」的閉環&lt;/strong> — 不修任何 production code、純粹從對話中浮現新卡。觸發點是 user 的一句反思：「&lt;strong>剛剛提出很多不同方向的決策做選擇、這些選擇應該被做成卡片然後分析或者分拆細節研究&lt;/strong>」。&lt;/p>
&lt;p>接下來六輪 spiral 迭代、產生 8 張新卡（&lt;a href="https://tarrragon.github.io/blog/report/" data-link-title="Report — 開發過程的事後檢討" data-link-desc="blog 開發過程中、把實際遇到的版型 / 整合 / 框架共處等情境、整理成『應該怎麼做、沒這樣做會有什麼麻煩』的事後檢討。每篇皆為正向指引、幫助下一輪同類任務跳過反覆試錯。">#74-#81&lt;/a>）+ 1 份 SKILL reference + skill v0.5。本文紀錄這條路徑、當作 &lt;a href="https://tarrragon.github.io/blog/report/cards-as-living-system-iteration/" data-link-title="卡片系統的迭代浮現：原子卡 → meta-卡 → reference 三層展開" data-link-desc="知識卡片系統不是一次寫成、是 dialogue → 原子卡 → meta-卡 → reference 的迭代浮現。每一輪迭代解決上一輪的 over-fit / under-fit、串連分散的卡片、抽出 meta-原則、最後沉澱成可直接套用的 reference 文件。本卡是 cards-skills 系統設計的 process-level 元原則。">#81 卡片系統的迭代浮現&lt;/a> 的具體實例。&lt;/p>
&lt;hr>
&lt;h2 id="起點對話中的反思訊號">起點：對話中的反思訊號&lt;/h2>
&lt;p>對話到第 N 回合時、agent 已經在多次出現「決策呈現」的場景：&lt;/p>
&lt;ul>
&lt;li>「Content mode 三選一」 → user 答 (a)&lt;/li>
&lt;li>「一次 ship 全部 vs 分批」 → user 答「一次」&lt;/li>
&lt;li>「五策略選一」 → user 答 「C 主 + D 補」&lt;/li>
&lt;li>「ship D 還是 B/C」 → user 答 「先 D、B/C 下輪」&lt;/li>
&lt;li>「反省選哪幾個」 → user 答 「1+2」&lt;/li>
&lt;/ul>
&lt;p>每次 agent 都呈現得不一樣、user 也每次回得不一樣。&lt;strong>反覆出現但形式各異 = 抽 meta 的訊號&lt;/strong>（&lt;a href="https://tarrragon.github.io/blog/report/two-occurrence-threshold/" data-link-title="2 次門檻：第一次是運氣、第二次是訊號" data-link-desc="同一個問題出現第 2 次時、就該停下來把處理層級升一階 — 從推理升到量測、從手動驗證升到自動化、從同方向嘗試升到換思路。第 1 次失敗的資訊不足、第 2 次提供「重複出現」的證據、值得付出升級成本。本文是 #11 / #15 / #20 / #23 四篇實作的共同抽象。">#42 2 次門檻&lt;/a> + &lt;a href="https://tarrragon.github.io/blog/report/cards-as-living-system-iteration/" data-link-title="卡片系統的迭代浮現：原子卡 → meta-卡 → reference 三層展開" data-link-desc="知識卡片系統不是一次寫成、是 dialogue → 原子卡 → meta-卡 → reference 的迭代浮現。每一輪迭代解決上一輪的 over-fit / under-fit、串連分散的卡片、抽出 meta-原則、最後沉澱成可直接套用的 reference 文件。本卡是 cards-skills 系統設計的 process-level 元原則。">#81 迭代浮現&lt;/a>）。&lt;/p>
&lt;p>User 的「&lt;strong>這些選擇應該被做成卡片&lt;/strong>」就是 meta-訊號的明確化 — 不是 agent 自己浮現的、是 user 點出來的。&lt;strong>External trigger（&lt;a href="https://tarrragon.github.io/blog/report/external-trigger-for-high-roi-work/" data-link-title="高 ROI 無外部觸發的工作會被結構性跳過" data-link-desc="工作有兩個獨立維度：ROI 高低 &amp;#43; 是否有外部觸發。高 ROI &amp;#43; 無觸發 = ROI 的承諾、拖延的現實。靠紀律不可行 — 結構性偏差需要結構性對策（外部觸發 / CI / hook / 排程 / pair）。本卡是 #67 便利反相關、#68 checkpoint 跳過、#69 RED 跳過的共同上位原則。">#72&lt;/a>）才能逼出抽 meta 這個高 ROI 但無觸發的工作&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要說什麼">這篇要說什麼</h2>
<p><a href="../cards-skills-system-case-study/">前一篇 case study</a> 紀錄的是「實作驅動」的閉環 — 從一個 bug 出發、逼出新卡片。</p>
<p>本篇紀錄的是 <strong>「對話驅動」的閉環</strong> — 不修任何 production code、純粹從對話中浮現新卡。觸發點是 user 的一句反思：「<strong>剛剛提出很多不同方向的決策做選擇、這些選擇應該被做成卡片然後分析或者分拆細節研究</strong>」。</p>
<p>接下來六輪 spiral 迭代、產生 8 張新卡（<a href="/blog/report/" data-link-title="Report — 開發過程的事後檢討" data-link-desc="blog 開發過程中、把實際遇到的版型 / 整合 / 框架共處等情境、整理成『應該怎麼做、沒這樣做會有什麼麻煩』的事後檢討。每篇皆為正向指引、幫助下一輪同類任務跳過反覆試錯。">#74-#81</a>）+ 1 份 SKILL reference + skill v0.5。本文紀錄這條路徑、當作 <a href="/blog/report/cards-as-living-system-iteration/" data-link-title="卡片系統的迭代浮現：原子卡 → meta-卡 → reference 三層展開" data-link-desc="知識卡片系統不是一次寫成、是 dialogue → 原子卡 → meta-卡 → reference 的迭代浮現。每一輪迭代解決上一輪的 over-fit / under-fit、串連分散的卡片、抽出 meta-原則、最後沉澱成可直接套用的 reference 文件。本卡是 cards-skills 系統設計的 process-level 元原則。">#81 卡片系統的迭代浮現</a> 的具體實例。</p>
<hr>
<h2 id="起點對話中的反思訊號">起點：對話中的反思訊號</h2>
<p>對話到第 N 回合時、agent 已經在多次出現「決策呈現」的場景：</p>
<ul>
<li>「Content mode 三選一」 → user 答 (a)</li>
<li>「一次 ship 全部 vs 分批」 → user 答「一次」</li>
<li>「五策略選一」 → user 答 「C 主 + D 補」</li>
<li>「ship D 還是 B/C」 → user 答 「先 D、B/C 下輪」</li>
<li>「反省選哪幾個」 → user 答 「1+2」</li>
</ul>
<p>每次 agent 都呈現得不一樣、user 也每次回得不一樣。<strong>反覆出現但形式各異 = 抽 meta 的訊號</strong>（<a href="/blog/report/two-occurrence-threshold/" data-link-title="2 次門檻：第一次是運氣、第二次是訊號" data-link-desc="同一個問題出現第 2 次時、就該停下來把處理層級升一階 — 從推理升到量測、從手動驗證升到自動化、從同方向嘗試升到換思路。第 1 次失敗的資訊不足、第 2 次提供「重複出現」的證據、值得付出升級成本。本文是 #11 / #15 / #20 / #23 四篇實作的共同抽象。">#42 2 次門檻</a> + <a href="/blog/report/cards-as-living-system-iteration/" data-link-title="卡片系統的迭代浮現：原子卡 → meta-卡 → reference 三層展開" data-link-desc="知識卡片系統不是一次寫成、是 dialogue → 原子卡 → meta-卡 → reference 的迭代浮現。每一輪迭代解決上一輪的 over-fit / under-fit、串連分散的卡片、抽出 meta-原則、最後沉澱成可直接套用的 reference 文件。本卡是 cards-skills 系統設計的 process-level 元原則。">#81 迭代浮現</a>）。</p>
<p>User 的「<strong>這些選擇應該被做成卡片</strong>」就是 meta-訊號的明確化 — 不是 agent 自己浮現的、是 user 點出來的。<strong>External trigger（<a href="/blog/report/external-trigger-for-high-roi-work/" data-link-title="高 ROI 無外部觸發的工作會被結構性跳過" data-link-desc="工作有兩個獨立維度：ROI 高低 &#43; 是否有外部觸發。高 ROI &#43; 無觸發 = ROI 的承諾、拖延的現實。靠紀律不可行 — 結構性偏差需要結構性對策（外部觸發 / CI / hook / 排程 / pair）。本卡是 #67 便利反相關、#68 checkpoint 跳過、#69 RED 跳過的共同上位原則。">#72</a>）才能逼出抽 meta 這個高 ROI 但無觸發的工作</strong>。</p>
<hr>
<h2 id="迭代過程六輪-spiral">迭代過程：六輪 spiral</h2>
<h3 id="輪-1列候選不寫卡">輪 1：列候選、不寫卡</h3>
<p>User 的「應該被做成卡片」沒指定要寫幾張、寫什麼。Agent 列五個候選（A-E）+ 推薦 B+C 組合。User 回「<strong>每個都做成卡片</strong>」+ 給出迭代原則：「<strong>先拓展知識庫、然後才整理成上層的抽象決策依據、最後才做決策</strong>」。</p>
<p>這個原則 = [#81] 的 process explicit form：先原子卡 → 後 meta-卡 → 最後決策（套用）。</p>
<h3 id="輪-2寫五張原子卡74-78">輪 2：寫五張原子卡（#74-#78）</h3>
<p>每張一個維度：</p>
<ul>
<li>#74 呈現格式（選項表 + 推薦 + 開放修改）</li>
<li>#75 主策略 + 補強疊加（不必互斥）</li>
<li>#76 分批 ship（三軸切分）</li>
<li>#77 「現在不決定」是合法選項</li>
<li>#78 反省任務預設複選</li>
</ul>
<p>寫的時候各自獨立、沒明確意識到「這五張其實是同一回事的五個面向」。<strong>在第 5 張寫到一半才開始覺得「好像每張都在打開一個固定的 default」</strong>。</p>
<p>訊號：[#81] 「寫第 N 張卡、發現大段內容跟前一張重複」 → 抽 meta。</p>
<h3 id="輪-3抽-meta-卡-79">輪 3：抽 meta-卡 #79</h3>
<p>User 接著問「都做、用多層迭代去拓展卡片」、agent 回應時自己浮現了 meta-發現：<strong>這五張其實對應五個獨立維度、可組合成 2^5 = 32 種對話形態</strong>。</p>
<p>寫 #79 時、把五張卡的「打開的 default」歸納成五個維度 + 五步判讀。<strong>Meta-卡讓五張原子卡從「平行五張」變成「有結構的網」</strong>。</p>
<p>回頭給每張原子卡補上「跟 #79 的對應」 cross-link、迭代結束。</p>
<h3 id="輪-4沉澱成-reference">輪 4：沉澱成 reference</h3>
<p>光有卡還不夠 — 實作中要翻 5 張卡才能完整 apply、太貴。</p>
<p>寫 <code>references/decision-dialogue.md</code>、把五步判讀 + 完整模板 + self-check 沉澱成一份可直接套用的 protocol。同時更新 SKILL.md 加 trigger route（「呈現決策 / 開放問 / 反省題」）+ Directory Index + 抽象層原則段。</p>
<p>訊號：[#81] 「實作中要回查 ≥ 3 張卡」 → 沉澱 reference。</p>
<h3 id="輪-5dogfood--反向補卡80-81">輪 5：dogfood + 反向補卡（#80-#81）</h3>
<p>User 的「我們想得到的都作、直到推演到極限」逼 agent 自查：</p>
<p><strong>自查 1</strong>：回頭看 agent 在這輪對話的回應、找 collapse 反模式。發現 4 處：</p>
<ul>
<li>「需要我繼續嗎？」 = yes/no（最隱形的 collapse）</li>
<li>「下一層候選」用 bullet 沒適配欄</li>
<li>推薦騎牆「A 比較好不過 B 也行」</li>
<li>反省題列點未明示「互不衝突」</li>
</ul>
<p>→ 寫 #80 Yes/No 二選、把 dogfood 4 例寫進 reference 作為「Bad/Good 對照」。</p>
<p><strong>自查 2</strong>：「這套迭代過程本身是不是 cardable？」是 — 寫 #81 卡片系統的迭代浮現、紀錄「原子 → meta → reference」的 spiral 結構。</p>
<p>訊號：[#81] 「meta-卡寫太早、新 case 一直破壞」的反面 — 寫得剛好、反而能容納新 case（#80、#81 自己）。</p>
<h3 id="輪-6跨連--補強">輪 6：跨連 + 補強</h3>
<p>把 #75（主+補強疊加）展開到 selector pattern：[#46-#49] 看似互斥（每個元件選一個起點）、實際在同一份 code 內可疊加（document + closest 共用）。<strong>Meta-原則的價值之一就是回頭發現舊卡之間有新關係</strong>。</p>
<p>更新 #59（五策略選擇矩陣）加「並用」段落、引用 #75 + #76。</p>
<hr>
<h2 id="過程中的觀察">過程中的觀察</h2>
<h3 id="1-user-的-prompt-直接決定-spiral-深度">1. User 的 prompt 直接決定 spiral 深度</h3>
<p>User 的三句話分別觸發三層深度：</p>
<table>
  <thead>
      <tr>
          <th>User 的話</th>
          <th>觸發深度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「應該被做成卡片」</td>
          <td>寫原子卡（layer 1）</td>
      </tr>
      <tr>
          <td>「先拓展知識庫、再整理上層、最後決策」</td>
          <td>抽 meta-卡（layer 2）+ reference（layer 3）</td>
      </tr>
      <tr>
          <td>「都作、推演到極限」</td>
          <td>dogfood + 反向補卡（layer 4-5）</td>
      </tr>
  </tbody>
</table>
<p>每句話都是 [#72] L4 外部觸發 — 沒這些話、agent 不會自己走到第 5 層。<strong>Spiral 深度由 trigger 決定、不由 agent 紀律決定</strong>。</p>
<h3 id="2-dogfood-回饋的-roi-比新卡高">2. Dogfood 回饋的 ROI 比新卡高</h3>
<p>#80（yes/no）的內容比 #74 短得多、但 ROI 可能更高 — 因為它捕捉的是「最常見、最隱形」的反模式。同樣 reference 的「dogfood Bad/Good 4 例」比抽象描述有用 — 將來 agent 看到自己寫類似格式、能直接認出來。</p>
<p>訊號：<strong>具體例子（特別是反例）的 ROI 通常 &gt; 抽象描述</strong>。</p>
<h3 id="3-meta-卡跟-reference-的職責不同">3. Meta-卡跟 reference 的職責不同</h3>
<p>寫完 #79 還不夠、需要 reference — 因為：</p>
<ul>
<li>卡片回答「為什麼」、reference 回答「怎麼做」</li>
<li>卡片是讀爽的、reference 是被翻的</li>
<li>卡片可選、reference 在實作中是 must</li>
</ul>
<p><strong>兩者缺一不可</strong>：只寫卡 → 知道但忘記用；只寫 reference → 知道做但不知道為什麼、難 maintain。</p>
<h3 id="4-真實的-spiral-不是線性">4. 真實的 spiral 不是線性</h3>
<p>寫 #74 時不知道有 #79、寫 #79 時回頭改 #74-#78、寫 reference 時又發現 #80 漏了、寫 #80 時補 reference 的 dogfood 段。<strong>每一層完成後都會反過來修上一層</strong>。</p>
<p>線性思維（「先寫完 layer 1 才寫 layer 2」）會卡住、spiral 思維（「來回修、每輪都加深」）才能浮現完整結構。</p>
<hr>
<h2 id="跟既有原則的關係">跟既有原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>既有原則</th>
          <th>在本次 spiral 中的角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/report/two-occurrence-threshold/" data-link-title="2 次門檻：第一次是運氣、第二次是訊號" data-link-desc="同一個問題出現第 2 次時、就該停下來把處理層級升一階 — 從推理升到量測、從手動驗證升到自動化、從同方向嘗試升到換思路。第 1 次失敗的資訊不足、第 2 次提供「重複出現」的證據、值得付出升級成本。本文是 #11 / #15 / #20 / #23 四篇實作的共同抽象。">#42 2 次門檻</a></td>
          <td>第 N 次出現決策呈現 = 抽 meta 的訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/report/minimum-necessary-scope-is-sanity-defense/" data-link-title="最小必要範圍是 sanity 防線：保護行為可預測性" data-link-desc="縮 selector 範圍、observer 範圍、JS 操作範圍 — 不是為了效能、是為了讓行為可預測、不被未來變動打破。本文是 #13 / #14 / #29 三篇實作的共同抽象。">#43 最小必要範圍</a></td>
          <td>先窄後寬：原子卡（窄）→ meta（寬）、不要直接寫 meta</td>
      </tr>
      <tr>
          <td><a href="/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">#67 寫作便利度反相關</a></td>
          <td>「直接寫 meta」容易、「迭代浮現」難 — 真實結構不對齊容易寫的格式</td>
      </tr>
      <tr>
          <td><a href="/blog/report/external-trigger-for-high-roi-work/" data-link-title="高 ROI 無外部觸發的工作會被結構性跳過" data-link-desc="工作有兩個獨立維度：ROI 高低 &#43; 是否有外部觸發。高 ROI &#43; 無觸發 = ROI 的承諾、拖延的現實。靠紀律不可行 — 結構性偏差需要結構性對策（外部觸發 / CI / hook / 排程 / pair）。本卡是 #67 便利反相關、#68 checkpoint 跳過、#69 RED 跳過的共同上位原則。">#72 高 ROI 無觸發</a></td>
          <td>抽 meta + 寫 reference 沒外部觸發不會做、user 的話是 L4 觸發</td>
      </tr>
      <tr>
          <td><a href="/blog/report/decision-dialogue-dimensions/" data-link-title="決策對話的五個維度：保持完整選擇空間" data-link-desc="對話中的「決策」不是單一動作、是多維度選擇空間：呈現格式 / 策略疊加 / 批次邊界 / 時間軸 / 選項類型。預設多半 collapse 到最窄格（開放問 &#43; 單策略 &#43; 一次完成 &#43; 立刻決 &#43; 單選）、塞使用者進最少自由度的盒子。本卡是 #74-#78 的上層串連 — 五張卡各對應一個維度的鬆綁。">#79 決策對話的五維度</a></td>
          <td>本次 spiral 的 output、也是元素之一</td>
      </tr>
      <tr>
          <td><a href="/blog/report/cards-as-living-system-iteration/" data-link-title="卡片系統的迭代浮現：原子卡 → meta-卡 → reference 三層展開" data-link-desc="知識卡片系統不是一次寫成、是 dialogue → 原子卡 → meta-卡 → reference 的迭代浮現。每一輪迭代解決上一輪的 over-fit / under-fit、串連分散的卡片、抽出 meta-原則、最後沉澱成可直接套用的 reference 文件。本卡是 cards-skills 系統設計的 process-level 元原則。">#81 卡片系統的迭代浮現</a></td>
          <td>本次 spiral 的 process-level 抽象</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="本次-spiral-的-output-清單">本次 spiral 的 output 清單</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>數量</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>原子卡</td>
          <td>5</td>
          <td>#74-#78（呈現 / 疊加 / 分批 / 延後 / 複選）</td>
      </tr>
      <tr>
          <td>Meta-卡</td>
          <td>1</td>
          <td>#79 五維度</td>
      </tr>
      <tr>
          <td>反向補卡</td>
          <td>2</td>
          <td>#80 yes/no、#81 迭代浮現</td>
      </tr>
      <tr>
          <td>Reference</td>
          <td>1</td>
          <td><code>decision-dialogue.md</code>（runtime + blog）</td>
      </tr>
      <tr>
          <td>Skill 整合</td>
          <td>2</td>
          <td>requirement-protocol v0.5、frontend-with-playwright v0.4</td>
      </tr>
      <tr>
          <td>跨連</td>
          <td>多處</td>
          <td>#59 加疊加段、#46-#49 加 #75 跨連</td>
      </tr>
      <tr>
          <td>Case study</td>
          <td>1</td>
          <td>本文</td>
      </tr>
  </tbody>
</table>
<p><strong>整輪迭代的成本</strong>：純對話、無 production code 改動、無新測試。<strong>整輪迭代的價值</strong>：未來 agent 在每次「決策呈現」場景都有 reference 可翻、有 self-check 可用、有 dogfood 例子可對照。</p>
<hr>
<h2 id="結語">結語</h2>
<p>本系統的成型不是「用心寫文件」、是接受**「對話會浮現結構、原子卡會自我串連、meta-卡會回頭修原子卡」這個 spiral 真相**、然後讓每輪迭代都加深一點。</p>
<p>下一次 user 在對話中又出現「這個應該被做成卡片」訊號時、流程已經是現成的 — 套 [#81] 的三層展開 + [#72] 的 L4 觸發、就能繼續長新卡。<strong>真正的 knowledge infrastructure 不是寫一次的文件、是長期 spiral 的 living system</strong>。</p>
]]></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>Fuse.js / MiniSearch：客戶端載入索引的搜尋方案</title><link>https://tarrragon.github.io/blog/posts/fuse.js-/-minisearch%E5%AE%A2%E6%88%B6%E7%AB%AF%E8%BC%89%E5%85%A5%E7%B4%A2%E5%BC%95%E7%9A%84%E6%90%9C%E5%B0%8B%E6%96%B9%E6%A1%88/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/fuse.js-/-minisearch%E5%AE%A2%E6%88%B6%E7%AB%AF%E8%BC%89%E5%85%A5%E7%B4%A2%E5%BC%95%E7%9A%84%E6%90%9C%E5%B0%8B%E6%96%B9%E6%A1%88/</guid><description>&lt;h2 id="客戶端搜尋的問題空間">客戶端搜尋的問題空間&lt;/h2>
&lt;p>靜態站搜尋必須在 build 時或 client runtime 完成。選擇&lt;strong>整包序列化 + client 載入&lt;/strong>這條路時，核心設計軸是：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計軸&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>索引內容&lt;/td>
 &lt;td>由作者在 build time 明確決定要搜哪些欄位、哪些 section&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>索引結構&lt;/td>
 &lt;td>扁平 JSON 陣列，每筆一個頁面，欄位直寫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>runtime 處理&lt;/td>
 &lt;td>在瀏覽器內建索引、記憶體內匹配&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Fuse.js 與 MiniSearch 是這條路上的兩個主要實作。差異在匹配策略（fuzzy vs 全文），共享的是「一包索引載入瀏覽器、之後所有查詢不再出站」這個骨幹。&lt;/p>
&lt;hr>
&lt;h2 id="核心設計build-時序列化--runtime-in-memory">核心設計：build 時序列化 + runtime in-memory&lt;/h2>
&lt;p>&lt;strong>商業邏輯&lt;/strong>：把搜尋放在 client runtime 的關鍵是&lt;strong>搜尋不再跨網路來回&lt;/strong>。第一次載入索引之後，每次打字的匹配都在使用者的 RAM 內完成，不受網路延遲影響、不受後端服務狀態影響、甚至不需要網路連線。&lt;/p>
&lt;p>此設計把「索引存放」從伺服端或 CDN 移到了訪客自己的瀏覽器，換得 runtime 的完全獨立。&lt;/p>
&lt;p>&lt;strong>CASE&lt;/strong>：整個流程兩個時點：&lt;/p>
&lt;p>&lt;strong>Build time（Hugo 階段）&lt;/strong>：Hugo 用 custom output format 產出一份 JSON，每筆一個頁面。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;WAF&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/backend/knowledge-cards/waf/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;說明 WAF 如何在入口層過濾攻擊&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;完整內文…&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Runtime（瀏覽器階段）&lt;/strong>：使用者打開搜尋頁，browser &lt;code>fetch&lt;/code> JSON → library 在 memory 中建索引 → 使用者打字 → library 匹配 → 結果渲染。&lt;/p>
&lt;p>第一次 fetch + build index 通常 100-500ms；之後的每次查詢在 memory 內匹配，一般 &amp;lt;10ms。&lt;/p>
&lt;hr>
&lt;h2 id="架構選擇作者定義索引內容">架構選擇：作者定義索引內容&lt;/h2>
&lt;p>&lt;strong>商業邏輯&lt;/strong>：索引的範圍與欄位由誰決定，這件事決定了搜尋結果的邊界。Fuse.js / MiniSearch 採「作者顯式宣告」的路線 — Hugo template 明確列出哪些 section 進索引、每筆要哪些欄位。&lt;/p>
&lt;p>這個選擇讓搜尋結果成為&lt;strong>作者設計決策的產物&lt;/strong>：想排除 work-log 類別就不列入 range；想讓 tag 也可搜就加一個 &lt;code>tags&lt;/code> 欄位到 JSON；想降低索引大小就只存 &lt;code>title + description&lt;/code> 而不存 &lt;code>content&lt;/code>。&lt;/p>
&lt;p>&lt;strong>CASE&lt;/strong>：&lt;code>layouts/index.json&lt;/code> 決定 JSON 內容：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$pages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">where&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Site.RegularPages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Section&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;in&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="k">slice&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;posts&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;backend&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;go&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;python&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">[
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">range&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$i&lt;/span>&lt;span class="o">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$p&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$pages&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$i&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>,&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> { &amp;#34;title&amp;#34;: &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &amp;#34;url&amp;#34;: &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.RelPermalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &amp;#34;description&amp;#34;: &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Description&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &amp;#34;content&amp;#34;: &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Plain&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">jsonify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="cp">{{-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">-}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配套在 &lt;code>hugo.toml&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">outputs&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">home&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;HTML&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;RSS&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;JSON&amp;#34;&lt;/span>&lt;span class="p">]&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="p">[&lt;/span>&lt;span class="nx">outputFormats&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JSON&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">mediaType&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">baseName&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;index&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nx">isPlainText&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Build 後 &lt;code>public/index.json&lt;/code> 就是整站可搜內容的權威來源。&lt;/p>
&lt;hr>
&lt;h2 id="整合步驟以-fusejs-為例">整合步驟（以 Fuse.js 為例）&lt;/h2>
&lt;h3 id="1-hugo-產生-indexjson">1. Hugo 產生 index.json&lt;/h3>
&lt;p>&lt;strong>核心動作&lt;/strong>：設定 custom output format，寫 template 輸出 JSON。&lt;/p></description><content:encoded><![CDATA[<h2 id="客戶端搜尋的問題空間">客戶端搜尋的問題空間</h2>
<p>靜態站搜尋必須在 build 時或 client runtime 完成。選擇<strong>整包序列化 + client 載入</strong>這條路時，核心設計軸是：</p>
<table>
  <thead>
      <tr>
          <th>設計軸</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引內容</td>
          <td>由作者在 build time 明確決定要搜哪些欄位、哪些 section</td>
      </tr>
      <tr>
          <td>索引結構</td>
          <td>扁平 JSON 陣列，每筆一個頁面，欄位直寫</td>
      </tr>
      <tr>
          <td>runtime 處理</td>
          <td>在瀏覽器內建索引、記憶體內匹配</td>
      </tr>
  </tbody>
</table>
<p>Fuse.js 與 MiniSearch 是這條路上的兩個主要實作。差異在匹配策略（fuzzy vs 全文），共享的是「一包索引載入瀏覽器、之後所有查詢不再出站」這個骨幹。</p>
<hr>
<h2 id="核心設計build-時序列化--runtime-in-memory">核心設計：build 時序列化 + runtime in-memory</h2>
<p><strong>商業邏輯</strong>：把搜尋放在 client runtime 的關鍵是<strong>搜尋不再跨網路來回</strong>。第一次載入索引之後，每次打字的匹配都在使用者的 RAM 內完成，不受網路延遲影響、不受後端服務狀態影響、甚至不需要網路連線。</p>
<p>此設計把「索引存放」從伺服端或 CDN 移到了訪客自己的瀏覽器，換得 runtime 的完全獨立。</p>
<p><strong>CASE</strong>：整個流程兩個時點：</p>
<p><strong>Build time（Hugo 階段）</strong>：Hugo 用 custom output format 產出一份 JSON，每筆一個頁面。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span> <span class="nt">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;WAF&#34;</span><span class="p">,</span> <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;/backend/knowledge-cards/waf/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;說明 WAF 如何在入口層過濾攻擊&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;完整內文…&#34;</span> <span class="p">}</span></span></span></code></pre></div><p><strong>Runtime（瀏覽器階段）</strong>：使用者打開搜尋頁，browser <code>fetch</code> JSON → library 在 memory 中建索引 → 使用者打字 → library 匹配 → 結果渲染。</p>
<p>第一次 fetch + build index 通常 100-500ms；之後的每次查詢在 memory 內匹配，一般 &lt;10ms。</p>
<hr>
<h2 id="架構選擇作者定義索引內容">架構選擇：作者定義索引內容</h2>
<p><strong>商業邏輯</strong>：索引的範圍與欄位由誰決定，這件事決定了搜尋結果的邊界。Fuse.js / MiniSearch 採「作者顯式宣告」的路線 — Hugo template 明確列出哪些 section 進索引、每筆要哪些欄位。</p>
<p>這個選擇讓搜尋結果成為<strong>作者設計決策的產物</strong>：想排除 work-log 類別就不列入 range；想讓 tag 也可搜就加一個 <code>tags</code> 欄位到 JSON；想降低索引大小就只存 <code>title + description</code> 而不存 <code>content</code>。</p>
<p><strong>CASE</strong>：<code>layouts/index.json</code> 決定 JSON 內容：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="nx">$pages</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">where</span><span class="w"> </span><span class="na">.Site.RegularPages</span><span class="w"> </span><span class="s">&#34;Section&#34;</span><span class="w"> </span><span class="s">&#34;in&#34;</span><span class="w"> </span><span class="o">(</span><span class="k">slice</span><span class="w"> </span><span class="s">&#34;posts&#34;</span><span class="w"> </span><span class="s">&#34;backend&#34;</span><span class="w"> </span><span class="s">&#34;go&#34;</span><span class="w"> </span><span class="s">&#34;python&#34;</span><span class="o">)</span><span class="w"> </span><span class="cp">-}}</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="cp">{{-</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">$i</span><span class="o">,</span><span class="w"> </span><span class="nx">$p</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">$pages</span><span class="w"> </span><span class="cp">-}}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="cp">{{-</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">$i</span><span class="w"> </span><span class="cp">}}</span>,<span class="cp">{{</span><span class="w"> </span><span class="k">end</span><span class="w"> </span><span class="cp">}}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  { &#34;title&#34;: <span class="cp">{{</span><span class="w"> </span><span class="na">.Title</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nx">jsonify</span><span class="w"> </span><span class="cp">}}</span>,
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    &#34;url&#34;: <span class="cp">{{</span><span class="w"> </span><span class="na">.RelPermalink</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nx">jsonify</span><span class="w"> </span><span class="cp">}}</span>,
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    &#34;description&#34;: <span class="cp">{{</span><span class="w"> </span><span class="na">.Description</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nx">jsonify</span><span class="w"> </span><span class="cp">}}</span>,
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    &#34;content&#34;: <span class="cp">{{</span><span class="w"> </span><span class="na">.Plain</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nx">jsonify</span><span class="w"> </span><span class="cp">}}</span> }
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="k">end</span><span class="w"> </span><span class="cp">-}}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">]</span></span></code></pre></div><p>配套在 <code>hugo.toml</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">outputs</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">home</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;HTML&#34;</span><span class="p">,</span> <span class="s2">&#34;RSS&#34;</span><span class="p">,</span> <span class="s2">&#34;JSON&#34;</span><span class="p">]</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="p">[</span><span class="nx">outputFormats</span><span class="p">.</span><span class="nx">JSON</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">mediaType</span> <span class="p">=</span> <span class="s2">&#34;application/json&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">baseName</span> <span class="p">=</span> <span class="s2">&#34;index&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nx">isPlainText</span> <span class="p">=</span> <span class="kc">true</span></span></span></code></pre></div><p>Build 後 <code>public/index.json</code> 就是整站可搜內容的權威來源。</p>
<hr>
<h2 id="整合步驟以-fusejs-為例">整合步驟（以 Fuse.js 為例）</h2>
<h3 id="1-hugo-產生-indexjson">1. Hugo 產生 index.json</h3>
<p><strong>核心動作</strong>：設定 custom output format，寫 template 輸出 JSON。</p>
<p>見上方「架構選擇」段落的 <code>hugo.toml</code> 與 <code>layouts/index.json</code>。</p>
<h3 id="2-搜尋頁載入-library--index">2. 搜尋頁載入 library + index</h3>
<p><strong>核心動作</strong>：前端一個 <code>&lt;input&gt;</code>、一段 script，完成 fetch + 建索引 + 匹配 + 渲染。</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">{{ define &#34;main&#34; }}
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;q&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;搜尋…&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">&lt;</span><span class="nt">ul</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;results&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">ul</span><span class="p">&gt;</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="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/fuse.js@7/dist/fuse.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;{{ &#34;index.json&#34; | relURL }}&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="p">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">json</span><span class="p">())</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="kr">const</span> <span class="nx">fuse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Fuse</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">keys</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">,</span> <span class="s1">&#39;description&#39;</span><span class="p">,</span> <span class="s1">&#39;content&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">threshold</span><span class="o">:</span> <span class="mf">0.3</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nx">includeMatches</span><span class="o">:</span> <span class="kc">true</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 class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;q&#39;</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="nx">e</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="kr">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">fuse</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;results&#39;</span><span class="p">).</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">results</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">          <span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">r</span> <span class="p">=&gt;</span> <span class="sb">`&lt;li&gt;&lt;a href=&#34;</span><span class="si">${</span><span class="nx">r</span><span class="p">.</span><span class="nx">item</span><span class="p">.</span><span class="nx">url</span><span class="si">}</span><span class="sb">&#34;&gt;</span><span class="si">${</span><span class="nx">r</span><span class="p">.</span><span class="nx">item</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span><span class="sb">&lt;/a&gt;
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="sb">                     &lt;p&gt;</span><span class="si">${</span><span class="nx">r</span><span class="p">.</span><span class="nx">item</span><span class="p">.</span><span class="nx">description</span><span class="si">}</span><span class="sb">&lt;/p&gt;&lt;/li&gt;`</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">          <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s1">&#39;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">{{ end }}</span></span></code></pre></div><p>30 行內可以跑起來。</p>
<h3 id="3-minisearch-的-api-差異">3. MiniSearch 的 API 差異</h3>
<p><strong>核心動作</strong>：選 MiniSearch 時，API 形狀相近、配置項不同。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">const</span> <span class="nx">mini</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MiniSearch</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">fields</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">,</span> <span class="s1">&#39;description&#39;</span><span class="p">,</span> <span class="s1">&#39;content&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">storeFields</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">,</span> <span class="s1">&#39;url&#39;</span><span class="p">,</span> <span class="s1">&#39;description&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">searchOptions</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">boost</span><span class="o">:</span> <span class="p">{</span> <span class="nx">title</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span> <span class="nx">description</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">prefix</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">fuzzy</span><span class="o">:</span> <span class="mf">0.2</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 class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">mini</span><span class="p">.</span><span class="nx">addAll</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kr">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">mini</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span></span></span></code></pre></div><ul>
<li><code>boost</code> 決定各欄位命中的權重：title 命中比 content 命中重 3 倍</li>
<li><code>prefix: true</code> 讓 &ldquo;WA&rdquo; 命中 &ldquo;WAF&rdquo;</li>
<li><code>fuzzy: 0.2</code> 開啟 approximate match，容錯程度可調</li>
</ul>
<hr>
<h2 id="方案的內在屬性">方案的內在屬性</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Fuse.js / MiniSearch 的特徵</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>覆蓋完整性</td>
          <td>由作者顯式宣告索引範圍 — 要搜什麼完全可控</td>
      </tr>
      <tr>
          <td>可逆性</td>
          <td>移除只需刪除 <code>index.json</code> output、搜尋頁、script reference</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>無額外 build step；索引 schema 改動要同步改 template 與 client code</td>
      </tr>
      <tr>
          <td>可理解性</td>
          <td>library 原始碼規模可讀（Fuse.js ~10KB、MiniSearch ~6KB gzipped），API 面積小</td>
      </tr>
      <tr>
          <td>依賴前提</td>
          <td>要求 Hugo 支援 custom output format（所有版本皆支援）；要求 client 能跑 JS</td>
      </tr>
      <tr>
          <td>擴展性</td>
          <td>單次查詢發生在 memory 內 — 查詢效能不受網路或站規模影響；索引載入是首次一次性</td>
      </tr>
  </tbody>
</table>
<p><strong>與 runtime 獨立相關的延伸特徵</strong>：</p>
<ul>
<li><strong>離線可用</strong>：索引載入後所有查詢不需要網路；PWA 加 Cache API 讓索引也能離線快取</li>
<li><strong>自託管</strong>：索引資料不離開你的網域；敏感內容或私有文件特別適合</li>
<li><strong>隱私</strong>：訪客查詢字串不會送到任何第三方服務</li>
</ul>
<p><strong>與 UI 獨立相關的延伸特徵</strong>：</p>
<ul>
<li><strong>樣式與互動 100% 可控</strong>：搜尋框位置、結果卡排版、modal 與否、鍵盤操作 — 每一項都由作者決定</li>
<li><strong>與 theme 緊密整合</strong>：UI 可以直接套用站上其他元件的 CSS variable 與設計 token</li>
</ul>
<hr>
<h2 id="兩家-library-的定位差異">兩家 library 的定位差異</h2>
<p>Fuse.js 與 MiniSearch 共享核心架構，<strong>設計重心不同</strong>：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Fuse.js</th>
          <th>MiniSearch</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>匹配策略</td>
          <td>以 fuzzy / approximate match 為主軸</td>
          <td>傳統全文檢索（詞項匹配 + 評分）</td>
      </tr>
      <tr>
          <td>擅長情境</td>
          <td>錯字容錯、近似詞匹配 — 搜 &ldquo;kubernates&rdquo; 命中 &ldquo;kubernetes&rdquo;</td>
          <td>精確詞匹配、field boosting、prefix 搜尋</td>
      </tr>
      <tr>
          <td>Gzipped 大小</td>
          <td>~10KB</td>
          <td>~6KB</td>
      </tr>
  </tbody>
</table>
<p>兩者的 API 形狀相近，切換成本低。決定用哪一個，主要看<strong>希望怎麼對待 query</strong>：可能有錯字的模糊輸入偏向 Fuse.js，結構化的技術關鍵字偏向 MiniSearch。</p>
<hr>
<h2 id="運作特徵">運作特徵</h2>
<h3 id="index-在首次載入">Index 在首次載入</h3>
<p><strong>核心定義</strong>：索引是一份 JSON，使用者打開搜尋頁時由瀏覽器一次性 fetch。</p>
<p><strong>含義</strong>：首次延遲 = 下載 JSON + library 建索引。常見做法是在 <code>DOMContentLoaded</code> 就 preload JSON，讓使用者看到搜尋框時索引已建好、第一次打字即可查詢。</p>
<p><strong>規模適合度</strong>：幾百到一兩千頁、索引 JSON 幾百 KB 到 1-2MB 的站，體驗最穩定。索引大小由作者在 Hugo template 內決定 — 只索引 title + description 可以把 size 壓到很小。</p>
<h3 id="索引範圍由作者決定">索引範圍由作者決定</h3>
<p><strong>核心定義</strong>：Hugo template 明確列出要進索引的 section 與欄位。</p>
<p><strong>含義</strong>：搜尋結果的邊界是作者設計決策。增減 section、增減 field、調整儲存策略，都在 template 這一層直接生效。</p>
<h3 id="tokenization-依-library-而異">Tokenization 依 library 而異</h3>
<p><strong>核心定義</strong>：Fuse.js 採 character-level 匹配；MiniSearch 預設用空白分詞。</p>
<p><strong>含義</strong>：</p>
<ul>
<li>Fuse.js 對中文天然能搜，不需要斷詞設定</li>
<li>MiniSearch 對中文需要傳自訂 <code>tokenize</code> function，可以一個字一 token，或接 Intl.Segmenter 做詞界切分</li>
</ul>
<h3 id="ui-由作者自己寫">UI 由作者自己寫</h3>
<p><strong>核心定義</strong>：library 只提供搜尋 API，不提供視覺組件。</p>
<p><strong>含義</strong>：排版、鍵盤操作、focus management、ARIA 這些 UI 層責任由作者顯式實作。收穫是與 theme 完全融合的客製體驗。</p>
<hr>
<h2 id="適合的場景">適合的場景</h2>
<ul>
<li>站的規模穩定在幾百到一兩千頁</li>
<li>UI 需要深度客製、與 theme 風格緊密整合</li>
<li>想要最單純的 build pipeline（無 post-build step、無額外工具）</li>
<li>內容敏感、希望索引不離開自家網域</li>
<li>希望搜尋在離線狀態仍可用</li>
<li>需要 fuzzy match（Fuse.js）或精細 field boost + prefix（MiniSearch）</li>
</ul>
]]></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>Pagefind：靜態站搜尋的 build-time 索引方案</title><link>https://tarrragon.github.io/blog/posts/pagefind%E9%9D%9C%E6%85%8B%E7%AB%99%E6%90%9C%E5%B0%8B%E7%9A%84-build-time-%E7%B4%A2%E5%BC%95%E6%96%B9%E6%A1%88/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/pagefind%E9%9D%9C%E6%85%8B%E7%AB%99%E6%90%9C%E5%B0%8B%E7%9A%84-build-time-%E7%B4%A2%E5%BC%95%E6%96%B9%E6%A1%88/</guid><description>&lt;h2 id="靜態站搜尋的問題空間">靜態站搜尋的問題空間&lt;/h2>
&lt;p>靜態站沒有後端可以接查詢，所有搜尋工作必須在兩個時點之一完成：&lt;strong>build 時&lt;/strong>產生索引、&lt;strong>client runtime&lt;/strong> 執行匹配。這個前提決定了所有靜態站搜尋方案共同面對的兩個設計軸：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計軸&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>索引產生時機&lt;/td>
 &lt;td>build 時靜態產生，或 client 載入後動態建立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>索引交付方式&lt;/td>
 &lt;td>一次全量下載，或按查詢 lazy-load&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>方案差異來自這兩軸的組合。Pagefind 選的是「build 時產生、按需載入」，它的所有設計決策都是這個選擇的延伸。&lt;/p>
&lt;hr>
&lt;h2 id="核心設計索引切片與按需載入">核心設計：索引切片與按需載入&lt;/h2>
&lt;p>&lt;strong>商業邏輯&lt;/strong>：搜尋索引的 scaling 關鍵是&lt;strong>單次查詢需要下載多少資料&lt;/strong>，而非壓縮率或演算法效率。若索引是一整包、每次查詢都要先整包載入，訪客體驗與站的大小線性綁定 — 站大 10 倍，首次搜尋延遲 10 倍。&lt;/p>
&lt;p>要脫離這條綁定，索引必須能以「與查詢相關」的粒度切片、按需傳輸。這把「索引多大」的問題從訪客手上移回 build pipeline。&lt;/p>
&lt;p>&lt;strong>CASE&lt;/strong>：Pagefind 的索引是三層結構：&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>pagefind-entry.json&lt;/code>&lt;/td>
 &lt;td>索引目錄，記載有哪些 chunk 與 fragment&lt;/td>
 &lt;td>&amp;lt;10KB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>index/*.pf_index&lt;/code>&lt;/td>
 &lt;td>倒排索引切片，依 term 前綴分片&lt;/td>
 &lt;td>10-50KB / chunk&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>fragment/*.pf_fragment&lt;/code>&lt;/td>
 &lt;td>每篇文章的 metadata、URL、摘要&lt;/td>
 &lt;td>2-5KB / fragment&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>查「WAF」時，client 下載路徑是：entry（10KB）→ 涵蓋 &amp;ldquo;W&amp;rdquo; 的 index chunk（~30KB）→ 命中文章的 fragment（每筆 3KB）。總傳輸量與全站大小幾乎脫鉤 — 站擴大 10 倍，單次搜尋仍然只下載「W」那個 chunk 與少數 fragment。&lt;/p>
&lt;hr>
&lt;h2 id="架構選擇爬-rendered-html">架構選擇：爬 rendered HTML&lt;/h2>
&lt;p>&lt;strong>商業邏輯&lt;/strong>：索引內容的來源有兩種可能：&lt;strong>source 層&lt;/strong>（markdown、frontmatter、結構化資料）或 &lt;strong>output 層&lt;/strong>（render 後的 HTML）。選哪一層決定工具與 framework 的耦合程度 — source 層要求工具懂特定 framework 的內容模型；output 層只要求結果是 HTML。&lt;/p>
&lt;p>Pagefind 選 output 層。含義是：它跟 Hugo、Jekyll、Zola、Next.js static export 完全解耦，只要該 framework 產出的是 HTML，Pagefind 都能索引。&lt;/p>
&lt;p>&lt;strong>CASE&lt;/strong>：此選擇在 blog 端的具體要求：希望被搜到的內容必須出現在 rendered HTML 上。frontmatter 的 &lt;code>description&lt;/code> 欄位若只存在於 markdown source、沒被 theme 輸出成 &lt;code>&amp;lt;meta&amp;gt;&lt;/code> 或可見文字，就不會進索引。&lt;/p>
&lt;p>這個 blog 天然滿足 — theme 把 description 寫進 &lt;code>&amp;lt;meta name=&amp;quot;description&amp;quot;&amp;gt;&lt;/code>，render hook 也用它做 tooltip。移植到任何其他 static site generator，只要目標的 output HTML 有這些欄位，搜尋整合不用重寫。&lt;/p>
&lt;hr>
&lt;h2 id="整合步驟">整合步驟&lt;/h2>
&lt;h3 id="1-build-pipeline">1. Build pipeline&lt;/h3>
&lt;p>&lt;strong>核心動作&lt;/strong>：Hugo build 後加一步 Pagefind。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">hugo --minify
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">npx -y pagefind --site public&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩步，沒有中間檔。Pagefind 自行讀取 &lt;code>public/&lt;/code> 的 HTML，將索引寫回 &lt;code>public/pagefind/&lt;/code>。&lt;/p>
&lt;h3 id="2-搜尋頁路由">2. 搜尋頁路由&lt;/h3>
&lt;p>&lt;strong>核心動作&lt;/strong>：建立 Hugo 單頁，指向專屬 layout。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;搜尋&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">layout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">search&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sitemap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">disable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>sitemap.disable&lt;/code> 避免搜尋頁自己被 Hugo sitemap 收錄。&lt;/p>
&lt;h3 id="3-ui-掛載">3. UI 掛載&lt;/h3>
&lt;p>&lt;strong>核心動作&lt;/strong>：在 layout 中載入 Pagefind UI 資源，指定 mount point。&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">{{ define &amp;#34;main&amp;#34; }}
&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">div&lt;/span> &lt;span class="na">data-pagefind-ignore&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 class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ &amp;#34;&lt;/span>&lt;span class="na">pagefind&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="na">pagefind-ui&lt;/span>&lt;span class="err">.&lt;/span>&lt;span class="na">css&lt;/span>&lt;span class="err">&amp;#34;&lt;/span> &lt;span class="err">|&lt;/span> &lt;span class="na">relURL&lt;/span> &lt;span class="err">}}&amp;#34;&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&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">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ &amp;#34;&lt;/span>&lt;span class="na">pagefind&lt;/span>&lt;span class="err">/&lt;/span>&lt;span class="na">pagefind-ui&lt;/span>&lt;span class="err">.&lt;/span>&lt;span class="na">js&lt;/span>&lt;span class="err">&amp;#34;&lt;/span> &lt;span class="err">|&lt;/span> &lt;span class="na">relURL&lt;/span> &lt;span class="err">}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&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">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">PagefindUI&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">element&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;#search&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">showSubResults&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">translations&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">placeholder&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;搜尋卡片或文章…&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&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;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">{{ end }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個細節：&lt;/p></description><content:encoded><![CDATA[<h2 id="靜態站搜尋的問題空間">靜態站搜尋的問題空間</h2>
<p>靜態站沒有後端可以接查詢，所有搜尋工作必須在兩個時點之一完成：<strong>build 時</strong>產生索引、<strong>client runtime</strong> 執行匹配。這個前提決定了所有靜態站搜尋方案共同面對的兩個設計軸：</p>
<table>
  <thead>
      <tr>
          <th>設計軸</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引產生時機</td>
          <td>build 時靜態產生，或 client 載入後動態建立</td>
      </tr>
      <tr>
          <td>索引交付方式</td>
          <td>一次全量下載，或按查詢 lazy-load</td>
      </tr>
  </tbody>
</table>
<p>方案差異來自這兩軸的組合。Pagefind 選的是「build 時產生、按需載入」，它的所有設計決策都是這個選擇的延伸。</p>
<hr>
<h2 id="核心設計索引切片與按需載入">核心設計：索引切片與按需載入</h2>
<p><strong>商業邏輯</strong>：搜尋索引的 scaling 關鍵是<strong>單次查詢需要下載多少資料</strong>，而非壓縮率或演算法效率。若索引是一整包、每次查詢都要先整包載入，訪客體驗與站的大小線性綁定 — 站大 10 倍，首次搜尋延遲 10 倍。</p>
<p>要脫離這條綁定，索引必須能以「與查詢相關」的粒度切片、按需傳輸。這把「索引多大」的問題從訪客手上移回 build pipeline。</p>
<p><strong>CASE</strong>：Pagefind 的索引是三層結構：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>內容</th>
          <th>大小</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pagefind-entry.json</code></td>
          <td>索引目錄，記載有哪些 chunk 與 fragment</td>
          <td>&lt;10KB</td>
      </tr>
      <tr>
          <td><code>index/*.pf_index</code></td>
          <td>倒排索引切片，依 term 前綴分片</td>
          <td>10-50KB / chunk</td>
      </tr>
      <tr>
          <td><code>fragment/*.pf_fragment</code></td>
          <td>每篇文章的 metadata、URL、摘要</td>
          <td>2-5KB / fragment</td>
      </tr>
  </tbody>
</table>
<p>查「WAF」時，client 下載路徑是：entry（10KB）→ 涵蓋 &ldquo;W&rdquo; 的 index chunk（~30KB）→ 命中文章的 fragment（每筆 3KB）。總傳輸量與全站大小幾乎脫鉤 — 站擴大 10 倍，單次搜尋仍然只下載「W」那個 chunk 與少數 fragment。</p>
<hr>
<h2 id="架構選擇爬-rendered-html">架構選擇：爬 rendered HTML</h2>
<p><strong>商業邏輯</strong>：索引內容的來源有兩種可能：<strong>source 層</strong>（markdown、frontmatter、結構化資料）或 <strong>output 層</strong>（render 後的 HTML）。選哪一層決定工具與 framework 的耦合程度 — source 層要求工具懂特定 framework 的內容模型；output 層只要求結果是 HTML。</p>
<p>Pagefind 選 output 層。含義是：它跟 Hugo、Jekyll、Zola、Next.js static export 完全解耦，只要該 framework 產出的是 HTML，Pagefind 都能索引。</p>
<p><strong>CASE</strong>：此選擇在 blog 端的具體要求：希望被搜到的內容必須出現在 rendered HTML 上。frontmatter 的 <code>description</code> 欄位若只存在於 markdown source、沒被 theme 輸出成 <code>&lt;meta&gt;</code> 或可見文字，就不會進索引。</p>
<p>這個 blog 天然滿足 — theme 把 description 寫進 <code>&lt;meta name=&quot;description&quot;&gt;</code>，render hook 也用它做 tooltip。移植到任何其他 static site generator，只要目標的 output HTML 有這些欄位，搜尋整合不用重寫。</p>
<hr>
<h2 id="整合步驟">整合步驟</h2>
<h3 id="1-build-pipeline">1. Build pipeline</h3>
<p><strong>核心動作</strong>：Hugo build 後加一步 Pagefind。</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">hugo --minify
</span></span><span class="line"><span class="ln">2</span><span class="cl">npx -y pagefind --site public</span></span></code></pre></div><p>兩步，沒有中間檔。Pagefind 自行讀取 <code>public/</code> 的 HTML，將索引寫回 <code>public/pagefind/</code>。</p>
<h3 id="2-搜尋頁路由">2. 搜尋頁路由</h3>
<p><strong>核心動作</strong>：建立 Hugo 單頁，指向專屬 layout。</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">layout</span><span class="p">:</span><span class="w"> </span><span class="l">search</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">sitemap</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">disable</span><span class="p">:</span><span class="w"> </span><span class="kc">true</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><p><code>sitemap.disable</code> 避免搜尋頁自己被 Hugo sitemap 收錄。</p>
<h3 id="3-ui-掛載">3. UI 掛載</h3>
<p><strong>核心動作</strong>：在 layout 中載入 Pagefind UI 資源，指定 mount point。</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">{{ define &#34;main&#34; }}
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">data-pagefind-ignore</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{ &#34;</span><span class="na">pagefind</span><span class="err">/</span><span class="na">pagefind-ui</span><span class="err">.</span><span class="na">css</span><span class="err">&#34;</span> <span class="err">|</span> <span class="na">relURL</span> <span class="err">}}&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;search&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 5</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;{{ &#34;</span><span class="na">pagefind</span><span class="err">/</span><span class="na">pagefind-ui</span><span class="err">.</span><span class="na">js</span><span class="err">&#34;</span> <span class="err">|</span> <span class="na">relURL</span> <span class="err">}}&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="k">new</span> <span class="nx">PagefindUI</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">element</span><span class="o">:</span> <span class="s2">&#34;#search&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">showSubResults</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">translations</span><span class="o">:</span> <span class="p">{</span> <span class="nx">placeholder</span><span class="o">:</span> <span class="s2">&#34;搜尋卡片或文章…&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">{{ end }}</span></span></code></pre></div><p>兩個細節：</p>
<ul>
<li><code>data-pagefind-ignore</code> 告訴 Pagefind 這頁本身不要進索引（避免搜「搜尋」出現搜尋頁）。</li>
<li><code>relURL</code> 處理 baseURL 的 subpath（例如 <code>/blog/</code>），讓 UI 自動推斷 chunk 相對位置。</li>
</ul>
<h3 id="4-ci-workflow">4. CI workflow</h3>
<p><strong>核心動作</strong>：GitHub Actions 在 Hugo build 步驟後插入 Pagefind。</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="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build Pagefind search index</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">run</span><span class="p">:</span><span class="w"> </span><span class="l">npx -y pagefind --site public</span></span></span></code></pre></div><p>ubuntu-latest runner 內建 node，<code>npx -y</code> 首次執行會下載並 cache binary，後續執行直接從 cache 取用。</p>
<hr>
<h2 id="方案的內在屬性">方案的內在屬性</h2>
<p>評估 Pagefind 不看「比較快」「比較省事」這類時間維度，用下列內在屬性：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Pagefind 的特徵</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>覆蓋完整性</td>
          <td>索引全站 HTML；不需要逐 section 註冊</td>
      </tr>
      <tr>
          <td>可逆性</td>
          <td>產物是檔案，移除就是刪除 <code>public/pagefind/</code> 與搜尋頁，無殘留依賴</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>build pipeline 多一步；無 runtime 服務、無 key 管理、無版本相依性</td>
      </tr>
      <tr>
          <td>可理解性</td>
          <td>UI drop-in、filter 用 HTML 屬性宣告、三層索引結構直觀</td>
      </tr>
      <tr>
          <td>依賴前提</td>
          <td>要求目標 framework 能產出 HTML（絕大多數 static generator 滿足）</td>
      </tr>
      <tr>
          <td>擴展性</td>
          <td>單次查詢下載量與全站大小脫鉤 — scaling 由 build time 吸收，不轉嫁到訪客</td>
      </tr>
  </tbody>
</table>
<p><strong>內建的一等公民特性</strong>：</p>
<ul>
<li><strong>Filter by facet</strong>：<code>data-pagefind-filter=&quot;type:card&quot;</code> 標在 HTML 元素上，UI 自動出現對應 filter checkbox</li>
<li><strong>Snippet highlighting</strong>：命中的關鍵字在結果摘要中高亮</li>
<li><strong>無障礙</strong>：Component UI（1.5.0+）內建 keyboard navigation、ARIA label、screen reader 公告</li>
</ul>
<p>這些特徵都源自「build 時產生 + 按需載入」這個核心選擇的延伸，不是外掛功能。</p>
<hr>
<h2 id="運作特徵">運作特徵</h2>
<h3 id="zh-tw-走-character-n-gram">zh-tw 走 character n-gram</h3>
<p><strong>核心定義</strong>：Pagefind 對非空白分詞語言採 n-gram — 以字元序列作為匹配單位，而非詞。</p>
<p><strong>行為</strong>：搜「負載平衡」能命中「負載平衡器」、「負載平衡器測試」等任何包含該字元序列的頁面。啟動時會印一行 stemming note，那是針對屈折變化語言（英文、德文）的 stemming 提示，對中文無意義也無限制。</p>
<p><strong>邊界</strong>：少數情境下跨詞邊界的字元組合會誤命中（例如搜「負載過」可能命中「負載過高」與「負載過往」）。在名詞為主的技術站影響極小。</p>
<h3 id="索引來自-rendered-html">索引來自 rendered HTML</h3>
<p><strong>核心定義</strong>：索引內容 = Pagefind 在 <code>public/*.html</code> 看到的可見文字與 meta tag。</p>
<p><strong>含義</strong>：想加入索引的欄位必須出現在 output HTML 上。想排除的區塊用 <code>data-pagefind-ignore</code> 標記。想作為 filter 的屬性用 <code>data-pagefind-filter=&quot;name:value&quot;</code>。</p>
<h3 id="default-ui-的樣式是-pagefind-自家風格">Default UI 的樣式是 Pagefind 自家風格</h3>
<p><strong>核心定義</strong>：<code>PagefindUI</code> component 有固定的視覺設計，透過 CSS variable 可微調顏色、圓角、spacing。</p>
<p><strong>含義</strong>：想要與 theme 完全融合有兩條路 — 覆寫 CSS variable（官方 docs 列出可覆寫清單），或改用 Pagefind JS API 自己組 UI（更完整客製）。</p>
<h3 id="build-pipeline-多一步">Build pipeline 多一步</h3>
<p><strong>核心定義</strong>：Pagefind 是 Hugo build 外的獨立步驟。</p>
<p><strong>含義</strong>：CI 與本地都要記得跑 <code>npx pagefind</code>。這個 blog 以 Makefile 的 <code>make site</code> 封裝 <code>hugo + pagefind</code> 兩步，把「記得」轉成 infrastructure 強制項。</p>
<hr>
<h2 id="適合的場景">適合的場景</h2>
<ul>
<li>靜態站、內容持續成長</li>
<li>部署在 GH Pages / Netlify / Cloudflare Pages 等純靜態平台</li>
<li>希望零外部依賴、完全自託管</li>
<li>內容以文字為主（blog、docs、knowledge base）</li>
<li>未來可能換 framework — 希望搜尋整合不隨之重寫</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>技術文章撰寫規範</title><link>https://tarrragon.github.io/blog/posts/%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%92%B0%E5%AF%AB%E8%A6%8F%E7%AF%84/</link><pubDate>Fri, 17 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%92%B0%E5%AF%AB%E8%A6%8F%E7%AF%84/</guid><description>&lt;h2 id="適用範圍">適用範圍&lt;/h2>
&lt;p>本規範適用於團隊內部各類型技術文章，包含：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>概念說明&lt;/strong>：技術原理、系統架構、協定規格&lt;/li>
&lt;li>&lt;strong>實作教學&lt;/strong>：操作步驟、範例、API 用法&lt;/li>
&lt;li>&lt;strong>架構決策&lt;/strong>：方案比較、選型紀錄、設計文件&lt;/li>
&lt;li>&lt;strong>除錯復盤&lt;/strong>：事故紀錄、疑難排解&lt;/li>
&lt;li>&lt;strong>技術評估&lt;/strong>：工具調研、可行性評估&lt;/li>
&lt;/ul>
&lt;p>核心目標：讓讀者能複製&lt;strong>思考過程&lt;/strong>，不只複製&lt;strong>結論&lt;/strong>。結論一行就能給，思考過程才是文章的主體。&lt;/p></description><content:encoded><![CDATA[<h2 id="適用範圍">適用範圍</h2>
<p>本規範適用於團隊內部各類型技術文章，包含：</p>
<ul>
<li><strong>概念說明</strong>：技術原理、系統架構、協定規格</li>
<li><strong>實作教學</strong>：操作步驟、範例、API 用法</li>
<li><strong>架構決策</strong>：方案比較、選型紀錄、設計文件</li>
<li><strong>除錯復盤</strong>：事故紀錄、疑難排解</li>
<li><strong>技術評估</strong>：工具調研、可行性評估</li>
</ul>
<p>核心目標：讓讀者能複製<strong>思考過程</strong>，不只複製<strong>結論</strong>。結論一行就能給，思考過程才是文章的主體。</p>
<p>本規範四條規則各自後方附有正反例，來源標註：</p>
<ul>
<li>正例多引自 <code>/work-log/</code> 目錄下的已成文文章</li>
<li>反例多引自同系列文章<strong>修改前的版本</strong>（實際寫作過程中踩到過的問題）</li>
</ul>
<hr>
<h2 id="一階段分層">一、階段分層</h2>
<h3 id="規則的商業邏輯">規則的商業邏輯</h3>
<p>技術文章的內容從「事實或需求」推導到「動作或結論」，這個過程有四個功能不同的階段。每個階段處理的問題不同、失敗模式不同：</p>
<ul>
<li><strong>觀察</strong>：描述現況、需求、事實或訊息</li>
<li><strong>判讀</strong>：說明本質、原理、問題所在</li>
<li><strong>策略</strong>：列出可選方案並比較</li>
<li><strong>執行</strong>：具體操作或實作</li>
</ul>
<p>四階段若混著寫，讀者無法區分「這一段失敗是哪個階段」，也無法判斷自己的類似情境卡在哪一步。文章只能被原封不動複製，不能被理解後套用。</p>
<h3 id="做法">做法</h3>
<p>每個主題段落應包含四階段。不同類型文章中四階段的對應：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>概念說明</th>
          <th>實作教學</th>
          <th>架構決策</th>
          <th>除錯復盤</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觀察</td>
          <td>需求或背景</td>
          <td>使用情境</td>
          <td>現況限制</td>
          <td>錯誤訊息</td>
      </tr>
      <tr>
          <td>判讀</td>
          <td>概念本質</td>
          <td>工具的位置與功能</td>
          <td>需求本質</td>
          <td>問題根因</td>
      </tr>
      <tr>
          <td>策略</td>
          <td>不同用法</td>
          <td>不同操作路徑</td>
          <td>可選方案</td>
          <td>可行解法</td>
      </tr>
      <tr>
          <td>執行</td>
          <td>程式碼範例</td>
          <td>操作步驟</td>
          <td>選定實作</td>
          <td>修復動作</td>
      </tr>
  </tbody>
</table>
<h3 id="實例對照">實例對照</h3>
<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"><span class="gu">## 錯誤
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>flutter_broadcasts_4m：Kotlin 1.8 vs Java 17 mismatch
</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="gu">## 解法
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="gu"></span>在 subprojects 加上：
</span></span><span class="line"><span class="ln">6</span><span class="cl">tasks.withType(KotlinCompile).configureEach { jvmTarget = &#39;17&#39; }</span></span></code></pre></div><p>問題：讀者能複製貼上，但無法回答「為什麼是 configureEach 不是其他方式」「遇到下一個類似問題怎麼想」。思考過程沒有被保存。</p>
<p><strong>正例</strong>：<code>gradle_reasoning_traps</code> 的節點 A 結構，每個節點顯式展開成「當下看到 → 判讀 → 可選策略 → 選擇與理由 → 結果 → 事後檢視」六個子段落。讀者看完後即使遇到不同錯誤訊息，也能套用同一個四階段推導。</p>
<h3 id="禁止事項">禁止事項</h3>
<ul>
<li>從觀察直接跳執行，省略判讀與策略</li>
<li>判讀留下未解問題就進策略</li>
<li>文章中出現「選擇」卻只列一個選項</li>
</ul>
<h3 id="例外">例外</h3>
<p>純介紹性段落（例如 API 參數說明）可以省略「策略」階段，但不得省略判讀。</p>
<hr>
<h2 id="二商業邏輯先於-case">二、商業邏輯先於 CASE</h2>
<h3 id="規則的商業邏輯-1">規則的商業邏輯</h3>
<p>技術文章的內容包含兩種資訊層次：</p>
<ul>
<li><strong>商業邏輯</strong>：系統層的概念（為什麼這件事存在、在體系中代表什麼）</li>
<li><strong>CASE</strong>：實例層的資料（具體的數值、路徑、屬性、設定）</li>
</ul>
<p>CASE 單獨存在沒有意義。「<code>jvmTarget = 17</code>」這個值需要「為什麼 JVM target 要一致」這個概念當容器才能被理解。</p>
<p>順序顛倒（先 CASE 後商業邏輯）等於讓讀者先記一組沒有脈絡的資料，再倒推抽象概念。這條認知路徑是反向的，多數讀者在倒推失敗後會放棄，即使專業讀者能勉強跟上也會覺得閱讀成本偏高。</p>
<h3 id="做法-1">做法</h3>
<p>每個主題段落包含兩層，順序不得顛倒：</p>
<h4 id="商業邏輯層">商業邏輯層</h4>
<ul>
<li>主題涉及的系統層概念</li>
<li>該概念為什麼存在、解決什麼問題</li>
<li>該類內容的共通本質</li>
</ul>
<p>此層不討論具體數值、路徑、屬性名。</p>
<h4 id="case-層">CASE 層</h4>
<ul>
<li>訊息或規格中的關鍵字對應商業邏輯的哪個位置</li>
<li>具體數值或內容</li>
<li>從 CASE 推論的結論</li>
</ul>
<h3 id="實例對照-1">實例對照</h3>
<p><strong>反例（直接給 CASE，預設讀者懂背景）</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"><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="k">-</span> 這個子專案的 Java task 產出 bytecode target = 17
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">-</span> 同一個子專案的 Kotlin task 產出 bytecode target = 1.8
</span></span><span class="line"><span class="ln">5</span><span class="cl">- 兩者不一致觸發 Kotlin 2.2 的 strict validation</span></span></code></pre></div><p>專業人士看了懂，但讀者若不知道「bytecode target」「strict validation」在系統中代表什麼，只能抓到字面字串，無法建立模型。</p>
<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"><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="gs">**這類錯誤的本質（商業邏輯）**</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">Android 每個 module 會分別編譯 Java 跟 Kotlin 原始碼，各自產出
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">JVM bytecode。每個 bytecode 有 target version，決定能在哪些 JVM
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">runtime 上跑。同 module 內若兩種語言產出不同 target，runtime
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">可能踩到 API 相容性問題。Kotlin 2.2 把這個從 warning 提升為 error。
</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="gs">**這次訊息具體說了什麼（CASE）**</span>：
</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="k">-</span> compileDebugJavaWithJavac (17) → Java 產出 target 17
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">-</span> compileDebugKotlin (1.8) → Kotlin 產出 target 1.8
</span></span><span class="line"><span class="ln">14</span><span class="cl">- 符合上面「module 內不一致」的 pattern</span></span></code></pre></div><p>（出自 <code>gradle_reasoning_traps</code> 節點 A 修改後的版本。修改前是反例那種寫法，修改後加入商業邏輯層。）</p>
<h3 id="完成標準">完成標準</h3>
<p>段落結束前，讀者應能回答：</p>
<ol>
<li>這個主題在系統中為什麼重要？</li>
<li>這個主題的具體案例對應商業邏輯的哪個位置？</li>
</ol>
<p>若只能回答第二題，商業邏輯層不足。</p>
<hr>
<h2 id="三評估用內在屬性">三、評估用內在屬性</h2>
<h3 id="規則的商業邏輯-2">規則的商業邏輯</h3>
<p>技術文章經常包含選擇或比較：選 A 不選 B、用 X 不用 Y、選這個架構不選另一個。選擇的優劣取決於方案的<strong>內在屬性</strong>，不取決於執行者的<strong>時間消耗</strong>：</p>
<ul>
<li><strong>內在屬性</strong>：覆蓋完整性、風險、可逆性、維護成本、可理解性、依賴前提</li>
<li><strong>時間消耗</strong>：實作要多久、多少人工時</li>
</ul>
<p>時間消耗是執行者的資源考量，跟方案本身的正確性無關。用時間當評估維度會造成結構性偏差：<strong>投資型方案</strong>（擴大影響、建立基礎）看起來總是比<strong>消費型方案</strong>（解當前問題）差，但兩者能力的性質不同，不應以同一量度比較。</p>
<p>讀者讀到時間評估會誤以為「這個方案比較好」，但真正該學到的是「兩個方案各自適合什麼情境」。</p>
<h3 id="做法-2">做法</h3>
<p>每個方案比較必須包含下列維度中至少三項：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>覆蓋完整性</td>
          <td>處理所有同類情境，還是只處理當前</td>
      </tr>
      <tr>
          <td>風險</td>
          <td>失敗機率與代價</td>
      </tr>
      <tr>
          <td>可逆性</td>
          <td>出錯能否回滾</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>長期需要多少精力</td>
      </tr>
      <tr>
          <td>可理解性</td>
          <td>未來接手者能否理解</td>
      </tr>
      <tr>
          <td>依賴前提</td>
          <td>建立在什麼假設上，前提變了會如何</td>
      </tr>
  </tbody>
</table>
<h3 id="實例對照-2">實例對照</h3>
<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">節點 A 事後回顧：
</span></span><span class="line"><span class="ln">2</span><span class="cl">這一步多 2 分鐘掃一遍 pub-cache，但可以省下後來約 30 分鐘的
</span></span><span class="line"><span class="ln">3</span><span class="cl">build-炸-修循環。明顯划算。</span></span></code></pre></div><p>讀者學到的是「划不划算」這個執行層結論，沒學到兩個方案的結構性差異。</p>
<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">節點 A 事後檢視：
</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">若當下選擇「A2 + 同步盤點 pub-cache」：
</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">-</span> 優點：一次盤點所有同類地雷，修復範圍完整；避免之後
</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></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></code></pre></div><p>（出自 <code>gradle_reasoning_traps</code> 節點 A 事後檢視。修改前是反例版本，修改後改用「覆蓋完整性」「範圍完整」這類內在屬性。）</p>
<h3 id="禁止事項-1">禁止事項</h3>
<p>不得以時間成本作為主要評估維度，包含：</p>
<ul>
<li>「這方案比較快」</li>
<li>「多花 X 分鐘省下 Y 分鐘」</li>
<li>「立即可完成」</li>
</ul>
<hr>
<h2 id="四事後檢視看判讀品質">四、事後檢視看判讀品質</h2>
<h3 id="規則的商業邏輯-3">規則的商業邏輯</h3>
<p>技術文章的品質檢視（review、事故檢討、復盤）常只看「結論對不對」，但多數失敗的根源在<strong>判讀階段</strong>：</p>
<ul>
<li>判讀未確認的推論被當結論</li>
<li>判讀觀察範圍不足（只看單點）</li>
<li>判讀用類比代替機制驗證</li>
</ul>
<p>這類問題會表現成「結論失敗」的樣子，但改善方向完全不同：</p>
<ul>
<li>結論失敗 → 下次多列幾個選項</li>
<li>判讀失敗 → 下次判讀要更嚴謹、更廣、更實證</li>
</ul>
<p>兩者混為一談會得到「要更仔細」這種無法行動的結論。</p>
<h3 id="做法-3">做法</h3>
<p>文章或決策完成後，必須回答下列四題：</p>
<ol>
<li>判讀階段的「需要確認」項目是否全部解答？</li>
<li>觀察範圍是否涵蓋同類情境，或僅處理當前一個？</li>
<li>推論中的類比假設是否驗證？</li>
<li>策略列舉是否完整？</li>
</ol>
<p>任一題答「否」，對應失敗類型必須明確標示：</p>
<table>
  <thead>
      <tr>
          <th>答「否」的題</th>
          <th>失敗類型</th>
          <th>改善方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>題 1</td>
          <td>未確認推論帶入結論</td>
          <td>判讀完成標準要收緊</td>
      </tr>
      <tr>
          <td>題 2</td>
          <td>觀察範圍不足</td>
          <td>擴大搜尋類似情境</td>
      </tr>
      <tr>
          <td>題 3</td>
          <td>類比代替驗證</td>
          <td>機制差異需實證</td>
      </tr>
      <tr>
          <td>題 4</td>
          <td>策略列舉不足</td>
          <td>至少列兩個選項</td>
      </tr>
  </tbody>
</table>
<h3 id="實例對照-3">實例對照</h3>
<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">這次修復走了彎路，下次應該更仔細。</span></span></code></pre></div><p>無法行動。下次遇到類似情境仍會犯同樣錯誤。</p>
<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">七個節點中四個失敗，分類：
</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">| 節點 C | 需要新資訊（toolchain 時機） | 節點 B 判讀留下「需要確認」但沒補 |
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">| 節點 D1 | 對稱假設 | 節點 D 判讀用「結構對稱」取代「機制驗證」 |
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">| 節點 F | 方法呼叫時機 | 節點 E 判讀沒展開 API 的兩階段行為 |
</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></span><span class="line"><span class="ln">10</span><span class="cl">進入策略時，還帶著未解決的問題。</span></span></code></pre></div><p>（出自 <code>gradle_reasoning_traps</code> 的「整個過程的決策品質檢視」段落。）</p>
<p>這樣的檢視產出具體改善方向：「判讀完成標準要收緊」、「機制差異需實證」，而不是模糊的「要更仔細」。</p>
<hr>
<h2 id="五判讀徵兆對照">五、判讀徵兆對照</h2>
<p>技術文章撰寫中常見的徵兆與對應的判讀要求。看到徵兆時，作者必須回答對應問題。</p>
<h3 id="徵兆單一位置指向檔案行號屬性名">徵兆：單一位置指向（檔案、行號、屬性名）</h3>
<p>該位置是「現場」，不一定是「根因」。必須追問：</p>
<ul>
<li>為什麼是這個位置出問題？</li>
<li>是目標的狀態錯了，還是呼叫的時機錯了？</li>
</ul>
<h3 id="徵兆同類情境第二次出現">徵兆：同類情境第二次出現</h3>
<p>前次處理範圍不完整。必須追問：</p>
<ul>
<li>還有哪些同類情境？</li>
<li>是否有共通原因？</li>
</ul>
<h3 id="徵兆修改看似合理但未生效">徵兆：修改看似合理但未生效</h3>
<p>時機或機制假設錯誤。必須追問：</p>
<ul>
<li>修改的生效時機是否晚於覆蓋對象？</li>
<li>覆蓋機制是否真的能蓋過目標？</li>
</ul>
<h3 id="徵兆finalalreadycannot這類字眼">徵兆：「final」「already」「cannot」這類字眼</h3>
<p>目標已進入不可修改狀態。必須追問：</p>
<ul>
<li>目標何時進入該狀態？</li>
<li>修改能否提前到狀態轉換之前？</li>
</ul>
<h3 id="徵兆inconsistentmismatch這類字眼">徵兆：「inconsistent」「mismatch」這類字眼</h3>
<p>兩部分不一致。必須追問：</p>
<ul>
<li>哪一邊是正確的目標值？</li>
<li>不一致的方向決定治理施加在哪一邊？</li>
</ul>
<h3 id="實例">實例</h3>
<p>這組徵兆對照表的完整應用案例見 <code>gradle_evaluation_order_traps</code>，該文將三種 Gradle 時序錯誤（<code>already evaluated</code>、<code>is final</code>、覆寫沒生效）各自對應到同一個底層問題（時機錯位），並給出對應的解法方向。這就是「看到徵兆 → 查表 → 推出判讀結論」的標準路徑。</p>
<hr>
<h2 id="六術語">六、術語</h2>
<table>
  <thead>
      <tr>
          <th>術語</th>
          <th>定義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>商業邏輯</td>
          <td>系統層次的概念說明，不涉及具體值</td>
      </tr>
      <tr>
          <td>CASE</td>
          <td>具體內容（數值、路徑、屬性名）</td>
      </tr>
      <tr>
          <td>判讀</td>
          <td>從事實推導本質的過程</td>
      </tr>
      <tr>
          <td>策略</td>
          <td>可選方案</td>
      </tr>
      <tr>
          <td>現場</td>
          <td>訊息直接指向的位置</td>
      </tr>
      <tr>
          <td>根因</td>
          <td>底層原因，不一定等於現場</td>
      </tr>
      <tr>
          <td>投資型策略</td>
          <td>有長期回報的方案（擴大覆蓋、建立認知）</td>
      </tr>
      <tr>
          <td>消費型策略</td>
          <td>只處理當前問題的方案</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="七提交自檢">七、提交自檢</h2>
<p>提交文章前自檢：</p>
<ul>
<li><input disabled="" type="checkbox"> 四階段（觀察／判讀／策略／執行）完整，或已說明可省略的階段</li>
<li><input disabled="" type="checkbox"> 每個主題段落先商業邏輯後 CASE</li>
<li><input disabled="" type="checkbox"> 判讀中所有「需要確認」項目已解答或註明可暫不確認</li>
<li><input disabled="" type="checkbox"> 每個方案比較至少三個評估維度</li>
<li><input disabled="" type="checkbox"> 未以時間成本為主要評估維度</li>
<li><input disabled="" type="checkbox"> 事後檢視四題已回答</li>
<li><input disabled="" type="checkbox"> 失敗類型已標示（若有）</li>
</ul>
<hr>
<h2 id="參考範例">參考範例</h2>
<p>本規範四條規則的完整應用案例：</p>
<ul>
<li><a href="/blog/work-log/gradle-jvm-target-%E9%99%A4%E9%8C%AF%E5%BE%A9%E7%9B%A4%E4%B8%83%E5%80%8B%E7%AF%80%E9%BB%9E%E7%9A%84%E7%AD%96%E7%95%A5%E6%AC%8A%E8%A1%A1/" data-link-title="Gradle JVM target 除錯復盤：七個節點的策略權衡" data-link-desc="Gradle JVM target 不一致的除錯決策復盤，重點在每步的策略權衡與走過的彎路。">Gradle JVM target 除錯復盤：七個節點的策略權衡</a> — 全四條規則的完整應用，每個節點皆含觀察／判讀／策略／結果／事後檢視五層</li>
<li><a href="/blog/work-log/gradle-%E5%BC%B7%E5%88%B6%E8%A6%86%E5%AF%AB-plugin-%E7%9A%84-jvm-targetkotlin-%E8%88%87-java-%E7%9A%84%E5%88%87%E5%85%A5%E9%BB%9E%E4%B8%8D%E5%B0%8D%E7%A8%B1/" data-link-title="Gradle 強制覆寫 plugin 的 JVM target：Kotlin 與 Java 的切入點不對稱" data-link-desc="Kotlin / AGP 升級後 build 報 `Inconsistent JVM-target compatibility`。為何要強制覆寫 plugin 的 JVM target，以及 Kotlin 與 Java 設定切入點的不對稱。">Gradle 強制覆寫 plugin 的 JVM target：Kotlin 與 Java 的切入點不對稱</a> — 商業邏輯先於 CASE 的範例（先講 Kotlin plugin 與 AGP 的機制差異，再講具體寫法）</li>
<li><a href="/blog/work-log/gradle-configuration-%E6%99%82%E5%BA%8F%E9%99%B7%E9%98%B1afterevaluateevaluationdependsonfinalized-properties/" data-link-title="Gradle Configuration 時序陷阱：afterEvaluate、evaluationDependsOn、finalized properties" data-link-desc="Gradle 報 `Cannot run Project.afterEvaluate ... already evaluated` 或 `property is final`。時序錯誤同源於 callback 註冊太晚或屬性賦值太晚，附各 API 的正確時機。">Gradle Configuration 時序陷阱：afterEvaluate、evaluationDependsOn、finalized properties</a> — 判讀徵兆對照的完整應用</li>
<li><a href="/blog/work-log/%E7%82%BA%E4%BB%80%E9%BA%BC-bug-%E5%9C%A8%E5%90%88%E4%BD%B5%E5%BE%8C%E6%89%8D%E7%88%86gradle-cache-%E6%8E%A9%E8%93%8B%E6%BD%9B%E4%BC%8F%E5%95%8F%E9%A1%8C%E7%9A%84%E9%82%8F%E8%BC%AF/" data-link-title="為什麼 Bug 在合併後才爆：Gradle Cache 掩蓋潛伏問題的邏輯" data-link-desc="feature branch build 正常、合併到 main 後才爆、但合併前 main 也沒錯。根因早已潛伏，Gradle cache 掩蓋、合併只是觸發條件。">為什麼 Bug 在合併後才爆：Gradle Cache 掩蓋潛伏問題的邏輯</a> — 事後檢視看判讀品質的範例（將「合併造成」重新歸因為「判讀時把觸發條件當根因」）</li>
</ul>]]></content:encoded></item><item><title>用 Claude Code GitHub Actions 自動除錯 CI 建置失敗</title><link>https://tarrragon.github.io/blog/posts/%E7%94%A8-claude-code-github-actions-%E8%87%AA%E5%8B%95%E9%99%A4%E9%8C%AF-ci-%E5%BB%BA%E7%BD%AE%E5%A4%B1%E6%95%97/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E7%94%A8-claude-code-github-actions-%E8%87%AA%E5%8B%95%E9%99%A4%E9%8C%AF-ci-%E5%BB%BA%E7%BD%AE%E5%A4%B1%E6%95%97/</guid><description>&lt;h2 id="這是什麼">這是什麼&lt;/h2>
&lt;p>&lt;a href="https://github.com/anthropics/claude-code-action">Claude Code GitHub Actions&lt;/a> 讓 Claude 直接參與你的 GitHub 工作流程，主要功能：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>互動式助手&lt;/strong> — 在 PR/Issue 留言 &lt;code>@claude&lt;/code>，Claude 會分析程式碼並回覆&lt;/li>
&lt;li>&lt;strong>自動 Code Review&lt;/strong> — PR 開啟時自動審查變更&lt;/li>
&lt;li>&lt;strong>CI 除錯修復&lt;/strong> — build 失敗時自動分析錯誤並修復&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="這是什麼">這是什麼</h2>
<p><a href="https://github.com/anthropics/claude-code-action">Claude Code GitHub Actions</a> 讓 Claude 直接參與你的 GitHub 工作流程，主要功能：</p>
<ul>
<li><strong>互動式助手</strong> — 在 PR/Issue 留言 <code>@claude</code>，Claude 會分析程式碼並回覆</li>
<li><strong>自動 Code Review</strong> — PR 開啟時自動審查變更</li>
<li><strong>CI 除錯修復</strong> — build 失敗時自動分析錯誤並修復</li>
</ul>
<p>完整功能說明參考 <a href="https://code.claude.com/docs/en/github-actions">官方文件</a>。</p>
<h2 id="設定方式">設定方式</h2>
<h3 id="install-github-app推薦"><code>/install-github-app</code>（推薦）</h3>
<p>在 Claude Code 終端執行 <code>/install-github-app</code>，它會引導你完成所有設定。</p>
<p>流程中的關鍵步驟：</p>
<ol>
<li><strong>選擇 repo</strong> — 指定要安裝的 GitHub repository</li>
<li><strong>安裝 Claude GitHub App</strong> — 自動安裝到指定 repo，授予 Contents、Issues、Pull requests 的 Read &amp; Write 權限</li>
<li><strong>選擇認證方式</strong> — 選擇 <strong>long-life token</strong> 會產生 OAuth token，自動寫入 GitHub Secrets 為 <code>CLAUDE_CODE_OAUTH_TOKEN</code></li>
<li><strong>建立 workflow 檔案</strong> — 自動建立並 push 兩個 workflow：
<ul>
<li><code>claude.yml</code> — <code>@claude</code> 互動回覆</li>
<li><code>claude-code-review.yml</code> — PR 自動 code review</li>
</ul>
</li>
</ol>
<p>完成後不需要額外設定。</p>
<h3 id="手動設定使用-anthropic-api-key">手動設定（使用 Anthropic API Key）</h3>
<p>如果不想用 <code>/install-github-app</code>，可以手動操作：</p>
<ol>
<li>前往 <a href="https://github.com/apps/claude">github.com/apps/claude</a> 安裝 App 到你的 repo</li>
<li>到 repo 的 <strong>Settings → Secrets and variables → Actions</strong>，新增 <code>ANTHROPIC_API_KEY</code></li>
<li>手動建立 workflow 檔案到 <code>.github/workflows/</code></li>
</ol>
<p>兩種認證方式的差異：</p>
<table>
  <thead>
      <tr>
          <th>認證方式</th>
          <th>Secret 名稱</th>
          <th>適用對象</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OAuth Token</td>
          <td><code>CLAUDE_CODE_OAUTH_TOKEN</code></td>
          <td>Pro/Max 用戶，<code>/install-github-app</code> 自動設定</td>
      </tr>
      <tr>
          <td>API Key</td>
          <td><code>ANTHROPIC_API_KEY</code></td>
          <td>直接使用 Anthropic API，需手動到 <a href="https://console.anthropic.com">console.anthropic.com</a> 取得</td>
      </tr>
  </tbody>
</table>
<h2 id="加入-ci-自動除錯">加入 CI 自動除錯</h2>
<p><code>/install-github-app</code> 建立的 workflow 只處理 <code>@claude</code> 互動和 code review。如果你想在 <strong>build 失敗時自動觸發 Claude 修復</strong>，需要修改既有的 deploy workflow。</p>
<p>首先，補上 Claude 需要的權限（原本可能只有 <code>contents: read</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="nt">permissions</span><span class="p">:</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">contents</span><span class="p">:</span><span class="w"> </span><span class="l">write       </span><span class="w"> </span><span class="c"># Claude 需要寫入修復後的檔案</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">pull-requests</span><span class="p">:</span><span class="w"> </span><span class="l">write  </span><span class="w"> </span><span class="c"># Claude 可能需要建立 PR</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">issues</span><span class="p">:</span><span class="w"> </span><span class="l">write         </span><span class="w"> </span><span class="c"># Claude 回報結果</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">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write          </span><span class="w"> </span><span class="c"># 原本的 deploy 權限</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">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write       </span><span class="w"> </span><span class="c"># 原本的 deploy 權限</span></span></span></code></pre></div><p>然後在 build 步驟加入 Claude 除錯邏輯：</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="c"># 在原本的 build step 加上 continue-on-error 和 id</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build</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">id</span><span class="p">:</span><span class="w"> </span><span class="l">hugo-build</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">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify 2&gt;&amp;1 | tee hugo-build-output.txt</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">continue-on-error</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c"># Build 失敗時觸發 Claude 除錯</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">Claude Debug on Build Failure</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">if</span><span class="p">:</span><span class="w"> </span><span class="l">steps.hugo-build.outcome == &#39;failure&#39;</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">uses</span><span class="p">:</span><span class="w"> </span><span class="l">anthropics/claude-code-action@v1</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">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="c"># 依你的認證方式擇一</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">claude_code_oauth_token</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="c"># anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}</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">prompt</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">      Hugo build failed. Here is the error output:
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="sd">      $(cat hugo-build-output.txt)
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="sd">      Please analyze the error, find the problematic file(s),
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="sd">      fix the YAML front matter or content issue, and commit the fix.</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">claude_args</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;--max-turns 10&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="c"># 修復後重新 build 驗證</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Retry build after fix</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">steps.hugo-build.outcome == &#39;failure&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify</span></span></span></code></pre></div><p>核心設計：</p>
<ol>
<li><code>continue-on-error: true</code> — build 失敗不中斷流程，讓後續 Claude 步驟有機會執行</li>
<li><code>if: steps.hugo-build.outcome == 'failure'</code> — 只在失敗時觸發，正常 build 不消耗 API 額度</li>
<li>修復後重新 <code>hugo --minify</code> 驗證是否成功</li>
</ol>
<h2 id="計費方式">計費方式</h2>
<p>計費取決於你使用哪種認證方式：</p>
<table>
  <thead>
      <tr>
          <th>認證方式</th>
          <th>計費來源</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OAuth Token</td>
          <td><strong>訂閱額度</strong>（Pro/Max）</td>
          <td>跟 claude.ai 網頁、Claude Code CLI、Claude Desktop <strong>共用同一個額度池</strong></td>
      </tr>
      <tr>
          <td>API Key</td>
          <td><strong>獨立 API 計費</strong></td>
          <td>按 token 用量付費，與訂閱額度完全分開</td>
      </tr>
  </tbody>
</table>
<p>OAuth token 的額度是共用的，GitHub Actions 跑多了會擠壓你日常在 claude.ai 和 CLI 的使用額度。如果 CI 觸發頻繁，建議改用 API Key 避免互相影響。</p>
<p>詳細的費率可參考 <a href="https://www.anthropic.com/pricing">Claude 定價頁面</a>。</p>
<h3 id="降低成本的設定">降低成本的設定</h3>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--max-turns 10</code></td>
          <td>限制迭代次數，避免無限循環</td>
      </tr>
      <tr>
          <td>只在 <code>failure</code> 時觸發</td>
          <td>正常 build 不消耗 API 額度</td>
      </tr>
      <tr>
          <td><code>@claude</code> 觸發詞</td>
          <td>互動模式只在明確呼叫時才啟動</td>
      </tr>
  </tbody>
</table>
<h2 id="搭配-claudemd">搭配 CLAUDE.md</h2>
<p>在 repo 根目錄建立 <code>CLAUDE.md</code>，Claude 會自動讀取作為上下文，提升修復準確度。</p>
<h2 id="參考資料">參考資料</h2>
<ul>
<li><a href="https://code.claude.com/docs/en/github-actions">Claude Code GitHub Actions 官方文件</a></li>
<li><a href="https://github.com/anthropics/claude-code-action">claude-code-action GitHub Repo</a></li>
<li><a href="https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md">Setup Guide</a></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>Hugo 部落格側邊章節導航 (TOC) 完整實現指南</title><link>https://tarrragon.github.io/blog/posts/hugo-%E9%83%A8%E8%90%BD%E6%A0%BC%E5%81%B4%E9%82%8A%E7%AB%A0%E7%AF%80%E5%B0%8E%E8%88%AA-toc-%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%E5%81%B4%E9%82%8A%E7%AB%A0%E7%AF%80%E5%B0%8E%E8%88%AA-toc-%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>因為文章太長，閱讀困難，所以看到別人部落格有的TOC功能，就找AI復刻&lt;/p>
&lt;h3 id="需求">需求&lt;/h3>
&lt;ul>
&lt;li>使用TOC快速定位&lt;/li>
&lt;li>TOC隨著本文滾動定位當前位置&lt;/li>
&lt;li>手機寬度下不顯示TOC，改用回到頁首的懸浮按鈕取代&lt;/li>
&lt;/ul>
&lt;h2 id="1-hugo-配置設定">1. Hugo 配置設定&lt;/h2>
&lt;h3 id="11-啟用-toc-功能">1.1 啟用 TOC 功能&lt;/h3>
&lt;p>在 &lt;code>hugo.toml&lt;/code> 中啟用 TOC 功能：&lt;/p>


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

```toml
[markup]
 [markup.tableOfContents]
 startLevel = 2
 endLevel = 4
 ordered = false
```


&lt;/details>
&lt;p>&lt;strong>參數說明&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;code>startLevel = 2&lt;/code>：從 H2 標題開始生成 TOC&lt;/li>
&lt;li>&lt;code>endLevel = 4&lt;/code>：到 H4 標題結束&lt;/li>
&lt;li>&lt;code>ordered = false&lt;/code>：使用無序列表格式&lt;/li>
&lt;/ul>
&lt;h2 id="2-自定義文章佈局">2. 自定義文章佈局&lt;/h2>
&lt;h3 id="21-建立自定義-singlehtml">2.1 建立自定義 single.html&lt;/h3>
&lt;p>在 &lt;code>layouts/_default/single.html&lt;/code> 中實現新的佈局結構：&lt;/p>


&lt;details>
 &lt;summary>點擊查看完整 HTML 佈局程式碼&lt;/summary>
 

```html
{{ define "main" }}
&lt;!-- 側邊章節導航 - 獨立於主內容區域 -->
&lt;aside class="toc-sidebar">
 &lt;h3>章節目錄&lt;/h3>
 {{ if .TableOfContents }}
 {{ .TableOfContents }}
 {{ else }}
 &lt;p style="color: rgba(255, 255, 255, 0.5); font-size: 0.85rem; margin: 0;">
 此文章沒有章節標題
 &lt;/p>
 {{ end }}
&lt;/aside>

&lt;!-- 文章內容 - 保持原有的置中佈局 -->
&lt;article class="article-content">
 {{ if not .Params.menu }}
 &lt;h1>{{ .Title }}&lt;/h1>
 &lt;p class="byline">
 &lt;time datetime='{{ .Date.Format "2006-01-02" }}' pubdate>
 {{ .Date.Format (default "2006-01-02" .Site.Params.dateFormat) }}
 &lt;/time>
 {{ with .Params.author }}· {{.}}{{ end }}
 &lt;/p>
 {{ end }}
 
 &lt;content>
 {{ .Content }}
 &lt;/content>
 
 &lt;p>
 {{ range (.GetTerms "tags") }}
 &lt;a class="blog-tags" href="{{ .RelPermalink }}">#{{ lower .LinkTitle }}&lt;/a>
 {{ end }}
 &lt;/p>
 
 {{ if not .Params.hideReply }}
 {{ with .Site.Params.author.email }}
 &lt;p>
 &lt;a href='mailto:{{ . }}?subject={{ i18n "email-subject" }}"{{ default $.Site.Title $.Page.Title }}"'>
 {{ i18n "email-reply" }} ↪
 &lt;/a>
 &lt;/p>
 {{ end }}
 {{ end }}
&lt;/article>

&lt;!-- 回到頂部按鈕 -->
&lt;button id="back-to-top" class="back-to-top-btn" aria-label="回到頂部">
 &lt;svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 &lt;path d="m18 15-6-6-6 6"/>
 &lt;/svg>
&lt;/button>

&lt;!-- 章節導航互動腳本 -->
&lt;script>
document.addEventListener('DOMContentLoaded', function() {
 // 檢查是否在手機版（隱藏 TOC 時不需要執行）
 const isMobile = window.innerWidth &lt;= 768;
 
 if (isMobile) {
 return; // 手機版不執行 TOC 相關功能
 }

 // 確保所有標題都有 ID
 const headings = document.querySelectorAll('.article-content h2, .article-content h3, .article-content h4');
 
 headings.forEach(function(heading) {
 // 如果沒有 ID，則生成一個
 if (!heading.id) {
 // 從標題文字生成 ID
 const text = heading.textContent.trim();
 const id = text.toLowerCase()
 .replace(/[^\w\s-]/g, '') // 移除特殊字符
 .replace(/\s+/g, '-') // 空格替換為連字符
 .replace(/-+/g, '-') // 多個連字符合併為一個
 .replace(/^-|-$/g, ''); // 移除開頭和結尾的連字符
 
 if (id) {
 heading.id = id;
 }
 }
 });

 // 更新側邊導航連結的 href
 const tocLinks = document.querySelectorAll('.toc-sidebar a[href^="#"]');
 tocLinks.forEach(function(link) {
 const href = link.getAttribute('href');
 if (href &amp;&amp; href.startsWith('#')) {
 const targetId = href.substring(1);
 const targetElement = document.getElementById(targetId);
 if (targetElement) {
 link.addEventListener('click', function(e) {
 e.preventDefault();
 targetElement.scrollIntoView({
 behavior: 'smooth',
 block: 'start'
 });
 });
 }
 }
 });

 // 滾動時高亮當前章節並自動滾動側邊欄
 function updateActiveSection() {
 const sections = document.querySelectorAll('.article-content h2, .article-content h3, .article-content h4');
 const tocLinks = document.querySelectorAll('.toc-sidebar a[href^="#"]');
 const tocSidebar = document.querySelector('.toc-sidebar');
 
 let currentSection = '';
 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
 
 sections.forEach(function(section) {
 const sectionTop = section.offsetTop - 100; // 提前 100px 觸發
 if (scrollTop >= sectionTop) {
 currentSection = section.id;
 }
 });
 
 // 移除所有 active 類別
 tocLinks.forEach(function(link) {
 link.classList.remove('active');
 });
 
 // 為當前章節添加 active 類別並自動滾動側邊欄
 if (currentSection) {
 const activeLink = document.querySelector('.toc-sidebar a[href="#' + currentSection + '"]');
 if (activeLink) {
 activeLink.classList.add('active');
 
 // 自動滾動側邊欄到當前章節位置
 if (tocSidebar &amp;&amp; activeLink) {
 // 獲取側邊欄的滾動容器信息
 const sidebarScrollTop = tocSidebar.scrollTop;
 const sidebarHeight = tocSidebar.clientHeight;
 const sidebarScrollHeight = tocSidebar.scrollHeight;
 
 // 獲取當前連結在側邊欄中的位置
 const linkOffsetTop = activeLink.offsetTop;
 const linkHeight = activeLink.offsetHeight;
 
 // 計算連結相對於側邊欄可視區域的位置
 const linkTop = linkOffsetTop - sidebarScrollTop;
 const linkBottom = linkTop + linkHeight;
 
 // 設定緩衝區域（側邊欄高度的 20%）
 const bufferZone = Math.max(20, sidebarHeight * 0.2);
 const safeTop = bufferZone;
 const safeBottom = sidebarHeight - bufferZone;
 
 // 檢查是否需要滾動
 let needsScroll = false;
 let targetScrollTop = sidebarScrollTop;
 
 if (linkTop &lt; safeTop) {
 // 連結太靠近頂部，滾動到連結上方預留緩衝空間
 targetScrollTop = linkOffsetTop - bufferZone;
 needsScroll = true;
 } else if (linkBottom > safeBottom) {
 // 連結太靠近底部，滾動到連結下方預留緩衝空間
 targetScrollTop = linkOffsetTop + linkHeight - sidebarHeight + bufferZone;
 needsScroll = true;
 }
 
 // 如果需要滾動，執行滾動
 if (needsScroll) {
 // 確保滾動位置在有效範圍內
 const maxScrollTop = sidebarScrollHeight - sidebarHeight;
 targetScrollTop = Math.max(0, Math.min(targetScrollTop, maxScrollTop));
 
 // 只有當目標位置與當前位置差距足夠大時才滾動
 if (Math.abs(targetScrollTop - sidebarScrollTop) > 10) {
 tocSidebar.scrollTop = targetScrollTop;
 }
 }
 }
 }
 }
 }

 // 監聽滾動事件
 window.addEventListener('scroll', updateActiveSection);
 
 // 初始化時執行一次
 updateActiveSection();
});

// 回到頂部按鈕功能（所有裝置都支援）
document.addEventListener('DOMContentLoaded', function() {
 const backToTopBtn = document.getElementById('back-to-top');
 
 if (!backToTopBtn) return;
 
 // 顯示/隱藏按鈕
 function toggleBackToTopBtn() {
 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
 
 if (scrollTop > 300) {
 backToTopBtn.style.display = 'flex';
 backToTopBtn.classList.add('visible');
 } else {
 backToTopBtn.style.display = 'none';
 backToTopBtn.classList.remove('visible');
 }
 }
 
 // 回到頂部功能
 function scrollToTop() {
 window.scrollTo({
 top: 0,
 behavior: 'smooth'
 });
 }
 
 // 綁定事件
 window.addEventListener('scroll', toggleBackToTopBtn);
 backToTopBtn.addEventListener('click', scrollToTop);
 
 // 初始化
 toggleBackToTopBtn();
});
&lt;/script>
{{ end }}
```


&lt;/details>
&lt;h2 id="3-css-樣式設計">3. CSS 樣式設計&lt;/h2>
&lt;h3 id="31-側邊欄樣式">3.1 側邊欄樣式&lt;/h3>
&lt;p>在 &lt;code>layouts/partials/custom_head.html&lt;/code> 中添加 CSS：&lt;/p></description><content:encoded><![CDATA[<h2 id="概述">概述</h2>
<p>因為文章太長，閱讀困難，所以看到別人部落格有的TOC功能，就找AI復刻</p>
<h3 id="需求">需求</h3>
<ul>
<li>使用TOC快速定位</li>
<li>TOC隨著本文滾動定位當前位置</li>
<li>手機寬度下不顯示TOC，改用回到頁首的懸浮按鈕取代</li>
</ul>
<h2 id="1-hugo-配置設定">1. Hugo 配置設定</h2>
<h3 id="11-啟用-toc-功能">1.1 啟用 TOC 功能</h3>
<p>在 <code>hugo.toml</code> 中啟用 TOC 功能：</p>


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

```toml
[markup]
  [markup.tableOfContents]
    startLevel = 2
    endLevel = 4
    ordered = false
```


</details>
<p><strong>參數說明</strong>：</p>
<ul>
<li><code>startLevel = 2</code>：從 H2 標題開始生成 TOC</li>
<li><code>endLevel = 4</code>：到 H4 標題結束</li>
<li><code>ordered = false</code>：使用無序列表格式</li>
</ul>
<h2 id="2-自定義文章佈局">2. 自定義文章佈局</h2>
<h3 id="21-建立自定義-singlehtml">2.1 建立自定義 single.html</h3>
<p>在 <code>layouts/_default/single.html</code> 中實現新的佈局結構：</p>


<details>
  <summary>點擊查看完整 HTML 佈局程式碼</summary>
  

```html
{{ define "main" }}
<!-- 側邊章節導航 - 獨立於主內容區域 -->
<aside class="toc-sidebar">
  <h3>章節目錄</h3>
  {{ if .TableOfContents }}
    {{ .TableOfContents }}
  {{ else }}
    <p style="color: rgba(255, 255, 255, 0.5); font-size: 0.85rem; margin: 0;">
      此文章沒有章節標題
    </p>
  {{ end }}
</aside>

<!-- 文章內容 - 保持原有的置中佈局 -->
<article class="article-content">
  {{ if not .Params.menu }}
  <h1>{{ .Title }}</h1>
  <p class="byline">
    <time datetime='{{ .Date.Format "2006-01-02" }}' pubdate>
      {{ .Date.Format (default "2006-01-02" .Site.Params.dateFormat) }}
    </time>
    {{ with .Params.author }}· {{.}}{{ end }}
  </p>
  {{ end }}
  
  <content>
    {{ .Content }}
  </content>
  
  <p>
    {{ range (.GetTerms "tags") }}
      <a class="blog-tags" href="{{ .RelPermalink }}">#{{ lower .LinkTitle }}</a>
    {{ end }}
  </p>
  
  {{ if not .Params.hideReply }}
  {{ with .Site.Params.author.email }}
    <p>
      <a href='mailto:{{ . }}?subject={{ i18n "email-subject" }}"{{ default $.Site.Title $.Page.Title }}"'>
        {{ i18n "email-reply" }} ↪
      </a>
    </p>
  {{ end }}
  {{ end }}
</article>

<!-- 回到頂部按鈕 -->
<button id="back-to-top" class="back-to-top-btn" aria-label="回到頂部">
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <path d="m18 15-6-6-6 6"/>
  </svg>
</button>

<!-- 章節導航互動腳本 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
  // 檢查是否在手機版（隱藏 TOC 時不需要執行）
  const isMobile = window.innerWidth <= 768;
  
  if (isMobile) {
    return; // 手機版不執行 TOC 相關功能
  }

  // 確保所有標題都有 ID
  const headings = document.querySelectorAll('.article-content h2, .article-content h3, .article-content h4');
  
  headings.forEach(function(heading) {
    // 如果沒有 ID，則生成一個
    if (!heading.id) {
      // 從標題文字生成 ID
      const text = heading.textContent.trim();
      const id = text.toLowerCase()
        .replace(/[^\w\s-]/g, '') // 移除特殊字符
        .replace(/\s+/g, '-')     // 空格替換為連字符
        .replace(/-+/g, '-')      // 多個連字符合併為一個
        .replace(/^-|-$/g, '');   // 移除開頭和結尾的連字符
      
      if (id) {
        heading.id = id;
      }
    }
  });

  // 更新側邊導航連結的 href
  const tocLinks = document.querySelectorAll('.toc-sidebar a[href^="#"]');
  tocLinks.forEach(function(link) {
    const href = link.getAttribute('href');
    if (href && href.startsWith('#')) {
      const targetId = href.substring(1);
      const targetElement = document.getElementById(targetId);
      if (targetElement) {
        link.addEventListener('click', function(e) {
          e.preventDefault();
          targetElement.scrollIntoView({
            behavior: 'smooth',
            block: 'start'
          });
        });
      }
    }
  });

  // 滾動時高亮當前章節並自動滾動側邊欄
  function updateActiveSection() {
    const sections = document.querySelectorAll('.article-content h2, .article-content h3, .article-content h4');
    const tocLinks = document.querySelectorAll('.toc-sidebar a[href^="#"]');
    const tocSidebar = document.querySelector('.toc-sidebar');
    
    let currentSection = '';
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    
    sections.forEach(function(section) {
      const sectionTop = section.offsetTop - 100; // 提前 100px 觸發
      if (scrollTop >= sectionTop) {
        currentSection = section.id;
      }
    });
    
    // 移除所有 active 類別
    tocLinks.forEach(function(link) {
      link.classList.remove('active');
    });
    
    // 為當前章節添加 active 類別並自動滾動側邊欄
    if (currentSection) {
      const activeLink = document.querySelector('.toc-sidebar a[href="#' + currentSection + '"]');
      if (activeLink) {
        activeLink.classList.add('active');
        
        // 自動滾動側邊欄到當前章節位置
        if (tocSidebar && activeLink) {
          // 獲取側邊欄的滾動容器信息
          const sidebarScrollTop = tocSidebar.scrollTop;
          const sidebarHeight = tocSidebar.clientHeight;
          const sidebarScrollHeight = tocSidebar.scrollHeight;
          
          // 獲取當前連結在側邊欄中的位置
          const linkOffsetTop = activeLink.offsetTop;
          const linkHeight = activeLink.offsetHeight;
          
          // 計算連結相對於側邊欄可視區域的位置
          const linkTop = linkOffsetTop - sidebarScrollTop;
          const linkBottom = linkTop + linkHeight;
          
          // 設定緩衝區域（側邊欄高度的 20%）
          const bufferZone = Math.max(20, sidebarHeight * 0.2);
          const safeTop = bufferZone;
          const safeBottom = sidebarHeight - bufferZone;
          
          // 檢查是否需要滾動
          let needsScroll = false;
          let targetScrollTop = sidebarScrollTop;
          
          if (linkTop < safeTop) {
            // 連結太靠近頂部，滾動到連結上方預留緩衝空間
            targetScrollTop = linkOffsetTop - bufferZone;
            needsScroll = true;
          } else if (linkBottom > safeBottom) {
            // 連結太靠近底部，滾動到連結下方預留緩衝空間
            targetScrollTop = linkOffsetTop + linkHeight - sidebarHeight + bufferZone;
            needsScroll = true;
          }
          
          // 如果需要滾動，執行滾動
          if (needsScroll) {
            // 確保滾動位置在有效範圍內
            const maxScrollTop = sidebarScrollHeight - sidebarHeight;
            targetScrollTop = Math.max(0, Math.min(targetScrollTop, maxScrollTop));
            
            // 只有當目標位置與當前位置差距足夠大時才滾動
            if (Math.abs(targetScrollTop - sidebarScrollTop) > 10) {
              tocSidebar.scrollTop = targetScrollTop;
            }
          }
        }
      }
    }
  }

  // 監聽滾動事件
  window.addEventListener('scroll', updateActiveSection);
  
  // 初始化時執行一次
  updateActiveSection();
});

// 回到頂部按鈕功能（所有裝置都支援）
document.addEventListener('DOMContentLoaded', function() {
  const backToTopBtn = document.getElementById('back-to-top');
  
  if (!backToTopBtn) return;
  
  // 顯示/隱藏按鈕
  function toggleBackToTopBtn() {
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    
    if (scrollTop > 300) {
      backToTopBtn.style.display = 'flex';
      backToTopBtn.classList.add('visible');
    } else {
      backToTopBtn.style.display = 'none';
      backToTopBtn.classList.remove('visible');
    }
  }
  
  // 回到頂部功能
  function scrollToTop() {
    window.scrollTo({
      top: 0,
      behavior: 'smooth'
    });
  }
  
  // 綁定事件
  window.addEventListener('scroll', toggleBackToTopBtn);
  backToTopBtn.addEventListener('click', scrollToTop);
  
  // 初始化
  toggleBackToTopBtn();
});
</script>
{{ end }}
```


</details>
<h2 id="3-css-樣式設計">3. CSS 樣式設計</h2>
<h3 id="31-側邊欄樣式">3.1 側邊欄樣式</h3>
<p>在 <code>layouts/partials/custom_head.html</code> 中添加 CSS：</p>


<details>
  <summary>點擊查看側邊欄 CSS 樣式</summary>
  

```css
/* 側邊章節導航樣式 - 獨立側邊欄 */
.toc-sidebar {
  position: fixed;
  top: 50%;
  right: 20px;
  transform: translateY(-50%);
  width: 280px;
  max-height: 80vh;
  overflow-y: auto;
  padding: 1.5rem;
  background: rgba(0, 0, 0, 0.8);
  backdrop-filter: blur(10px);
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  z-index: 1000;
  transition: all 0.3s ease;
}

.toc-sidebar:hover {
  background: rgba(0, 0, 0, 0.9);
  border-color: rgba(255, 255, 255, 0.25);
}

/* 文章內容保持原有佈局 */
.article-content {
  max-width: 800px;
  margin: 0 auto;
  padding: 0 1rem;
}

.toc-sidebar h3 {
  margin: 0 0 1rem 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--primary-color, #fff);
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  padding-bottom: 0.5rem;
}

.toc-sidebar ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.toc-sidebar li {
  margin: 0.25rem 0;
}

.toc-sidebar a {
  display: block;
  padding: 0.25rem 0.5rem;
  color: rgba(255, 255, 255, 0.7);
  text-decoration: none;
  border-radius: 4px;
  transition: all 0.2s ease;
  font-size: 0.9rem;
  line-height: 1.4;
}

.toc-sidebar a:hover {
  background: rgba(255, 255, 255, 0.1);
  color: var(--primary-color, #fff);
}

.toc-sidebar a.active {
  background: rgba(255, 255, 255, 0.15);
  color: var(--primary-color, #fff);
  font-weight: 500;
}

/* 不同層級的縮排 */
.toc-sidebar ul ul {
  margin-left: 1rem;
  border-left: 1px solid rgba(255, 255, 255, 0.1);
  padding-left: 0.5rem;
}

.toc-sidebar ul ul ul {
  margin-left: 1rem;
}
```


</details>
<h3 id="32-回到頂部按鈕樣式">3.2 回到頂部按鈕樣式</h3>


<details>
  <summary>點擊查看回到頂部按鈕 CSS 樣式</summary>
  

```css
/* 回到頂部按鈕樣式 */
.back-to-top-btn {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  width: 50px;
  height: 50px;
  background: rgba(0, 0, 0, 0.8);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 50%;
  color: white;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  transition: all 0.3s ease;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.back-to-top-btn:hover {
  background: rgba(0, 0, 0, 0.9);
  border-color: rgba(255, 255, 255, 0.4);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}

.back-to-top-btn:active {
  transform: translateY(0);
}

.back-to-top-btn.visible {
  display: flex;
  animation: fadeInUp 0.3s ease;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
```


</details>
<h3 id="33-響應式設計">3.3 響應式設計</h3>


<details>
  <summary>點擊查看響應式設計 CSS 樣式</summary>
  

```css
/* 響應式設計 */
@media (max-width: 1024px) {
  .toc-sidebar {
    width: 240px;
    right: 15px;
  }
}

@media (max-width: 768px) {
  /* 手機版隱藏 TOC */
  .toc-sidebar {
    display: none;
  }
  
  /* 確保文章內容在手機版有足夠的邊距 */
  .article-content {
    max-width: 100%;
    padding: 0 1.5rem;
  }
  
  .back-to-top-btn {
    bottom: 1.5rem;
    right: 1.5rem;
    width: 45px;
    height: 45px;
  }
}

/* 平板版調整 */
@media (max-width: 1024px) and (min-width: 769px) {
  .toc-sidebar {
    width: 220px;
    padding: 1rem;
    font-size: 0.9rem;
  }
  
  .toc-sidebar h3 {
    font-size: 0.9rem;
  }
  
  .toc-sidebar a {
    font-size: 0.85rem;
    padding: 0.2rem 0.4rem;
  }
}
```


</details>
<h2 id="4-標題間距優化">4. 標題間距優化</h2>
<h3 id="41-改善文章可讀性">4.1 改善文章可讀性</h3>


<details>
  <summary>點擊查看標題間距 CSS 樣式</summary>
  

```css
/* 調整標題間距 */
.article-content h2 {
  margin-top: 4.5rem;
  margin-bottom: 1.5rem;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

.article-content h3 {
  margin-top: 2.5rem;
  margin-bottom: 1.2rem;
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}

.article-content h4 {
  margin-top: 2rem;
  margin-bottom: 1rem;
  padding-top: 0.3rem;
  padding-bottom: 0.3rem;
}

/* 第一個標題不需要上邊距 */
.article-content h2:first-child,
.article-content h3:first-child,
.article-content h4:first-child {
  margin-top: 0;
}

/* 段落與標題之間的間距 */
.article-content p {
  margin-bottom: 1.2rem;
  line-height: 1.6;
}

/* 列表與標題之間的間距 */
.article-content ul,
.article-content ol {
  margin-top: 1rem;
  margin-bottom: 1.5rem;
}

.article-content li {
  margin-bottom: 0.5rem;
  line-height: 1.5;
}

/* 確保標題有正確的錨點 ID */
.article-content h2,
.article-content h3,
.article-content h4 {
  scroll-margin-top: 2rem;
}
```


</details>
<h2 id="5-需求描述">5. 需求描述</h2>
<h3 id="51-桌面版功能">5.1 桌面版功能</h3>
<ul>
<li><strong>固定側邊欄</strong>：右側固定位置的章節目錄</li>
<li><strong>自動高亮</strong>：滾動時自動高亮當前章節</li>
<li><strong>智能滾動</strong>：側邊欄自動滾動到當前章節位置</li>
<li><strong>平滑跳轉</strong>：點擊章節標題平滑滾動到對應位置</li>
</ul>
<h3 id="52-平板版功能">5.2 平板版功能</h3>
<ul>
<li><strong>縮小側邊欄</strong>：較窄的側邊欄（220px）</li>
<li><strong>保持所有功能</strong>：與桌面版相同的導航功能</li>
</ul>
<h3 id="53-手機版功能">5.3 手機版功能</h3>
<ul>
<li><strong>隱藏 TOC</strong>：手機寬度不足以顯示TOC</li>
<li><strong>回到頂部按鈕</strong>：使用懸浮按鈕讓使用者至少可以快速回到開頭</li>
<li><strong>響應式佈局</strong>：文章內容全寬顯示</li>
</ul>
<h2 id="6-技術實現細節">6. 技術實現細節</h2>
<h3 id="61-自動滾動算法">6.1 自動滾動算法</h3>
<ul>
<li>使用動態緩衝區域（側邊欄高度的 20%）</li>
<li>智能判斷是否需要滾動</li>
<li>避免微小震盪的閾值保護</li>
</ul>
<h3 id="62-效能優化">6.2 效能優化</h3>
<ul>
<li>手機版不執行 TOC 相關功能</li>
<li>滾動事件節流處理</li>
<li>條件式 DOM 操作</li>
</ul>
<h3 id="63-無障礙設計">6.3 無障礙設計</h3>
<ul>
<li>正確的 ARIA 標籤</li>
<li>鍵盤導航支援</li>
<li>語義化 HTML 結構</li>
</ul>
]]></content:encoded></item><item><title>在文章中加入圖片的語法</title><link>https://tarrragon.github.io/blog/posts/%E5%9C%A8%E6%96%87%E7%AB%A0%E4%B8%AD%E5%8A%A0%E5%85%A5%E5%9C%96%E7%89%87%E7%9A%84%E8%AA%9E%E6%B3%95/</link><pubDate>Wed, 17 Sep 2025 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E5%9C%A8%E6%96%87%E7%AB%A0%E4%B8%AD%E5%8A%A0%E5%85%A5%E5%9C%96%E7%89%87%E7%9A%84%E8%AA%9E%E6%B3%95/</guid><description>&lt;h2 id="在文章中引用assets的圖片">在文章中引用assets的圖片&lt;/h2>
&lt;p>我現在的做法是assets第一層資料夾是大分類，第二層資料夾一個文章一個資料夾，然後每個文章使用的圖片集中那個資料夾&lt;/p></description><content:encoded><![CDATA[<h2 id="在文章中引用assets的圖片">在文章中引用assets的圖片</h2>
<p>我現在的做法是assets第一層資料夾是大分類，第二層資料夾一個文章一個資料夾，然後每個文章使用的圖片集中那個資料夾</p>
<h2 id="語法">語法</h2>
<p>1.使用 Hugo 的圖片處理功能</p>





<pre tabindex="0"><code class="language-makdown" data-lang="makdown"><figure><img src="/blog/work-log/flutter_toggle_button/ToggleButtons.png"
    alt="ToggleButtons 樣式">
</figure>
</code></pre><p>2.使用標準 Markdown 語法</p>





<pre tabindex="0"><code class="language-makdown" data-lang="makdown">![ToggleButtons 樣式](/work-log/flutter_toggle_button/ToggleButtons.png)</code></pre><p>3.使用 Hugo 的圖片 shortcode</p>





<pre tabindex="0"><code class="language-makdown" data-lang="makdown"><figure><img src="/blog/work-log/flutter_toggle_button/ToggleButtons.png"
    alt="ToggleButtons 樣式" width="600"><figcaption>
      <p>Flutter ToggleButtons 元件樣式展示</p>
    </figcaption>
</figure>
</code></pre><h2 id="重要注意事項">重要注意事項</h2>
<p>1.圖片路徑：在 Hugo 中，assets 資料夾的內容會被處理並放在網站根目錄下，所以路徑是 /work-log/flutter_toggle_button/ToggleButtons.png</p>
<p>2.圖片優化：Hugo 會自動處理圖片優化，但你可以透過 shortcode 參數來控制大小和品質</p>
<p>3.響應式設計：使用 <figure><img src="">
</figure>
 shortcode 可以確保圖片在不同裝置上都能正確顯示</p>]]></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><item><title>Hugo + Bear Cub 主題設定完整教學</title><link>https://tarrragon.github.io/blog/posts/hugo--bear-cub-%E4%B8%BB%E9%A1%8C%E8%A8%AD%E5%AE%9A%E5%AE%8C%E6%95%B4%E6%95%99%E5%AD%B8/</link><pubDate>Fri, 22 Aug 2025 20:41:50 +0800</pubDate><guid>https://tarrragon.github.io/blog/posts/hugo--bear-cub-%E4%B8%BB%E9%A1%8C%E8%A8%AD%E5%AE%9A%E5%AE%8C%E6%95%B4%E6%95%99%E5%AD%B8/</guid><description>&lt;h2 id="hugo--主題設定完整教學">Hugo + 主題設定完整教學&lt;/h2>
&lt;p>這篇文章記錄了我從零開始建立 Hugo 部落格並安裝 Bear Cub 主題的完整過程，包含常見的設定問題和解決方案。&lt;/p></description><content:encoded><![CDATA[<h2 id="hugo--主題設定完整教學">Hugo + 主題設定完整教學</h2>
<p>這篇文章記錄了我從零開始建立 Hugo 部落格並安裝 Bear Cub 主題的完整過程，包含常見的設定問題和解決方案。</p>
<h2 id="為什麼選擇-hugo-">為什麼選擇 Hugo ？</h2>
<h3 id="特點go-語言寫的速度極快">特點：Go 語言寫的，速度極快</h3>
<h3 id="優點">優點</h3>
<p>生成速度最快（</p>
<p>單一 binary → 不需要安裝 Node/Ruby 環境，跨平台好用</p>
<p>主題多</p>
<p>部署方便（build → 靜態檔 → push）</p>
<h3 id="缺點">缺點</h3>
<p>需要本地 build，再 push 結果到 GitHub Pages（不像 Jekyll 原生）</p>
<p>模板語法（Go Template）對新手稍有難度</p>
<p>Plugin 擴展性不如 Node.js 生態</p>
<h2 id="基本安裝與設定">基本安裝與設定</h2>
<h3 id="1-初始化-hugo-專案">1. 初始化 Hugo 專案</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="c1"># 在現有的 GitHub 專案中初始化 Hugo</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hugo new site . --force</span></span></code></pre></div><h3 id="2-安裝-bear-cub-主題">2. 安裝 Bear Cub 主題</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="c1"># 使用 git submodule 安裝（推薦）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git submodule add https://github.com/clente/hugo-bearcub.git themes/hugo-bearcub</span></span></code></pre></div><h3 id="3-基本設定檔案-hugotoml">3. 基本設定檔案 (hugo.toml)</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><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="nx">baseURL</span> <span class="p">=</span> <span class="s1">&#39;https://你的用戶名.github.io/blog&#39;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">theme</span> <span class="p">=</span> <span class="s1">&#39;hugo-bearcub&#39;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nx">copyright</span> <span class="p">=</span> <span class="s1">&#39;你的名字 (CC BY 4.0)&#39;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nx">defaultContentLanguage</span> <span class="p">=</span> <span class="s1">&#39;zh-tw&#39;</span>
</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="c"># 產生 robots.txt 以利於 SEO</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">enableRobotsTXT</span> <span class="p">=</span> <span class="kc">true</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="c"># 設定語法高亮</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">[</span><span class="nx">markup</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">highlight</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">lineNos</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">lineNumbersInTable</span> <span class="p">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">noClasses</span> <span class="p">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">goldmark</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">goldmark</span><span class="p">.</span><span class="nx">renderer</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nx">unsafe</span> <span class="p">=</span> <span class="kc">true</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="c"># 多語言模式設定</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">[</span><span class="nx">languages</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">[</span><span class="nx">languages</span><span class="p">.</span><span class="nx">zh-tw</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="nx">title</span> <span class="p">=</span> <span class="s1">&#39;我的部落格&#39;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="nx">languageName</span> <span class="p">=</span> <span class="s1">&#39;zh-TW 🇹🇼&#39;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="nx">LanguageCode</span> <span class="p">=</span> <span class="s1">&#39;zh-TW&#39;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="nx">contentDir</span> <span class="p">=</span> <span class="s1">&#39;content&#39;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="p">[</span><span class="nx">languages</span><span class="p">.</span><span class="nx">zh-tw</span><span class="p">.</span><span class="nx">params</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">      <span class="nx">madeWith</span> <span class="p">=</span> <span class="s1">&#39;使用 [Bear Cub](https://github.com/clente/hugo-bearcub) 製作&#39;</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="p">[</span><span class="nx">params</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="c"># 網站描述</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="nx">description</span> <span class="p">=</span> <span class="s1">&#39;我的個人部落格，分享技術與生活&#39;</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="c"># 網站標題</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">  <span class="nx">title</span> <span class="p">=</span> <span class="s1">&#39;我的部落格&#39;</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">  
</span></span><span class="line"><span class="ln">37</span><span class="cl">  <span class="c"># 日期格式</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">  <span class="nx">dateFormat</span> <span class="p">=</span> <span class="s1">&#39;2006-01-02&#39;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">  
</span></span><span class="line"><span class="ln">40</span><span class="cl">  <span class="c"># 主題樣式 (original 或 herman)</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">  <span class="nx">themeStyle</span> <span class="p">=</span> <span class="s1">&#39;original&#39;</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">  
</span></span><span class="line"><span class="ln">43</span><span class="cl">  <span class="c"># 自動產生社群媒體卡片</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">  <span class="nx">generateSocialCard</span> <span class="p">=</span> <span class="kc">true</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="c"># 作者資訊</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">  <span class="p">[</span><span class="nx">params</span><span class="p">.</span><span class="nx">author</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="nx">name</span> <span class="p">=</span> <span class="s1">&#39;你的名字&#39;</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">    <span class="nx">email</span> <span class="p">=</span> <span class="s1">&#39;your.email@example.com&#39;</span></span></span></code></pre></div><h2 id="首頁內容自訂">首頁內容自訂</h2>
<h3 id="建立首頁內容檔案">建立首頁內容檔案</h3>
<p>在 <code>content/_index.md</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">title: &#34;首頁&#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 class="gh"># 歡迎來到我的個人部落格
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">嗨！我是 <span class="gs">**你的名字**</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="gu">## 關於這個部落格
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="gu"></span>
</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> **技術文章**：程式開發經驗分享
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">-</span> **學習筆記**：新技術的學習過程
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">-</span> **專案記錄**：有趣的專案開發歷程
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="k">-</span> **生活感悟**：日常生活的思考與體驗
</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 class="gu">## 最新文章
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">歡迎查看我的[<span class="nt">最新文章</span>](<span class="na">/posts/</span>)，或者透過[<span class="nt">標籤</span>](<span class="na">/tags/</span>)來瀏覽特定主題的內容。
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</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="gu"></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></span><span class="line"><span class="ln">26</span><span class="cl"><span class="k">-</span> Email: your.email<span class="ni">@example</span>.com
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="k">-</span> 有任何問題或建議，也歡迎在文章下方留言
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</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></span><span class="line"><span class="ln">31</span><span class="cl">*感謝你的造訪，希望這些內容對你有所幫助！*</span></span></code></pre></div><h2 id="建立文章">建立文章</h2>
<h3 id="快速建立新文章">快速建立新文章</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="c1"># 建立新文章</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hugo new content posts/文章標題.md</span></span></code></pre></div><h3 id="文章格式範例">文章格式範例</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;文章標題&#34;
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">date: 2025-08-22T20:41:50+08:00
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">draft: false
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">description: &#34;文章描述，用於 SEO 和社群分享&#34;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">tags: [&#34;標籤1&#34;, &#34;標籤2&#34;]
</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></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 class="gu">## 小節標題
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">-</span> 列表項目
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">-</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="gu">### 程式碼範例
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="gu"></span>\`\`\`bash
</span></span><span class="line"><span class="ln">18</span><span class="cl">echo &#34;Hello Hugo!&#34;
</span></span><span class="line"><span class="ln">19</span><span class="cl">\`\`\`</span></span></code></pre></div><h2 id="文章摘要與繼續閱讀設定">文章摘要與繼續閱讀設定</h2>
<h3 id="摘要顯示功能">摘要顯示功能</h3>
<p>為了避免文章列表頁面顯示完整內容，我們可以設定文章摘要功能，讓列表只顯示文章摘要並提供「繼續閱讀」按鈕。</p>
<h3 id="1-自動摘要設定">1. 自動摘要設定</h3>
<p>在 <code>hugo.toml</code> 中設定摘要長度：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><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="nx">summaryLength</span> <span class="p">=</span> <span class="mi">200</span>  <span class="c"># 摘要字數限制</span></span></span></code></pre></div><h4 id="說明">說明</h4>
<ul>
<li>當文章沒有手動設定摘要時，Hugo 會自動截取前 200 個字元</li>
<li>對於中文內容，200 個字元大約是 100-150 個中文字</li>
<li>對於英文內容，200 個字元大約是 30-40 個英文單詞</li>
</ul>
<h3 id="2-手動摘要設定">2. 手動摘要設定</h3>
<h4 id="方法一使用---more---標記">方法一：使用 <code>&lt;!--more--&gt;</code> 標記</h4>
<p>在文章中插入 <code>&lt;!--more--&gt;</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">title: &#34;文章標題&#34;
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">date: 2025-01-XX
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">tags: [&#34;標籤1&#34;, &#34;標籤2&#34;]
</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">
</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 class="c">&lt;!--more--&gt;</span>
</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></span></code></pre></div><h4 id="方法二使用-description-參數">方法二：使用 <code>description</code> 參數</h4>
<p>在文章的 front matter 中設定 <code>description</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">title: &#34;文章標題&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">date: 2025-01-XX
</span></span><span class="line"><span class="ln">4</span><span class="cl">description: &#34;這是文章的摘要，會顯示在列表中&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">tags: [&#34;標籤1&#34;, &#34;標籤2&#34;]
</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></span></code></pre></div><h3 id="3-摘要優先順序">3. 摘要優先順序</h3>
<p>Hugo 的摘要顯示優先順序：</p>
<ol>
<li><strong><code>&lt;!--more--&gt;</code> 標記</strong> - 最高優先級</li>
<li><strong><code>description</code> 參數</strong> - 第二優先級</li>
<li><strong>自動截取</strong> - 使用 <code>summaryLength</code> 設定</li>
</ol>
<h3 id="4-列表頁面樣式">4. 列表頁面樣式</h3>
<p>設定完成後，文章列表會以卡片式設計顯示：</p>
<ul>
<li><strong>文章標題</strong>：可點擊進入完整文章</li>
<li><strong>發布日期</strong>：顯示在標題旁邊</li>
<li><strong>文章摘要</strong>：只顯示摘要內容</li>
<li><strong>繼續閱讀按鈕</strong>：當文章被截斷時顯示</li>
<li><strong>標籤</strong>：顯示文章相關標籤</li>
</ul>
<h2 id="github-actions-自動部署">GitHub Actions 自動部署</h2>
<h3 id="設定工作流程">設定工作流程</h3>
<p>在 <code>.github/workflows/hugo.yml</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="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy Hugo site to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</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">push</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">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;main&#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="nt">workflow_dispatch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="nt">permissions</span><span class="p">:</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">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</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">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write</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">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</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">14</span><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</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">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">16</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">17</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">18</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">submodules</span><span class="p">:</span><span class="w"> </span><span class="l">recursive</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Hugo</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">peaceiris/actions-hugo@v2</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">          </span><span class="nt">hugo-version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;latest&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">          
</span></span></span><span class="line"><span class="ln">26</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</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">        
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">peaceiris/actions-gh-pages@v3</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">          </span><span class="nt">github_token</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.GITHUB_TOKEN }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">          </span><span class="nt">publish_dir</span><span class="p">:</span><span class="w"> </span><span class="l">./public</span></span></span></code></pre></div><h2 id="常見問題與解決方案">常見問題與解決方案</h2>
<h3 id="社群媒體預覽卡片顯示亂碼問題">社群媒體預覽卡片顯示亂碼問題</h3>
<p>當你在 Discord、Facebook 等社群媒體分享文章時，如果預覽卡片中的中文顯示為問號（？），這是因為預設字型不支援中文字符。</p>
<h4 id="問題表現">問題表現</h4>
<ul>
<li>分享連結時，預覽卡片的標題顯示為問號</li>
<li>作者名稱顯示為 <code>map[name:作者名稱]</code> 而不是純文字</li>
</ul>
<h4 id="解決方案">解決方案</h4>
<h5 id="1-建立客製化的社群媒體卡片檔案">1. 建立客製化的社群媒體卡片檔案</h5>
<p>首先，在你的專案根目錄建立目錄並複製檔案：</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"># 建立 layouts/partials 目錄</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkdir -p layouts/partials
</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"># 複製主題的 social_card.html 到你的專案中</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">cp themes/hugo-bearcub/layouts/partials/social_card.html layouts/partials/</span></span></code></pre></div><h5 id="2-修改字型設定">2. 修改字型設定</h5>
<p>編輯 <code>layouts/partials/social_card.html</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="c">&lt;!-- 將第 2 行的字型 URL 替換為支援中文的字型 --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ $font := resources.GetRemote &#34;https://github.com/adobe-fonts/source-han-sans/raw/release/OTF/TraditionalChinese/SourceHanSansTC-Bold.otf&#34; }}</span></span></code></pre></div><h5 id="3-修正作者名稱顯示">3. 修正作者名稱顯示</h5>
<p>在同一個檔案中，找到第 27 行並修改：</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="c">&lt;!-- 原來的程式碼 --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ $author := (default $.Site.Params.author.name ($.Param &#34;author&#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 class="c">&lt;!-- 修改為 --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">{{ $author := (or ($.Param &#34;author&#34;) $.Site.Params.author.name) }}</span></span></code></pre></div><blockquote>
<p><strong>重要說明</strong>：我們將檔案複製到專案的 <code>layouts/partials/</code> 目錄而不是直接修改主題檔案，這樣做的好處是：</p>
<ul>
<li>保持主題的 git submodule 乾淨</li>
<li>未來更新主題時不會遺失客製化設定</li>
<li>Hugo 會優先使用專案中的檔案覆蓋主題檔案</li>
</ul></blockquote>
<h5 id="4-確保社群媒體卡片功能開啟">4. 確保社群媒體卡片功能開啟</h5>
<p>在 <code>hugo.toml</code> 中確認設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">params</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c"># 自動產生社群媒體卡片</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">generateSocialCard</span> <span class="p">=</span> <span class="kc">true</span></span></span></code></pre></div><h4 id="字型選擇說明">字型選擇說明</h4>
<ul>
<li><strong>Source Han Sans TC</strong>：Adobe 思源黑體繁體中文版本，支援完整中文字符集</li>
<li><strong>替代方案</strong>：也可以使用 Google 的 Noto Sans CJK 字型</li>
<li><strong>檔案大小</strong>：中文字型檔案較大，但現代瀏覽器會快取字型檔案</li>
</ul>
<p>修改完成後，重新建立並部署網站，社群媒體預覽就會正確顯示中文內容了。</p>
<h2 id="實用技巧與其他設定">實用技巧與其他設定</h2>
<h3 id="本地開發">本地開發</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="c1"># 啟動開發伺服器</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hugo server
</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"># 包含草稿</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">hugo server --buildDrafts
</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="c1"># 指定 IP 和 Port</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">hugo server --bind 0.0.0.0 --port <span class="m">1313</span></span></span></code></pre></div><h3 id="不同電腦上的工作流程">不同電腦上的工作流程</h3>
<h4 id="主要開發電腦">主要開發電腦</h4>
<ul>
<li>安裝 Hugo 進行完整開發</li>
<li>可以本地預覽和測試</li>
</ul>
<h4 id="其他電腦緊急更新">其他電腦/緊急更新</h4>
<ul>
<li>只需要 Git 和文字編輯器</li>
<li>直接編輯 Markdown 檔案推送即可</li>
<li>GitHub Actions 會自動編譯和部署</li>
</ul>
<h3 id="常見檔案位置">常見檔案位置</h3>
<ul>
<li><strong>設定檔</strong>：<code>hugo.toml</code></li>
<li><strong>首頁內容</strong>：<code>content/_index.md</code></li>
<li><strong>文章</strong>：<code>content/posts/</code></li>
<li><strong>主題</strong>：<code>themes/hugo-bearcub/</code></li>
<li><strong>靜態檔案</strong>：<code>static/</code></li>
</ul>
<h2 id="總結">總結</h2>
<p>透過這個設定流程，我們成功建立了：</p>
<ul>
<li>快速載入的靜態部落格</li>
<li>支援繁體中文的介面</li>
<li>自動化的 GitHub Pages 部署</li>
</ul>
<p>現在你可以專注於寫作，讓 Hugo 和 GitHub Actions 處理其他的技術細節！</p>
<hr>
<p>這篇教學記錄了我的實際設定過程，希望對其他想要建立 Hugo 部落格的朋友有所幫助。</p>]]></content:encoded></item></channel></rss>