<?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>Hugo on Tarragon</title><link>https://tarrragon.github.io/blog/tags/hugo/</link><description>Recent content in Hugo on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 15 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/hugo/index.xml" rel="self" type="application/rss+xml"/><item><title>CSS / JS 拆出獨立檔案</title><link>https://tarrragon.github.io/blog/report/extract-css-js-files/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/extract-css-js-files/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Inline CSS / JS 超過 ~30 行就值得拆出獨立檔案、走 Hugo &lt;code>resources.Get | minify | fingerprint&lt;/code> 引入。&lt;/strong> Template 變單純、editor 對 .css/.js 有 syntax highlight、minify 自動化、cache-busting fingerprint 自動處理。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-inline-有上限">為什麼 inline 有上限&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Inline CSS / JS 在 Hugo template 內看似省事（一個檔案搞定），但隨著規模上升出現多個成本：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>規模&lt;/th>
 &lt;th>Inline 的代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&amp;lt; 10 行&lt;/td>
 &lt;td>幾乎無 — 一目了然&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10-30 行&lt;/td>
 &lt;td>中 — Editor 不太能 highlight、template 開始混雜&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>30+ 行&lt;/td>
 &lt;td>高 — 找東西要在 template 模式間切換、minify 沒做、cache 控制困難&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>拆檔的成本是「多 1-2 個檔案」、收益是「multiple」 — 過了 30 行門檻、ROI 已正向。&lt;/p>
&lt;h3 id="拆檔的實際得益">拆檔的實際得益&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Inline&lt;/th>
 &lt;th>拆檔 + Resources Pipeline&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Editor syntax highlight&lt;/td>
 &lt;td>部分 — 看 editor 是否支援 mixed mode&lt;/td>
 &lt;td>完整 — 純 .css / .js 檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Minify&lt;/td>
 &lt;td>手動或 hugo template minify&lt;/td>
 &lt;td>Hugo &lt;code>minify&lt;/code> pipe 自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache-busting&lt;/td>
 &lt;td>手動加版本號&lt;/td>
 &lt;td>&lt;code>fingerprint&lt;/code> 自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式碼重用&lt;/td>
 &lt;td>難 — 跟 template 綁&lt;/td>
 &lt;td>容易 — 多 template 共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Version control diff&lt;/td>
 &lt;td>跟 template 改動混&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;hr>
&lt;h2 id="這次任務的拆檔目標">這次任務的拆檔目標&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>layouts/_default/search.html&lt;/code> 現況：&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>Hugo template 與 HTML&lt;/td>
 &lt;td>~30&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Inline &lt;code>&amp;lt;script&amp;gt;&lt;/code>&lt;/td>
 &lt;td>~110&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Inline &lt;code>&amp;lt;style&amp;gt;&lt;/code>&lt;/td>
 &lt;td>~80&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>總計&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>~220&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>220 行的 single-file template、CSS / JS 各超過拆檔門檻 3-4 倍。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 CSS 拆到 &lt;code>assets/search.css&lt;/code>、JS 拆到 &lt;code>assets/search.js&lt;/code>、template 只剩 HTML 結構與 Hugo 引入。&lt;/p>
&lt;h3 id="執行拆檔步驟">執行：拆檔步驟&lt;/h3>
&lt;h4 id="step-1建立-assets-檔">Step 1：建立 assets 檔&lt;/h4>





&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">assets/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── search.css # 原本 inline &amp;lt;style&amp;gt; 內容
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">└── search.js # 原本 inline &amp;lt;script&amp;gt; 內容&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-2template-引入">Step 2：template 引入&lt;/h4>





&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">{{- $css := resources.Get &amp;#34;search.css&amp;#34; | minify | fingerprint -}}
&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;{{ $css.RelPermalink }}&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="na">integrity&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $css.Data.Integrity }}&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>&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">div&lt;/span> &lt;span class="na">data-pagefind-ignore&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-shell&amp;#34;&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>&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">div&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">{{- $js := resources.Get &amp;#34;search.js&amp;#34; | minify | fingerprint -}}
&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">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $js.RelPermalink }}&amp;#34;&lt;/span> &lt;span class="na">integrity&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $js.Data.Integrity }}&amp;#34;&lt;/span> &lt;span class="na">defer&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">11&lt;/span>&lt;span class="cl">{{ end }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-3js-從全域-windowpagefindui-改為-module-模式">Step 3：JS 從全域 &lt;code>window.PagefindUI&lt;/code> 改為 module 模式&lt;/h4>
&lt;p>如果原本 inline JS 用 &lt;code>new PagefindUI(...)&lt;/code> 直接執行、拆檔後仍然可以這樣寫。但若想進一步，把 init 包成 function：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Inline CSS / JS 超過 ~30 行就值得拆出獨立檔案、走 Hugo <code>resources.Get | minify | fingerprint</code> 引入。</strong> Template 變單純、editor 對 .css/.js 有 syntax highlight、minify 自動化、cache-busting fingerprint 自動處理。</p>
<hr>
<h2 id="為什麼-inline-有上限">為什麼 inline 有上限</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Inline CSS / JS 在 Hugo template 內看似省事（一個檔案搞定），但隨著規模上升出現多個成本：</p>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>Inline 的代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&lt; 10 行</td>
          <td>幾乎無 — 一目了然</td>
      </tr>
      <tr>
          <td>10-30 行</td>
          <td>中 — Editor 不太能 highlight、template 開始混雜</td>
      </tr>
      <tr>
          <td>30+ 行</td>
          <td>高 — 找東西要在 template 模式間切換、minify 沒做、cache 控制困難</td>
      </tr>
  </tbody>
</table>
<p>拆檔的成本是「多 1-2 個檔案」、收益是「multiple」 — 過了 30 行門檻、ROI 已正向。</p>
<h3 id="拆檔的實際得益">拆檔的實際得益</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Inline</th>
          <th>拆檔 + Resources Pipeline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Editor syntax highlight</td>
          <td>部分 — 看 editor 是否支援 mixed mode</td>
          <td>完整 — 純 .css / .js 檔</td>
      </tr>
      <tr>
          <td>Minify</td>
          <td>手動或 hugo template minify</td>
          <td>Hugo <code>minify</code> pipe 自動</td>
      </tr>
      <tr>
          <td>Cache-busting</td>
          <td>手動加版本號</td>
          <td><code>fingerprint</code> 自動</td>
      </tr>
      <tr>
          <td>程式碼重用</td>
          <td>難 — 跟 template 綁</td>
          <td>容易 — 多 template 共用</td>
      </tr>
      <tr>
          <td>Version control diff</td>
          <td>跟 template 改動混</td>
          <td>純檔案改動、清楚</td>
      </tr>
      <tr>
          <td>測試</td>
          <td>難</td>
          <td>可單獨測</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="這次任務的拆檔目標">這次任務的拆檔目標</h2>
<h3 id="觀察">觀察</h3>
<p><code>layouts/_default/search.html</code> 現況：</p>
<table>
  <thead>
      <tr>
          <th>段落</th>
          <th>行數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hugo template 與 HTML</td>
          <td>~30</td>
      </tr>
      <tr>
          <td>Inline <code>&lt;script&gt;</code></td>
          <td>~110</td>
      </tr>
      <tr>
          <td>Inline <code>&lt;style&gt;</code></td>
          <td>~80</td>
      </tr>
      <tr>
          <td><strong>總計</strong></td>
          <td><strong>~220</strong></td>
      </tr>
  </tbody>
</table>
<p>220 行的 single-file template、CSS / JS 各超過拆檔門檻 3-4 倍。</p>
<h3 id="判讀">判讀</h3>
<p>把 CSS 拆到 <code>assets/search.css</code>、JS 拆到 <code>assets/search.js</code>、template 只剩 HTML 結構與 Hugo 引入。</p>
<h3 id="執行拆檔步驟">執行：拆檔步驟</h3>
<h4 id="step-1建立-assets-檔">Step 1：建立 assets 檔</h4>





<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">assets/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── search.css      # 原本 inline &lt;style&gt; 內容
</span></span><span class="line"><span class="ln">3</span><span class="cl">└── search.js       # 原本 inline &lt;script&gt; 內容</span></span></code></pre></div><h4 id="step-2template-引入">Step 2：template 引入</h4>





<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">{{- $css := resources.Get &#34;search.css&#34; | minify | fingerprint -}}
</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;{{ $css.RelPermalink }}&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;{{ $css.Data.Integrity }}&#34;</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">div</span> <span class="na">data-pagefind-ignore</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-shell&#34;</span><span class="p">&gt;</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="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</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">{{- $js := resources.Get &#34;search.js&#34; | minify | fingerprint -}}
</span></span><span class="line"><span class="ln">10</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;{{ $js.RelPermalink }}&#34;</span> <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;{{ $js.Data.Integrity }}&#34;</span> <span class="na">defer</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">11</span><span class="cl">{{ end }}</span></span></code></pre></div><h4 id="step-3js-從全域-windowpagefindui-改為-module-模式">Step 3：JS 從全域 <code>window.PagefindUI</code> 改為 module 模式</h4>
<p>如果原本 inline JS 用 <code>new PagefindUI(...)</code> 直接執行、拆檔後仍然可以這樣寫。但若想進一步，把 init 包成 function：</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="c1">// assets/search.js
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></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"> 3</span><span class="cl">  <span class="kd">function</span> <span class="nx">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">new</span> <span class="nx">PagefindUI</span><span class="p">({</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1">// ... rest of setup
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="s1">&#39;loading&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nb">document</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="nx">init</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</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">10</span><span class="cl">    <span class="nx">init</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">})();</span></span></span></code></pre></div><h4 id="step-4清理-template">Step 4：清理 template</h4>
<p>Template 從 220 行降到 ~30 行 — 只剩 HTML 結構。</p>
<hr>
<h2 id="內在屬性比較四種引入方式">內在屬性比較：四種引入方式</h2>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>維護成本</th>
          <th>Cache 控制</th>
          <th>可重用性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Inline <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code></td>
          <td>中 — template 混雜</td>
          <td>自動跟著 template</td>
          <td>低 — 跟特定 template 綁</td>
      </tr>
      <tr>
          <td>拆 .css / .js + 直接 link / script tag</td>
          <td>低 — 純檔案</td>
          <td>手動加版本號</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Hugo resources.Get + minify</td>
          <td>低</td>
          <td>內容變動觸發新 path</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Hugo resources.Get + minify + fingerprint</td>
          <td>低</td>
          <td>內容 hash 自動 cache-bust</td>
          <td>高 + 安全</td>
      </tr>
  </tbody>
</table>
<p>優先選 fingerprint — Hugo 自動處理快取、瀏覽器看到內容變動的 fingerprint 一定 reload。</p>
<hr>
<h2 id="hugo-resources-pipeline-的細節">Hugo Resources Pipeline 的細節</h2>
<h3 id="resourcesget"><code>resources.Get</code></h3>





<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="p">{{</span> <span class="err">$</span><span class="nx">css</span> <span class="o">:=</span> <span class="nx">resources</span><span class="p">.</span><span class="nx">Get</span> <span class="s">&#34;search.css&#34;</span> <span class="p">}}</span></span></span></code></pre></div><p>讀 <code>assets/search.css</code>。如果路徑下沒有、回傳 nil（要做 nil 檢查）。</p>
<h3 id="-minify"><code>| minify</code></h3>
<p>去除空白、註解、合併 selector — 減少傳輸大小。</p>
<h3 id="-fingerprint"><code>| fingerprint</code></h3>
<p>對檔案內容做 hash、加到 URL（<code>search.abc123.css</code>）。內容變動時 fingerprint 變、瀏覽器把它當新檔案。</p>
<h3 id="relpermalink--permalink"><code>.RelPermalink</code> / <code>.Permalink</code></h3>
<p><code>RelPermalink</code> — site root 相對路徑（<code>/search.abc123.css</code>）<br>
<code>Permalink</code> — 完整 URL（<code>https://site.com/search.abc123.css</code>）</p>
<p>通常用 <code>RelPermalink</code> 即可。</p>
<h3 id="dataintegrity"><code>.Data.Integrity</code></h3>
<p>Subresource Integrity hash — 給 <code>integrity</code> attribute 用、瀏覽器驗證下載內容沒被篡改。</p>
<hr>
<h2 id="拆檔的判斷門檻">拆檔的判斷門檻</h2>
<table>
  <thead>
      <tr>
          <th>Template 內含</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0-10 行 inline CSS / JS</td>
          <td>不拆 — 維護成本最低</td>
      </tr>
      <tr>
          <td>10-30 行</td>
          <td>視情況 — 有重用性需求就拆</td>
      </tr>
      <tr>
          <td>30+ 行</td>
          <td>拆 — 各方面收益都正向</td>
      </tr>
      <tr>
          <td>50+ 行</td>
          <td>強烈建議拆</td>
      </tr>
      <tr>
          <td>多個 template 共用同一段</td>
          <td>立刻拆 — 重用性主導</td>
      </tr>
  </tbody>
</table>
<p>當前 search.html 的 ~190 行 inline 程式碼遠超門檻、屬於「強烈建議拆」。</p>
<hr>
<h2 id="設計取捨css--js-引入策略">設計取捨：CSS / JS 引入策略</h2>
<p>四種做法、各自機會成本不同。這個專案在 inline &gt; 30 行時選 A（拆檔 + Hugo pipeline）當預設、其他做法在特定情境合理。</p>
<h3 id="a拆檔--hugo-resourcesget--minify--fingerprint這個專案的預設">A：拆檔 + Hugo <code>resources.Get | minify | fingerprint</code>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：CSS / JS 拆到 <code>assets/</code>、template 用 <code>resources.Get | minify | fingerprint</code> 引入</li>
<li><strong>選 A 的理由</strong>：minify 自動、cache-bust 自動、editor syntax highlight、跨 template 重用</li>
<li><strong>適合</strong>：規模超過 30 行、預期長期維護的客製</li>
<li><strong>代價</strong>：多 1-2 個檔案、template 跟 assets 分屬兩處（grep 多一步）</li>
</ul>
<h3 id="b拆檔--直接-link--script-tag">B：拆檔 + 直接 <code>&lt;link&gt;</code> / <code>&lt;script&gt;</code> tag</h3>
<ul>
<li><strong>機制</strong>：拆檔到 <code>static/</code> 或 <code>assets/</code>、template 直接 link</li>
<li><strong>跟 A 的取捨</strong>：B 簡單、A 自動處理 minify / fingerprint；B 改檔案後 cache 可能用舊版（要手動加版本號）</li>
<li><strong>B 比 A 好的情境</strong>：簡單 prototype、確定不需要 cache-bust（純內部工具）</li>
</ul>
<h3 id="c保持-inline">C：保持 inline</h3>
<ul>
<li><strong>機制</strong>：CSS / JS 寫在 template 的 <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code> 內</li>
<li><strong>跟 A 的取捨</strong>：C 一個檔案搞定、A 拆兩個；但 C 在 30+ 行時 syntax highlight 失效、難維護</li>
<li><strong>C 比 A 好的情境</strong>：&lt; 10 行的小段、跟 template 邏輯緊密相關</li>
</ul>
<h3 id="dcdn-引入第三方資源">D：CDN 引入第三方資源</h3>
<ul>
<li><strong>機制</strong>：<code>&lt;script src=&quot;https://cdn.../lib.js&quot;&gt;</code></li>
<li><strong>成本特別高的原因</strong>：依賴第三方可用性、跨域 CORS / SRI 處理、隱私問題（追蹤）</li>
<li><strong>D 才合理的情境</strong>：第三方明確支援 SRI 且 CDN 是官方建議方式（少數 vendor library）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>拆檔動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Template 內 <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code> 超過 30 行</td>
          <td>拆到 <code>assets/</code> 下對應 .css / .js</td>
      </tr>
      <tr>
          <td>Editor 對 inline CSS / JS 沒 highlight</td>
          <td>拆檔讓 editor 套對應 mode</td>
      </tr>
      <tr>
          <td>改 inline JS 後 cache 沒更新</td>
          <td>拆檔 + fingerprint 自動 cache-bust</td>
      </tr>
      <tr>
          <td>同樣的 CSS / JS 在多個 template 重複</td>
          <td>拆出共用檔案</td>
      </tr>
      <tr>
          <td>Inline 程式碼跟 Hugo template 邏輯混在一起難 grep</td>
          <td>拆檔讓 grep 範圍清楚</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Template 是 markup 的家、CSS / JS 是各自獨立檔案的家。三者混在一個檔案是過渡狀態、不是長期方案。</p>
]]></content:encoded></item><item><title>baseof.html override 範圍最小化</title><link>https://tarrragon.github.io/blog/report/minimize-baseof-override/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/minimize-baseof-override/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Override theme 檔案的範圍越小、theme 升級時越容易 sync。&lt;/strong> 整檔 copy + 改 1-2 行的 override 在 theme 改了 baseof 時、本地必須手動 merge；只 override 必要的部分（用 partial 或最小檔案）讓變更面積小、merge 容易。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-override-要小">為什麼 override 要小&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Hugo theme 的 lookup order：本地 &lt;code>layouts/&lt;/code> 優先於 &lt;code>themes/&amp;lt;name&amp;gt;/layouts/&lt;/code>。本地有同名檔案、本地的整個內容生效、theme 的版本完全被忽略。&lt;/p>
&lt;p>當本地 override 整個 baseof.html、只改 1-2 行：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Theme 升級時的代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>本地 override 不會自動更新 — 永遠是當初 copy 的版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Theme 的新功能（新 partial、改進的 SEO meta）不會生效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>要手動 diff &lt;code>themes/&amp;lt;name&amp;gt;/layouts/baseof.html&lt;/code> 與本地、合併變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容易忘記、theme 的修正在本地永遠沒套到&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Override 範圍小 = merge 面積小 = 升級時手動 sync 的工作量小。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的-override">這次任務的 override&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>layouts/_default/baseof.html&lt;/code> 是整個 theme 的 baseof 複製、只改了：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gd">- &amp;lt;body&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="gd">&lt;/span>&lt;span class="gi">+ &amp;lt;body{{ if eq .Layout &amp;#34;search&amp;#34; }} class=&amp;#34;page-search&amp;#34;{{ end }}&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="gi">+ {{- partial &amp;#34;pagefind_meta.html&amp;#34; . -}}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個改動：&lt;/p>
&lt;ol>
&lt;li>&lt;code>&amp;lt;body&amp;gt;&lt;/code> 加條件 class（搜尋頁需要的 hook）&lt;/li>
&lt;li>&lt;code>&amp;lt;main&amp;gt;&lt;/code> 內加 &lt;code>pagefind_meta.html&lt;/code> partial 引用&lt;/li>
&lt;/ol>
&lt;p>整個 baseof（44 行）完全 copy、只為了這兩處 5 行改動。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>兩個改動都有更小的 override 方式：&lt;/p>
&lt;h4 id="改動-1body-class">改動 1：body class&lt;/h4>
&lt;p>Hugo 的 &lt;code>block&lt;/code> 機制讓 child template override &lt;code>block&lt;/code> 內容。如果 theme baseof 預先定義了 &lt;code>body-class&lt;/code> block：&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="c">&amp;lt;!-- theme baseof.html --&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">body&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ block &amp;#34;&lt;/span>&lt;span class="na">body-class&lt;/span>&lt;span class="err">&amp;#34;&lt;/span> &lt;span class="err">.&lt;/span> &lt;span class="err">}}{{&lt;/span> &lt;span class="na">end&lt;/span> &lt;span class="err">}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>那本地搜尋頁 layout 可以：&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="c">&amp;lt;!-- layouts/_default/search.html --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">{{ define &amp;#34;body-class&amp;#34; }}page-search{{ end }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不需要 override 整個 baseof。&lt;/p>
&lt;p>但這次的 theme 沒有 &lt;code>body-class&lt;/code> block — 所以這條路不通、必須 override。&lt;/p>
&lt;p>替代：用 &lt;code>custom_body.html&lt;/code> 之類已有的 partial hook。Theme 可能在 body 結束前 inject &lt;code>custom_body.html&lt;/code>、但那發生在 body 開始之後、無法影響 body 的 attribute。&lt;/p>
&lt;p>結論：第一個改動需要 override baseof、無更小的方式。&lt;/p>
&lt;h4 id="改動-2pagefind_metahtml-partial">改動 2：pagefind_meta.html partial&lt;/h4>
&lt;p>這個 partial 注入在 &lt;code>&amp;lt;main&amp;gt;&lt;/code> 開頭、加 hidden filter spans。可以放到 &lt;code>custom_head.html&lt;/code>（theme 已有的 hook） — 但 head 內的元素不會被 pagefind 索引、所以那條路不通。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Override theme 檔案的範圍越小、theme 升級時越容易 sync。</strong> 整檔 copy + 改 1-2 行的 override 在 theme 改了 baseof 時、本地必須手動 merge；只 override 必要的部分（用 partial 或最小檔案）讓變更面積小、merge 容易。</p>
<hr>
<h2 id="為什麼-override-要小">為什麼 override 要小</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Hugo theme 的 lookup order：本地 <code>layouts/</code> 優先於 <code>themes/&lt;name&gt;/layouts/</code>。本地有同名檔案、本地的整個內容生效、theme 的版本完全被忽略。</p>
<p>當本地 override 整個 baseof.html、只改 1-2 行：</p>
<table>
  <thead>
      <tr>
          <th>Theme 升級時的代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本地 override 不會自動更新 — 永遠是當初 copy 的版本</td>
      </tr>
      <tr>
          <td>Theme 的新功能（新 partial、改進的 SEO meta）不會生效</td>
      </tr>
      <tr>
          <td>要手動 diff <code>themes/&lt;name&gt;/layouts/baseof.html</code> 與本地、合併變更</td>
      </tr>
      <tr>
          <td>容易忘記、theme 的修正在本地永遠沒套到</td>
      </tr>
  </tbody>
</table>
<p>Override 範圍小 = merge 面積小 = 升級時手動 sync 的工作量小。</p>
<hr>
<h2 id="這次任務的-override">這次任務的 override</h2>
<h3 id="觀察">觀察</h3>
<p><code>layouts/_default/baseof.html</code> 是整個 theme 的 baseof 複製、只改了：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="ln">1</span><span class="cl"><span class="gd">- &lt;body&gt;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gd"></span><span class="gi">+ &lt;body{{ if eq .Layout &#34;search&#34; }} class=&#34;page-search&#34;{{ end }}&gt;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="gi">+   {{- partial &#34;pagefind_meta.html&#34; . -}}
</span></span></span></code></pre></div><p>兩個改動：</p>
<ol>
<li><code>&lt;body&gt;</code> 加條件 class（搜尋頁需要的 hook）</li>
<li><code>&lt;main&gt;</code> 內加 <code>pagefind_meta.html</code> partial 引用</li>
</ol>
<p>整個 baseof（44 行）完全 copy、只為了這兩處 5 行改動。</p>
<h3 id="判讀">判讀</h3>
<p>兩個改動都有更小的 override 方式：</p>
<h4 id="改動-1body-class">改動 1：body class</h4>
<p>Hugo 的 <code>block</code> 機制讓 child template override <code>block</code> 內容。如果 theme baseof 預先定義了 <code>body-class</code> block：</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;!-- theme baseof.html --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">body</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;{{ block &#34;</span><span class="na">body-class</span><span class="err">&#34;</span> <span class="err">.</span> <span class="err">}}{{</span> <span class="na">end</span> <span class="err">}}&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>那本地搜尋頁 layout 可以：</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;!-- layouts/_default/search.html --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ define &#34;body-class&#34; }}page-search{{ end }}</span></span></code></pre></div><p>不需要 override 整個 baseof。</p>
<p>但這次的 theme 沒有 <code>body-class</code> block — 所以這條路不通、必須 override。</p>
<p>替代：用 <code>custom_body.html</code> 之類已有的 partial hook。Theme 可能在 body 結束前 inject <code>custom_body.html</code>、但那發生在 body 開始之後、無法影響 body 的 attribute。</p>
<p>結論：第一個改動需要 override baseof、無更小的方式。</p>
<h4 id="改動-2pagefind_metahtml-partial">改動 2：pagefind_meta.html partial</h4>
<p>這個 partial 注入在 <code>&lt;main&gt;</code> 開頭、加 hidden filter spans。可以放到 <code>custom_head.html</code>（theme 已有的 hook） — 但 head 內的元素不會被 pagefind 索引、所以那條路不通。</p>
<p>也可以從每個 layout 內手動引入：</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;!-- layouts/_default/single.html --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ define &#34;main&#34; }}
</span></span><span class="line"><span class="ln">3</span><span class="cl">{{- partial &#34;pagefind_meta.html&#34; . -}}
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>{{ .Title }}<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</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">{{ end }}</span></span></code></pre></div><p>但這樣每個 layout（single、list、search、taxonomy）都要重複引用 — 維護成本不一定更低。</p>
<p>結論：第二個改動放在 baseof 比放在每個 layout 更乾淨。</p>
<h3 id="執行當前-override-已是最小">執行：當前 override 已是最小</h3>
<p>兩個改動都是 baseof override 較合理。但可以做的精簡是 <strong>註解標明跟 theme 版本的差異</strong>：</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></span><span class="line"><span class="ln"> 2</span><span class="cl">  本地 override theme baseof.html。
</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">  跟 themes/hugo-bearcub/layouts/_default/baseof.html 的差異：
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    1. <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span> 加條件 class=&#34;page-search&#34;（給搜尋頁的 CSS / JS hook 用）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    2. <span class="p">&lt;</span><span class="nt">main</span><span class="p">&gt;</span> 內加 partial &#34;pagefind_meta.html&#34;（注入 pagefind filter metadata）
</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">  Theme 升級時、把上面兩個改動套到新版 baseof 即可。
</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="cp">&lt;!DOCTYPE html&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;...&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">...</span></span></code></pre></div><p>註解告訴未來的維護者「這檔案 override 了什麼、為什麼、升級時要看哪些 diff」。</p>
<hr>
<h2 id="內在屬性比較四種-override-策略">內在屬性比較：四種 override 策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>改動面積</th>
          <th>升級成本</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>整檔 copy + 修改</td>
          <td>大</td>
          <td>高 — 手動 merge 整檔</td>
          <td>Theme 沒提供 hook、必要</td>
      </tr>
      <tr>
          <td>Override 加註解標明 diff</td>
          <td>大</td>
          <td>中 — 註解告訴升級者改了什麼</td>
          <td>整檔 override 的最佳實踐</td>
      </tr>
      <tr>
          <td>用 theme 提供的 partial / block hook</td>
          <td>小</td>
          <td>低 — theme 升級不影響</td>
          <td>Theme 設計時預留了 hook</td>
      </tr>
      <tr>
          <td>Fork theme 並維護</td>
          <td>整個 theme</td>
          <td>最高 — 整個 theme 都要 sync</td>
          <td>客製極深、theme 沒 hook</td>
      </tr>
  </tbody>
</table>
<p>優先選「用 theme 提供的 hook」、次選「override 加註解」、最後才考慮 fork。</p>
<hr>
<h2 id="override-的具體最佳實踐">Override 的具體最佳實踐</h2>
<h3 id="1-註解標明-diff">1. 註解標明 diff</h3>





<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">  Override theme/.../baseof.html。
</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">&lt;</span><span class="nt">body</span><span class="p">&gt;</span> 加 class hook
</span></span><span class="line"><span class="ln">5</span><span class="cl">    - <span class="p">&lt;</span><span class="nt">main</span><span class="p">&gt;</span> 內加 partial
</span></span><span class="line"><span class="ln">6</span><span class="cl">*/ -}}</span></span></code></pre></div><p>註解讓未來維護者一眼知道改了什麼。</p>
<h3 id="2-override-檔案內容對齊-theme-版本">2. Override 檔案內容對齊 theme 版本</h3>
<p>當 theme 升級時：</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">diff themes/hugo-bearcub/layouts/_default/baseof.html <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>     layouts/_default/baseof.html</span></span></code></pre></div><p>差異應該只有註解內標明的那幾處。如果差異更多 — 表示 theme 有變更我們沒套到。</p>
<h3 id="3-標明-theme-版本">3. 標明 theme 版本</h3>





<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">  Override based on themes/hugo-bearcub@v1.2.3.
</span></span><span class="line"><span class="ln">3</span><span class="cl">  跟該版本的 baseof.html 差異：...
</span></span><span class="line"><span class="ln">4</span><span class="cl">*/ -}}</span></span></code></pre></div><p>知道是基於哪個版本 override、升級到 v1.3.0 時知道要 diff 哪兩個版本。</p>
<h3 id="4-主動建議-theme-加-hook">4. 主動建議 theme 加 hook</h3>
<p>如果常需要 override theme 同樣的位置、考慮給 theme 提 PR 加 <code>block</code> 或 <code>partial</code> hook — 這樣升級後 hook 自動有、不需要繼續 override。</p>
<hr>
<h2 id="設計取捨theme-客製的策略">設計取捨：Theme 客製的策略</h2>
<p>四種做法、各自機會成本不同。優先選 A（用 theme hook）— 不夠用才退到 B / C / D。</p>
<h3 id="a用-theme-提供的-partial--block--template-hook最佳">A：用 theme 提供的 partial / block / template hook（最佳）</h3>
<ul>
<li><strong>機制</strong>：theme 預留 <code>block</code>、<code>custom_head.html</code>、<code>custom_body.html</code> 等 hook、本地只填 hook</li>
<li><strong>選 A 的理由</strong>：theme 升級不影響本地客製、hook 是公開介面</li>
<li><strong>適合</strong>：theme 設計時預留了對應 hook 的客製需求</li>
<li><strong>代價</strong>：需要 theme 預先支援、若不支援考慮給 theme 提 PR 加 hook</li>
</ul>
<h3 id="boverride-加-diff-註解這個專案的預設">B：Override 加 diff 註解（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：複製 theme 檔案到本地、改必要的部分、註解標明跟 theme 版本的差異</li>
<li><strong>跟 A 的取捨</strong>：B 不需要 theme 預留 hook、A 需要；B 升級時要手動 sync</li>
<li><strong>適合</strong>：theme 沒對應 hook、必須 override</li>
<li><strong>代價</strong>：升級時要 diff theme 新版手動 merge、註解可降低 merge 成本</li>
</ul>
<h3 id="coverride-不加註解">C：Override 不加註解</h3>
<ul>
<li><strong>機制</strong>：複製 theme 檔案、改必要部分、不註解</li>
<li><strong>跟 B 的取捨</strong>：C 寫法簡單、B 額外註解；但 C 未來維護者不知為什麼這檔案在本地、漏 sync 風險高</li>
<li><strong>C 才合理的情境</strong>：純探索性 override、之後會還原 — production 不該如此</li>
</ul>
<h3 id="dfork-theme-維護自己版本">D：Fork theme 維護自己版本</h3>
<ul>
<li><strong>機制</strong>：fork theme 整個 repo、所有客製改在 fork 內</li>
<li><strong>成本特別高的原因</strong>：每次原 theme 升級都要 merge upstream、長期維護負擔重</li>
<li><strong>D 才合理的情境</strong>：客製極深（多檔案 override + 改 internal logic）、且願意承擔 fork 維護成本</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>整檔 override theme 檔案、只改 1-2 行</td>
          <td>加註解標明 diff、未來容易升級</td>
      </tr>
      <tr>
          <td>Override 不知道是基於哪個 theme 版本</td>
          <td>加版本註解</td>
      </tr>
      <tr>
          <td>Theme 升級後本地客製失效 / 出怪事</td>
          <td>Diff theme 新版與本地 override、手動 sync</td>
      </tr>
      <tr>
          <td>多個 override 檔案、不知道為什麼存在</td>
          <td>每個 override 加用途註解</td>
      </tr>
      <tr>
          <td>同樣的客製需求要 override 多個檔案</td>
          <td>評估給 theme 提 PR 加 hook</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Override 是雙面刃 — 短期解決客製、長期增加升級成本。把 override 範圍與 diff 範圍維持最小、註解說明來由 — 是長期可維護的妥協。</p>
]]></content:encoded></item><item><title>新增頂層 content 資料夾要同步首頁 _index.md 入口</title><link>https://tarrragon.github.io/blog/report/top-level-content-folder-needs-homepage-entry/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/top-level-content-folder-needs-homepage-entry/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>新增頂層 content 資料夾的同個 commit、必須同步把該模組入口加進首頁 &lt;code>content/_index.md&lt;/code> 的對應分類段。Hugo 不會自動把 content/ 下的頂層目錄列在首頁、首頁完全靠 &lt;code>content/_index.md&lt;/code> 的 markdown 內容渲染。模組內部建得再完整、首頁沒入口 = 該模組對新讀者不可發現。&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;code>content/&amp;lt;module&amp;gt;/_index.md&lt;/code>&lt;/td>
 &lt;td>不會、Hugo 不 auto-list 頂層目錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在 &lt;code>hugo.toml&lt;/code> 加 &lt;code>[[menu.main]]&lt;/code> entry&lt;/td>
 &lt;td>顯示在頂部選單、跟首頁清單無關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在 &lt;code>content/_index.md&lt;/code> 對應分類段加 markdown link&lt;/td>
 &lt;td>顯示在首頁&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「&lt;strong>新讀者從首頁進來、能不能 1-2 個 click 走到該模組？&lt;/strong>」答案是「不能」就是入口斷裂。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>建立新教學模組（例：&lt;code>content/business/&lt;/code>）走 case-first 完整流程：寫 &lt;code>_index.md&lt;/code>、批次建 knowledge-cards、補 reading-frameworks、寫 case-analyses 文章、跑 mdtools 三項檢查、commit + push。模組內部 50 個檔案全綠、卡片網路雙向連結完整、case-first + agent team review 跑完。&lt;/p>
&lt;p>問題是首頁 &lt;code>content/_index.md&lt;/code> 的「教學系列」段沒同步更新、business 模組從首頁進入的入口完全缺席。讀者反饋「我在 blog 首頁沒看到商業這個分類」之前、新模組對 organic traffic 是隱形的。&lt;/p>
&lt;p>具體 case：c2c01bf 建立 &lt;code>content/business/&lt;/code> 50 個檔案、但 &lt;code>content/_index.md&lt;/code> 的 &lt;code>## 教學系列&lt;/code> 段沒加 &lt;code>[商業概念與策略分析](/business/)&lt;/code> 入口；f665e6d 才補上。這個 gap 在過去 backend / llm / ci 等模組可能也遇過、但當時由建立者順手補上、沒成為被紀錄的 retrospective、所以 pattern 沒浮現。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步把新增頂層資料夾跟首頁入口綁成同一個-commit">第一步：把「新增頂層資料夾」跟「首頁入口」綁成同一個 commit&lt;/h3>
&lt;p>寫新模組的 PR / commit 時、把 &lt;code>content/_index.md&lt;/code> 的修改一起放進去。Commit message 明確點出兩件事：&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">business: 新增商業教材模組 + 首頁入口
&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">- content/business/ 50 檔
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">- content/_index.md 教學系列段加入口&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>避免拆兩個 commit、避免「下個 PR 再補」。&lt;/p>
&lt;h3 id="第二步完稿檢查清單加一條">第二步：完稿檢查清單加一條&lt;/h3>
&lt;p>AGENTS.md 完稿檢查清單明列：&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">- [ ] 新增頂層 content/&amp;lt;module&amp;gt;/ 資料夾時、已同步更新 content/_index.md 對應分類段&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寫完模組準備 commit 前、過 checklist 時會被 catch。&lt;/p>
&lt;h3 id="第三步考慮工具化可選不強制">第三步：考慮工具化（可選、不強制）&lt;/h3>
&lt;p>長期可寫 mdtools check：列出 &lt;code>content/&lt;/code> 頂層資料夾、跟 &lt;code>content/_index.md&lt;/code> 的 markdown link 比對、缺的警告。但這個 check 有 false positive 風險（不是所有資料夾都該上首頁、例如 &lt;code>record&lt;/code> / &lt;code>report&lt;/code> / &lt;code>work-log&lt;/code> 是已存在但獨立的 surface、&lt;code>tags&lt;/code> 是 Hugo 自動產生），實作前先設計 whitelist 機制。建議優先靠 checklist、不急著工具化。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="新模組對首頁讀者完全不可發現">新模組對首頁讀者完全不可發現&lt;/h3>
&lt;p>讀者從 blog 首頁進來、看到的是 &lt;code>content/_index.md&lt;/code> 渲染的內容。新模組沒在那個 markdown 裡 = 該模組對 organic traffic 完全消失。內部結構建得再好、卡片再多、case 寫得再深、新讀者進不來 = 等於沒上線。Search 跟 sitemap 是備援、不是主要入口。&lt;/p>
&lt;h3 id="補救拆成第二個-commit歷史變零碎">補救拆成第二個 commit、歷史變零碎&lt;/h3>
&lt;p>漏掉的入口要事後補、commit 歷史會變成「建模組」+「補首頁入口」兩個 commit。如果在多個 PR 之間，補入口 commit 容易被誤判為瑣碎的 typo fix、reviewer 不會深究、變成 silent fix。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>新增頂層 content 資料夾的同個 commit、必須同步把該模組入口加進首頁 <code>content/_index.md</code> 的對應分類段。Hugo 不會自動把 content/ 下的頂層目錄列在首頁、首頁完全靠 <code>content/_index.md</code> 的 markdown 內容渲染。模組內部建得再完整、首頁沒入口 = 該模組對新讀者不可發現。</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>是否會在首頁顯示</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建立 <code>content/&lt;module&gt;/_index.md</code></td>
          <td>不會、Hugo 不 auto-list 頂層目錄</td>
      </tr>
      <tr>
          <td>在 <code>hugo.toml</code> 加 <code>[[menu.main]]</code> entry</td>
          <td>顯示在頂部選單、跟首頁清單無關</td>
      </tr>
      <tr>
          <td>在 <code>content/_index.md</code> 對應分類段加 markdown link</td>
          <td>顯示在首頁</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「<strong>新讀者從首頁進來、能不能 1-2 個 click 走到該模組？</strong>」答案是「不能」就是入口斷裂。</p>
<hr>
<h2 id="情境">情境</h2>
<p>建立新教學模組（例：<code>content/business/</code>）走 case-first 完整流程：寫 <code>_index.md</code>、批次建 knowledge-cards、補 reading-frameworks、寫 case-analyses 文章、跑 mdtools 三項檢查、commit + push。模組內部 50 個檔案全綠、卡片網路雙向連結完整、case-first + agent team review 跑完。</p>
<p>問題是首頁 <code>content/_index.md</code> 的「教學系列」段沒同步更新、business 模組從首頁進入的入口完全缺席。讀者反饋「我在 blog 首頁沒看到商業這個分類」之前、新模組對 organic traffic 是隱形的。</p>
<p>具體 case：c2c01bf 建立 <code>content/business/</code> 50 個檔案、但 <code>content/_index.md</code> 的 <code>## 教學系列</code> 段沒加 <code>[商業概念與策略分析](/business/)</code> 入口；f665e6d 才補上。這個 gap 在過去 backend / llm / ci 等模組可能也遇過、但當時由建立者順手補上、沒成為被紀錄的 retrospective、所以 pattern 沒浮現。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步把新增頂層資料夾跟首頁入口綁成同一個-commit">第一步：把「新增頂層資料夾」跟「首頁入口」綁成同一個 commit</h3>
<p>寫新模組的 PR / commit 時、把 <code>content/_index.md</code> 的修改一起放進去。Commit message 明確點出兩件事：</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">business: 新增商業教材模組 + 首頁入口
</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">- content/business/ 50 檔
</span></span><span class="line"><span class="ln">4</span><span class="cl">- content/_index.md 教學系列段加入口</span></span></code></pre></div><p>避免拆兩個 commit、避免「下個 PR 再補」。</p>
<h3 id="第二步完稿檢查清單加一條">第二步：完稿檢查清單加一條</h3>
<p>AGENTS.md 完稿檢查清單明列：</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">- [ ] 新增頂層 content/&lt;module&gt;/ 資料夾時、已同步更新 content/_index.md 對應分類段</span></span></code></pre></div><p>寫完模組準備 commit 前、過 checklist 時會被 catch。</p>
<h3 id="第三步考慮工具化可選不強制">第三步：考慮工具化（可選、不強制）</h3>
<p>長期可寫 mdtools check：列出 <code>content/</code> 頂層資料夾、跟 <code>content/_index.md</code> 的 markdown link 比對、缺的警告。但這個 check 有 false positive 風險（不是所有資料夾都該上首頁、例如 <code>record</code> / <code>report</code> / <code>work-log</code> 是已存在但獨立的 surface、<code>tags</code> 是 Hugo 自動產生），實作前先設計 whitelist 機制。建議優先靠 checklist、不急著工具化。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="新模組對首頁讀者完全不可發現">新模組對首頁讀者完全不可發現</h3>
<p>讀者從 blog 首頁進來、看到的是 <code>content/_index.md</code> 渲染的內容。新模組沒在那個 markdown 裡 = 該模組對 organic traffic 完全消失。內部結構建得再好、卡片再多、case 寫得再深、新讀者進不來 = 等於沒上線。Search 跟 sitemap 是備援、不是主要入口。</p>
<h3 id="補救拆成第二個-commit歷史變零碎">補救拆成第二個 commit、歷史變零碎</h3>
<p>漏掉的入口要事後補、commit 歷史會變成「建模組」+「補首頁入口」兩個 commit。如果在多個 PR 之間，補入口 commit 容易被誤判為瑣碎的 typo fix、reviewer 不會深究、變成 silent fix。</p>
<h3 id="pattern-不紀錄每個新模組都重蹈覆轍">Pattern 不紀錄、每個新模組都重蹈覆轍</h3>
<p>backend / llm / ci 等過去新模組可能也遇過相同 gap、但當時被建立者順手補了、沒成為被紀錄的 retrospective。Pattern 不浮現 = 下個建模組者重蹈覆轍。本卡的責任就是把這個 pattern 紀錄下來、進完稿檢查清單後變成結構性保證。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../single-source-of-truth/">#44 Single Source of Truth</a></strong>：本卡是 #44 在「首頁清單」維度的具體案例。<code>content/_index.md</code> 的「教學系列」段是讀者入口的 SSoT、不是 auto-derived from filesystem。建立模組時這個 SSoT 沒同步 = SSoT 違反。</li>
<li><strong><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></strong>：本卡是 #97 在「上一層 surface」的具體形態。一個模組的 metadata surface 不只是它自己的 title / description / heading、還包括「上一層索引怎麼提它」— 首頁清單就是 module 的上層 metadata surface、跟模組內 metadata 一樣值得納入 review。</li>
<li><strong><a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證</a></strong>：本卡是 #131 在「讀者旅程起點」的具體 gotcha。讀者旅程的入口是首頁、首頁沒 link 等於旅程斷在 step 0。模組內部教學完整性再高、入口斷裂就是 0-completion。</li>
<li><strong><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass scope 要蓋同類風險區</a></strong>：本卡跟 #95 的關係是「同類風險區包括上游引用點」。寫新模組時 scope 不只限模組內部、要涵蓋上游引用該模組的所有位置（首頁 / sibling 模組 _index.md cross-link / <code>AGENTS.md</code> / <code>CLAUDE.md</code>）。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commit 建了新頂層 <code>content/&lt;module&gt;/</code> 但沒動 <code>content/_index.md</code></td>
          <td>check 首頁入口、補同 commit 或下個 commit 立刻補 + 寫 retro 卡</td>
      </tr>
      <tr>
          <td>讀者反饋「我在首頁沒看到 X」</td>
          <td>too late、應在建模組同 commit 修；同步紀錄 retro 確認 pattern</td>
      </tr>
      <tr>
          <td>不確定首頁清單是否自動產生</td>
          <td>不是、<code>content/_index.md</code> 是手動 markdown、Hugo 不 auto-list</td>
      </tr>
      <tr>
          <td>完稿檢查清單沒列「同步首頁入口」</td>
          <td>補進 <code>AGENTS.md</code> 完稿檢查清單</td>
      </tr>
      <tr>
          <td>Sibling 模組 <code>_index.md</code> 提到新模組時沒 cross-link</td>
          <td>同類風險、scope 要蓋所有上游引用點、不只首頁</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用範圍</strong>：
<ul>
<li>新增 <code>content/&lt;module&gt;/</code> 頂層教學資料夾（Backend / LLM / CI / Business / 未來新模組）</li>
<li>把既有頂層資料夾「升格」成正式教學系列（例如從草稿區抽出獨立 surface）</li>
</ul>
</li>
<li><strong>不適用</strong>：
<ul>
<li>新增現有模組的子章節（例如 <code>content/backend/01-database/</code> 下新檔）— 這由模組自己的 <code>_index.md</code> 路由、不涉及首頁</li>
<li>新增 <code>content/posts/</code> 下單篇文章 — posts 是 chronological feed、首頁分類段不負責列每篇文章</li>
<li>新增 <code>content/work-log/</code> 或 <code>content/report/</code> 下單張卡片 — 同上、這些是已建立的 surface、清單在各自 <code>_index.md</code></li>
</ul>
</li>
<li><strong>邊界</strong>：本卡只處理「首頁入口」一個 surface；其他上游引用點（例如 sibling 模組 <code>_index.md</code> 提及本模組、<code>AGENTS.md</code> 提及本模組、<code>CLAUDE.md</code> 提及本模組）是同 pattern 的延伸、但個別 cross-link 要靠該位置自己的 review 維護</li>
</ul>
]]></content:encoded></item><item><title>文章列表</title><link>https://tarrragon.github.io/blog/posts/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/</guid><description>&lt;p>這個資料夾收錄 blog 本身的&lt;strong>規範文件&lt;/strong>、&lt;strong>設計/架構筆記&lt;/strong>，以及不屬於特定語言教材區（&lt;code>content/backend/&lt;/code>、&lt;code>content/go/&lt;/code>、&lt;code>content/python/&lt;/code> 等）的雜項技術筆記。&lt;/p>
&lt;p>內容大致分三類：&lt;/p>
&lt;p>&lt;strong>規範與契約&lt;/strong> — agent / 工具鏈行為的單一真實來源，被 &lt;code>AGENTS.md&lt;/code> 或其他 config 引用：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查&lt;/a> — 排版規則、反釣魚校驗、卡片雙向完整性的工具化契約&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/blog-article-template-design/" data-link-title="Blog 文章模板設計：作者品質閘門與正文分工" data-link-desc="文章模板的定位與 SSoT 歸屬：模板是作者品質閘門、正文仍走技術推導、backend 正文不暴露填表結構。">Blog 文章模板設計：作者品質閘門與正文分工&lt;/a> — 文章模板的 blog-specific SSoT，供人類作者、Claude Code 與 Codex 共用&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST — 從字串到語法樹的視角轉換&lt;/a> — 為什麼 blog 選 AST-based linter 而非 regex&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &amp;#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &amp;#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools：Go + goldmark 的 markdown 工具鏈設計&lt;/a> — 子命令架構、語言選擇 tripwire、pre-commit 與 CI 整合&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Hugo 與 Markdown 操作經驗&lt;/strong> — 具體寫作與渲染問題的事故紀錄。&lt;/p>
&lt;p>&lt;strong>AI 協作與工程心得&lt;/strong> — CI 自動除錯、技術寫作結構、專案經營相關反思。&lt;/p>
&lt;p>底下自動列出本資料夾的所有文章，依日期排序。&lt;/p></description><content:encoded><![CDATA[<p>這個資料夾收錄 blog 本身的<strong>規範文件</strong>、<strong>設計/架構筆記</strong>，以及不屬於特定語言教材區（<code>content/backend/</code>、<code>content/go/</code>、<code>content/python/</code> 等）的雜項技術筆記。</p>
<p>內容大致分三類：</p>
<p><strong>規範與契約</strong> — agent / 工具鏈行為的單一真實來源，被 <code>AGENTS.md</code> 或其他 config 引用：</p>
<ul>
<li><a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查</a> — 排版規則、反釣魚校驗、卡片雙向完整性的工具化契約</li>
<li><a href="/blog/posts/blog-article-template-design/" data-link-title="Blog 文章模板設計：作者品質閘門與正文分工" data-link-desc="文章模板的定位與 SSoT 歸屬：模板是作者品質閘門、正文仍走技術推導、backend 正文不暴露填表結構。">Blog 文章模板設計：作者品質閘門與正文分工</a> — 文章模板的 blog-specific SSoT，供人類作者、Claude Code 與 Codex 共用</li>
<li><a href="/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST — 從字串到語法樹的視角轉換</a> — 為什麼 blog 選 AST-based linter 而非 regex</li>
<li><a href="/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools：Go + goldmark 的 markdown 工具鏈設計</a> — 子命令架構、語言選擇 tripwire、pre-commit 與 CI 整合</li>
</ul>
<p><strong>Hugo 與 Markdown 操作經驗</strong> — 具體寫作與渲染問題的事故紀錄。</p>
<p><strong>AI 協作與工程心得</strong> — CI 自動除錯、技術寫作結構、專案經營相關反思。</p>
<p>底下自動列出本資料夾的所有文章，依日期排序。</p>
]]></content:encoded></item><item><title>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>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>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>用 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/record/%E5%A4%A7%E8%A6%8F%E6%A8%A1%E7%B3%BB%E7%B5%B1%E9%81%B7%E7%A7%BB%E6%96%B9%E6%B3%95%E8%AB%96/</link><pubDate>Wed, 17 Sep 2025 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E5%A4%A7%E8%A6%8F%E6%A8%A1%E7%B3%BB%E7%B5%B1%E9%81%B7%E7%A7%BB%E6%96%B9%E6%B3%95%E8%AB%96/</guid><description>&lt;h2 id="方法論起源與核心問題">方法論起源與核心問題&lt;/h2>
&lt;h3 id="概念卡片a過度工程化危機的發現">【概念卡片A】過度工程化危機的發現&lt;/h3>
&lt;p>本方法論源自一次讓AI codereview的時候發現的設計問題，結果考慮需要大幅度重構，後來設計了一整套調整流程，並請AI記錄詳細的重構方法論，所以這文章不是我寫的&lt;/p></description><content:encoded><![CDATA[<h2 id="方法論起源與核心問題">方法論起源與核心問題</h2>
<h3 id="概念卡片a過度工程化危機的發現">【概念卡片A】過度工程化危機的發現</h3>
<p>本方法論源自一次讓AI codereview的時候發現的設計問題，結果考慮需要大幅度重構，後來設計了一整套調整流程，並請AI記錄詳細的重構方法論，所以這文章不是我寫的</p>
<h4 id="複雜度爆炸的警示信號錯誤分類過度細化問題">複雜度爆炸的警示信號：錯誤分類過度細化問題</h4>
<ul>
<li><strong>觀察</strong>：系統中存在 30+ 個錯誤代碼，每個功能模組都定義自己的錯誤類型</li>
<li><strong>問題</strong>：開發者需要記憶大量錯誤代碼，維護成本指數增長</li>
<li><strong>教訓</strong>：過度分類不能解決問題，反而創造新的複雜性</li>
</ul>
<h5 id="效能聲稱與現實的落差">效能聲稱與現實的落差</h5>
<ul>
<li><strong>觀察</strong>：運行時字串拼接在熱路徑中累積效能成本</li>
<li><strong>問題</strong>：樂觀的效能估計缺乏實際測量數據支撐</li>
<li><strong>教訓</strong>：效能改善必須基於可測量的真實數據</li>
</ul>
<h6 id="跨平台一致性缺失">跨平台一致性缺失</h6>
<ul>
<li><strong>觀察</strong>：不同平台使用不同的錯誤處理模式</li>
<li><strong>問題</strong>：開發者在平台間切換時面臨學習成本</li>
<li><strong>教訓</strong>：一致性是降低複雜度的關鍵因素</li>
</ul>
<h3 id="概念卡片b分散系統的混亂狀態">【概念卡片B】分散系統的混亂狀態</h3>
<h4 id="錯誤處理模式的分裂現象實際發現的不一致模式">錯誤處理模式的分裂現象：實際發現的不一致模式</h4>





<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">// 功能模組A：字串錯誤 (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">moduleA_operation</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">failed</span><span class="p">)</span> <span class="k">throw</span> <span class="s1">&#39;OPERATION_FAILED&#39;</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="c1">// 功能模組B：自定義錯誤類別 (Sample Code)
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">moduleB_operation</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">if</span> <span class="p">(</span><span class="nx">failed</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nx">CustomError</span><span class="p">(</span><span class="s1">&#39;MODULE_B_ERROR&#39;</span><span class="p">,</span> <span class="nx">details</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="c1">// 功能模組C：原生錯誤 (Sample Code)
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">moduleC_operation</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">failed</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;Generic error message&#39;</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="c1">// 結果：三種不同的錯誤處理方式在同一系統中並存
</span></span></span></code></pre></div><h5 id="維護成本的幾何級數增長測試複雜化">維護成本的幾何級數增長：測試複雜化</h5>
<p>每種錯誤模式需要不同的測試策略：</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">// 測試模組A (Sample Code)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">expect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">moduleA</span><span class="p">()).</span><span class="nx">toThrow</span><span class="p">(</span><span class="s1">&#39;OPERATION_FAILED&#39;</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="c1">// 測試模組B (Sample Code)
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">expect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">moduleB</span><span class="p">()).</span><span class="nx">toThrow</span><span class="p">(</span><span class="nx">CustomError</span><span class="p">)</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="c1">// 測試模組C (Sample Code)
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="nx">expect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">moduleC</span><span class="p">()).</span><span class="nx">toThrow</span><span class="p">(</span><span class="nb">Error</span><span class="p">)</span></span></span></code></pre></div><h6 id="序列化問題不同錯誤格式無法統一處理">序列化問題：不同錯誤格式無法統一處理</h6>





<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">// 跨系統傳輸時的序列化困境 (Sample Code)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">serializeError</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">error</span> <span class="o">===</span> <span class="s1">&#39;string&#39;</span><span class="p">)</span> <span class="k">return</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;string&#39;</span><span class="p">,</span> <span class="nx">value</span><span class="o">:</span> <span class="nx">error</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">error</span> <span class="k">instanceof</span> <span class="nx">CustomError</span><span class="p">)</span> <span class="k">return</span> <span class="nx">error</span><span class="p">.</span><span class="nx">toJSON</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">error</span> <span class="k">instanceof</span> <span class="nb">Error</span><span class="p">)</span> <span class="k">return</span> <span class="p">{</span> <span class="nx">message</span><span class="o">:</span> <span class="nx">error</span><span class="p">.</span><span class="nx">message</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="c1">// 需要處理每種錯誤類型...
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><h3 id="概念卡片c系統性解決方案的設計發想">【概念卡片C】系統性解決方案的設計發想</h3>
<h4 id="從單點修復到整體重構的思維轉變錯誤分析">從單點修復到整體重構的思維轉變：錯誤分析</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"># 我們曾經嘗試的方法 (Sample Code)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 1. 逐個修復各 UC 的錯誤處理 → 不一致狀態持續存在</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 2. 建立新的 ErrorCodes → 與舊系統並存造成更大混亂</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 3. 強制統一標準 → 開發阻力大，容易半途而廢</span></span></span></code></pre></div><h5 id="系統性重構的核心洞察">系統性重構的核心洞察</h5>
<blockquote>
<p>分散的問題需要統一的解決方案。局部最佳化往往導致全域最差化。</p></blockquote>
<h6 id="雙軌並行的過渡策略橋接模式的創新設計">雙軌並行的過渡策略：橋接模式的創新設計</h6>





<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">// 統一錯誤處理橋接器 (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">class</span> <span class="nx">ErrorSystemBridge</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kr">static</span> <span class="nx">TRANSITION_MODES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">LEGACY_COMPATIBLE</span><span class="o">:</span> <span class="s1">&#39;legacy_first&#39;</span><span class="p">,</span>    <span class="c1">// 向後相容優先
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="nx">MODERN_PREFERRED</span><span class="o">:</span> <span class="s1">&#39;modern_first&#39;</span><span class="p">,</span>     <span class="c1">// 新系統優先
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>    <span class="nx">DUAL_VALIDATION</span><span class="o">:</span> <span class="s1">&#39;parallel_check&#39;</span><span class="p">,</span>    <span class="c1">// 雙系統驗證
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>    <span class="nx">GRADUAL_MIGRATION</span><span class="o">:</span> <span class="s1">&#39;step_by_step&#39;</span>     <span class="c1">// 逐步遷移
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <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="kr">static</span> <span class="nx">handleError</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">mode</span> <span class="o">=</span> <span class="s1">&#39;GRADUAL_MIGRATION&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1">// 核心創新：同時支援舊新系統，確保零中斷遷移
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">legacyFormat</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">toLegacyFormat</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="kr">const</span> <span class="nx">modernFormat</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">toModernFormat</span><span class="p">(</span><span class="nx">error</span><span class="p">)</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="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">selectByMode</span><span class="p">(</span><span class="nx">legacyFormat</span><span class="p">,</span> <span class="nx">modernFormat</span><span class="p">,</span> <span class="nx">mode</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="概念卡片d適配器模式的精確轉換">【概念卡片D】適配器模式的精確轉換</h3>
<h4 id="零語意損失的錯誤映射功能模組專用適配器設計">零語意損失的錯誤映射：功能模組專用適配器設計</h4>





<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">// 模組特化適配器範例 (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">class</span> <span class="nx">ModuleErrorAdapter</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kr">static</span> <span class="nx">ERROR_MAPPING</span> <span class="o">=</span> <span class="p">{</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"></span>    <span class="s1">&#39;OLD_VALIDATION_ERROR&#39;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nx">newType</span><span class="o">:</span> <span class="s1">&#39;VALIDATION_ERROR&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nx">severity</span><span class="o">:</span> <span class="s1">&#39;MODERATE&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nx">recovery</span><span class="o">:</span> <span class="s1">&#39;USER_INPUT_REQUIRED&#39;</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="s1">&#39;OLD_NETWORK_TIMEOUT&#39;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nx">newType</span><span class="o">:</span> <span class="s1">&#39;TIMEOUT_ERROR&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="nx">severity</span><span class="o">:</span> <span class="s1">&#39;HIGH&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">      <span class="nx">recovery</span><span class="o">:</span> <span class="s1">&#39;AUTOMATIC_RETRY&#39;</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="p">}</span>
</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="kr">static</span> <span class="nx">convertError</span><span class="p">(</span><span class="nx">oldError</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="kr">const</span> <span class="nx">mapping</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">ERROR_MAPPING</span><span class="p">[</span><span class="nx">oldError</span><span class="p">.</span><span class="nx">code</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">mapping</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;Unknown error type&#39;</span><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="c1">// &lt;1ms 轉換目標，保證熱路徑效能
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="k">new</span> <span class="nx">StandardError</span><span class="p">(</span><span class="nx">mapping</span><span class="p">.</span><span class="nx">newType</span><span class="p">,</span> <span class="nx">oldError</span><span class="p">.</span><span class="nx">message</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">      <span class="nx">severity</span><span class="o">:</span> <span class="nx">mapping</span><span class="p">.</span><span class="nx">severity</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">      <span class="nx">recovery</span><span class="o">:</span> <span class="nx">mapping</span><span class="p">.</span><span class="nx">recovery</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">      <span class="nx">originalCode</span><span class="o">:</span> <span class="nx">oldError</span><span class="p">.</span><span class="nx">code</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="概念卡片e方法論驗證與量化成果">【概念卡片E】方法論驗證與量化成果</h3>
<h4 id="可測量的改善指標系統複雜度降低">可測量的改善指標：系統複雜度降低</h4>
<ul>
<li>錯誤類型：30+ → 15 個核心類型 (50% 減少)</li>
<li>測試案例：分散式 → 607 個統一測試 (100% 通過率)</li>
<li>開發者學習成本：多套規範 → 單一標準</li>
</ul>
<h5 id="效能實際改善">效能實際改善</h5>





<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">// 效能基準測試結果 (Sample Code)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">performanceMetrics</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">errorCreationSpeed</span><span class="o">:</span> <span class="s1">&#39;2-10x faster&#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="nx">memoryUsage</span><span class="o">:</span> <span class="s1">&#39;35-40% reduction&#39;</span><span class="p">,</span>         <span class="c1">// 記憶體使用減少
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">serializationTime</span><span class="o">:</span> <span class="s1">&#39;&lt;1ms per error&#39;</span><span class="p">,</span>     <span class="c1">// 序列化時間
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">crossPlatformConsistency</span><span class="o">:</span> <span class="s1">&#39;100%&#39;</span>        <span class="c1">// 跨平台一致性
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><h6 id="維護成本量化">維護成本量化</h6>
<ul>
<li>程式碼重複：消除 14 個重複的錯誤處理模式</li>
<li>文檔維護：統一 API 文檔，減少 60% 維護工作量</li>
<li>新人上手：學習時間從 2 週縮短到 3 天</li>
</ul>
<h6 id="方法論的核心洞察驗證的設計原則">方法論的核心洞察：驗證的設計原則</h6>
<ol>
<li><strong>統一性優於客製化</strong>：一致的介面比特殊需求更重要</li>
<li><strong>測量優於估計</strong>：真實數據比理論分析更可靠</li>
<li><strong>漸進優於激進</strong>：可控的變更比一次性重寫更安全</li>
<li><strong>自動化優於手工</strong>：工具化流程比人工操作更可靠</li>
</ol>
<h6 id="可複製的成功模式">可複製的成功模式</h6>
<p>本方法論已在實際專案中驗證，具備跨專案、跨領域的適用性。關鍵在於將【概念卡片A-E】的思維模式系統性地應用到任何大規模重構場景中。</p>
<hr>
<h2 id="方法論核心架構五大安全支柱與風險預防機制">方法論核心架構：五大安全支柱與風險預防機制</h2>
<h3 id="支柱一漸進式風險控制---避免系統性崩潰">支柱一：漸進式風險控制 - 避免系統性崩潰</h3>
<p><strong>核心問題：</strong> 「如何防止小問題演變成系統性災難？」</p>
<h4 id="風險失控的常見模式與預防模式1雪崩效應">風險失控的常見模式與預防：模式1雪崩效應</h4>
<ul>
<li>
<p><strong>發生條件</strong>：高耦合系統中的單點故障</p>
</li>
<li>
<p><strong>危險信號</strong>：修改一個檔案需要同時修改10+個其他檔案</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:analyze --mode<span class="o">=</span>dependency_impact</span></span></code></pre></div></li>
</ul>
<h5 id="模式2狀態不一致積累">模式2：狀態不一致積累</h5>
<ul>
<li>
<p><strong>發生條件</strong>：部分遷移完成，系統處於中間狀態</p>
</li>
<li>
<p><strong>危險信號</strong>：舊新系統並存，但沒有明確的狀態管理</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:validate --check<span class="o">=</span>consistency</span></span></code></pre></div></li>
</ul>
<h6 id="模式3回滾不可能">模式3：回滾不可能</h6>
<ul>
<li>
<p><strong>發生條件</strong>：單向變更，無法逆轉操作</p>
</li>
<li>
<p><strong>危險信號</strong>：資料結構變更、API破壞性變更</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:prepare-rollback</span></span></code></pre></div></li>
</ul>
<h3 id="支柱二自動化優先原則---避免人為錯誤">支柱二：自動化優先原則 - 避免人為錯誤</h3>
<p><strong>核心問題：</strong> 「如何防止人為疏忽導致的致命錯誤？」</p>
<h4 id="人為錯誤的危險模式與自動化防護錯誤模式1模式識別失誤">人為錯誤的危險模式與自動化防護：錯誤模式1模式識別失誤</h4>
<ul>
<li>
<p><strong>風險</strong>：手工識別遺漏關鍵程式碼模式</p>
</li>
<li>
<p><strong>案例</strong>：遺漏錯誤處理語句，導致異常未捕獲</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:scan --pattern<span class="o">=</span><span class="s2">&#34;throw new Error&#34;</span></span></span></code></pre></div></li>
</ul>
<h5 id="錯誤模式2變更生成不一致">錯誤模式2：變更生成不一致</h5>
<ul>
<li>
<p><strong>風險</strong>：手工修改時邏輯不一致</p>
</li>
<li>
<p><strong>案例</strong>：A檔案使用新錯誤格式，B檔案仍用舊格式</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:convert --mode<span class="o">=</span>auto --verify-consistency</span></span></code></pre></div></li>
</ul>
<h6 id="錯誤模式3驗證覆蓋不足">錯誤模式3：驗證覆蓋不足</h6>
<ul>
<li>
<p><strong>風險</strong>：手工驗證遺漏邊界情況</p>
</li>
<li>
<p><strong>案例</strong>：正常流程驗證通過，但異常流程未測試</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:validate --comprehensive</span></span></code></pre></div></li>
</ul>
<h3 id="支柱三相容性橋接設計---避免破壞性變更">支柱三：相容性橋接設計 - 避免破壞性變更</h3>
<p><strong>核心問題：</strong> 「如何防止新舊系統整合時的災難性故障？」</p>
<h4 id="相容性失敗的危險模式與預防策略">相容性失敗的危險模式與預防策略</h4>
<h4 id="危險模式1語意漂移-semantic-drift">危險模式1：語意漂移 (Semantic Drift)</h4>
<ul>
<li>
<p><strong>風險</strong>：相同介面在新舊系統中行為不同</p>
</li>
<li>
<p><strong>症狀</strong>：測試通過但線上行為異常</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:verify-semantics --compare-behaviors</span></span></code></pre></div></li>
</ul>
<h4 id="-實作範例包裝器模式-wrapper-pattern">🛠 實作範例：包裝器模式 (Wrapper Pattern)</h4>





<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">// src/core/migration/StandardErrorWrapper.js (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">class</span> <span class="nx">StandardErrorWrapper</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kr">static</span> <span class="nx">MIGRATION_MODES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">LEGACY_ONLY</span><span class="o">:</span> <span class="s1">&#39;legacy_only&#39;</span><span class="p">,</span>      <span class="c1">// 只使用舊系統
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="nx">WRAPPER_MODE</span><span class="o">:</span> <span class="s1">&#39;wrapper_mode&#39;</span><span class="p">,</span>    <span class="c1">// 包裝器模式（預設）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>    <span class="nx">DUAL_MODE</span><span class="o">:</span> <span class="s1">&#39;dual_mode&#39;</span><span class="p">,</span>          <span class="c1">// 雙重系統並行
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>    <span class="nx">ERRORCODES_ONLY</span><span class="o">:</span> <span class="s1">&#39;errorcodes_only&#39;</span> <span class="c1">// 只使用新系統
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <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">// 向後相容的 StandardError 介面
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>  <span class="nx">constructor</span><span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">details</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">mode</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">migrationMode</span> <span class="o">||</span> <span class="s1">&#39;wrapper_mode&#39;</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 class="c1">// 安全機制：保持舊介面不變
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span>    <span class="k">this</span><span class="p">.</span><span class="nx">code</span> <span class="o">=</span> <span class="nx">code</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">message</span> <span class="o">=</span> <span class="nx">message</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">details</span> <span class="o">=</span> <span class="nx">details</span>
</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 class="c1">// 內部轉換：映射到新的 ErrorCodes 系統
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span>    <span class="k">this</span><span class="p">.</span><span class="nx">errorCodesInstance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_convertToErrorCodes</span><span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">details</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></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="nx">_convertToErrorCodes</span><span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">details</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="c1">// 安全轉換邏輯，確保語意一致性
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">mapping</span> <span class="o">=</span> <span class="nx">ErrorMapping</span><span class="p">.</span><span class="nx">getMapping</span><span class="p">(</span><span class="nx">code</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="k">return</span> <span class="k">new</span> <span class="nx">ErrorCodes</span><span class="p">[</span><span class="nx">mapping</span><span class="p">.</span><span class="nx">newType</span><span class="p">](</span><span class="nx">mapping</span><span class="p">.</span><span class="nx">newCode</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">      <span class="p">...</span><span class="nx">details</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">      <span class="nx">migrationSource</span><span class="o">:</span> <span class="s1">&#39;StandardError&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">      <span class="nx">originalCode</span><span class="o">:</span> <span class="nx">code</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="c1">// 關鍵：完全保持舊 API 的行為
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"></span>  <span class="nx">toString</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">errorCodesInstance</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">  <span class="nx">toJSON</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">errorCodesInstance</span><span class="p">.</span><span class="nx">toJSON</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="危險模式2資料轉換精度損失">危險模式2：資料轉換精度損失</h4>
<ul>
<li>
<p><strong>風險</strong>：新舊格式轉換時資料損壞</p>
</li>
<li>
<p><strong>症狀</strong>：精度丟失、型別錯誤、資料截斷</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:data-integrity --round-trip-test</span></span></code></pre></div></li>
</ul>
<h4 id="實作範例橋接器模式-bridge-pattern">實作範例：橋接器模式 (Bridge Pattern)</h4>





<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">// src/core/migration/DualErrorSystemBridge.js (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">class</span> <span class="nx">DualErrorSystemBridge</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kr">static</span> <span class="nx">DUAL_SYSTEM_MODES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">LEGACY_FIRST</span><span class="o">:</span> <span class="s1">&#39;legacy_first&#39;</span><span class="p">,</span>         <span class="c1">// 優先使用 StandardError
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="nx">ERRORCODES_FIRST</span><span class="o">:</span> <span class="s1">&#39;errorcodes_first&#39;</span><span class="p">,</span> <span class="c1">// 優先使用 ErrorCodes
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>    <span class="nx">PARALLEL</span><span class="o">:</span> <span class="s1">&#39;parallel&#39;</span><span class="p">,</span>                 <span class="c1">// 平行處理兩套系統
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>    <span class="nx">TRANSITIONAL</span><span class="o">:</span> <span class="s1">&#39;transitional&#39;</span>          <span class="c1">// 過渡模式
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <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="nx">constructor</span><span class="p">(</span><span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">mode</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">mode</span> <span class="o">||</span> <span class="s1">&#39;PARALLEL&#39;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">compatibilityLevel</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">compatibility</span> <span class="o">||</span> <span class="s1">&#39;STRICT&#39;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">performanceMonitor</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PerformanceMonitor</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="c1">// 雙向轉換核心：確保資料完整性
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span>  <span class="kr">async</span> <span class="nx">handleError</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">context</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="kr">const</span> <span class="nx">startTime</span> <span class="o">=</span> <span class="nx">performance</span><span class="p">.</span><span class="nx">now</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="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">      <span class="c1">// 安全檢查：驗證輸入錯誤的有效性
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span>      <span class="k">this</span><span class="p">.</span><span class="nx">_validateErrorInput</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">      <span class="kd">let</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">      <span class="k">switch</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">mode</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="k">case</span> <span class="s1">&#39;PARALLEL&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">          <span class="c1">// 平行處理：同時使用兩套系統，確保一致性
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"></span>          <span class="nx">result</span> <span class="o">=</span> <span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">_handleParallelMode</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">          <span class="k">break</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="k">case</span> <span class="s1">&#39;LEGACY_FIRST&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">          <span class="nx">result</span> <span class="o">=</span> <span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">_handleLegacyFirst</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">          <span class="k">break</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="k">case</span> <span class="s1">&#39;ERRORCODES_FIRST&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">          <span class="nx">result</span> <span class="o">=</span> <span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">_handleErrorCodesFirst</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">          <span class="k">break</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="k">default</span><span class="o">:</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">          <span class="nx">result</span> <span class="o">=</span> <span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">_handleTransitionalMode</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">      <span class="p">}</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="c1">//  關鍵：雙向驗證確保轉換精度
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="c1"></span>      <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">compatibilityLevel</span> <span class="o">===</span> <span class="s1">&#39;STRICT&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">        <span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">_verifyConversionAccuracy</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl">      <span class="k">return</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">conversionError</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">      <span class="c1">// 錯誤處理：轉換失敗時的安全機制
</span></span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="c1"></span>      <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">_handleConversionFailure</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">conversionError</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">    <span class="p">}</span> <span class="k">finally</span> <span class="p">{</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="c1"></span>      <span class="k">this</span><span class="p">.</span><span class="nx">performanceMonitor</span><span class="p">.</span><span class="nx">recordConversion</span><span class="p">(</span><span class="nx">performance</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">startTime</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 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="kr">async</span> <span class="nx">_verifyConversionAccuracy</span><span class="p">(</span><span class="nx">original</span><span class="p">,</span> <span class="nx">converted</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="c1">// 檢查語意完整性
</span></span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">original</span><span class="p">.</span><span class="nx">code</span> <span class="o">!==</span> <span class="nx">converted</span><span class="p">.</span><span class="nx">getOriginalCode</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">      <span class="k">throw</span> <span class="k">new</span> <span class="nx">ConversionError</span><span class="p">(</span><span class="s1">&#39;Code mapping失敗&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">
</span></span><span class="line"><span class="ln">61</span><span class="cl">    <span class="c1">// 檢查資料完整性
</span></span></span><span class="line"><span class="ln">62</span><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">roundTripResult</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_convertBack</span><span class="p">(</span><span class="nx">converted</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_isDataEquivalent</span><span class="p">(</span><span class="nx">original</span><span class="p">,</span> <span class="nx">roundTripResult</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">      <span class="k">throw</span> <span class="k">new</span> <span class="nx">ConversionError</span><span class="p">(</span><span class="s1">&#39;Round-trip 驗證失敗&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="危險模式3效能懸崖-performance-cliff">危險模式3：效能懸崖 (Performance Cliff)</h4>
<ul>
<li>
<p><strong>風險</strong>：相容性層導致效能急劇下降</p>
</li>
<li>
<p><strong>症狀</strong>：記憶體洩漏、CPU爆炸、回應時間暴增</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:performance-test --baseline</span></span></code></pre></div></li>
</ul>
<h4 id="實作範例適配器模式-adapter-pattern">實作範例：適配器模式 (Adapter Pattern)</h4>





<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">// src/core/errors/UC01ErrorAdapter.js (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">class</span> <span class="nx">UC01ErrorAdapter</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c1">// 錯誤映射表：確保精確轉換
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kr">static</span> <span class="nx">getErrorMapping</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_errorMapping</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="k">this</span><span class="p">.</span><span class="nx">_errorMapping</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="c1">// DOM_ERROR 類型映射
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>        <span class="s1">&#39;PAGE_DETECTION_FAILED&#39;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">          <span class="nx">newType</span><span class="o">:</span> <span class="s1">&#39;DOM_ERROR&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">          <span class="nx">newCode</span><span class="o">:</span> <span class="s1">&#39;DOM_001&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">          <span class="nx">riskLevel</span><span class="o">:</span> <span class="s1">&#39;HIGH&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">          <span class="nx">conversionComplexity</span><span class="o">:</span> <span class="s1">&#39;SIMPLE&#39;</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="s1">&#39;ELEMENT_EXTRACTION_FAILED&#39;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">          <span class="nx">newType</span><span class="o">:</span> <span class="s1">&#39;DOM_ERROR&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">          <span class="nx">newCode</span><span class="o">:</span> <span class="s1">&#39;DOM_002&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">          <span class="nx">riskLevel</span><span class="o">:</span> <span class="s1">&#39;MEDIUM&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">          <span class="nx">conversionComplexity</span><span class="o">:</span> <span class="s1">&#39;SIMPLE&#39;</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="c1">// NETWORK_ERROR 類型映射
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span>        <span class="s1">&#39;CONNECTION_TIMEOUT&#39;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">          <span class="nx">newType</span><span class="o">:</span> <span class="s1">&#39;NETWORK_ERROR&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">          <span class="nx">newCode</span><span class="o">:</span> <span class="s1">&#39;NET_001&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">          <span class="nx">riskLevel</span><span class="o">:</span> <span class="s1">&#39;HIGH&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">          <span class="nx">conversionComplexity</span><span class="o">:</span> <span class="s1">&#39;MEDIUM&#39;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="p">},</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 class="c1">// SYSTEM_ERROR 類型映射
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="c1"></span>        <span class="s1">&#39;STORAGE_QUOTA_EXCEEDED&#39;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">          <span class="nx">newType</span><span class="o">:</span> <span class="s1">&#39;SYSTEM_ERROR&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">          <span class="nx">newCode</span><span class="o">:</span> <span class="s1">&#39;SYS_001&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">          <span class="nx">riskLevel</span><span class="o">:</span> <span class="s1">&#39;CRITICAL&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">          <span class="nx">conversionComplexity</span><span class="o">:</span> <span class="s1">&#39;COMPLEX&#39;</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="c1">// ... 更多映射定義
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="c1"></span>      <span class="p">}</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">_errorMapping</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">
</span></span><span class="line"><span class="ln">42</span><span class="cl">  <span class="c1">// 高效能轉換：&lt;1ms 目標
</span></span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="c1"></span>  <span class="kr">static</span> <span class="nx">convertError</span><span class="p">(</span><span class="nx">standardError</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="kr">const</span> <span class="nx">startTime</span> <span class="o">=</span> <span class="nx">performance</span><span class="p">.</span><span class="nx">now</span><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="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">      <span class="c1">// 快速查找：使用預建索引
</span></span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="c1"></span>      <span class="kr">const</span> <span class="nx">mapping</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getErrorMapping</span><span class="p">()[</span><span class="nx">standardError</span><span class="p">.</span><span class="nx">code</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">mapping</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        <span class="k">throw</span> <span class="k">new</span> <span class="nx">AdapterError</span><span class="p">(</span><span class="sb">`無法映射錯誤代碼: </span><span class="si">${</span><span class="nx">standardError</span><span class="p">.</span><span class="nx">code</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl">      <span class="c1">// 安全轉換：保持所有原始資訊
</span></span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="c1"></span>      <span class="kr">const</span> <span class="nx">errorCodesInstance</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ErrorCodes</span><span class="p">[</span><span class="nx">mapping</span><span class="p">.</span><span class="nx">newType</span><span class="p">](</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">        <span class="nx">mapping</span><span class="p">.</span><span class="nx">newCode</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">        <span class="nx">standardError</span><span class="p">.</span><span class="nx">message</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">          <span class="p">...</span><span class="nx">standardError</span><span class="p">.</span><span class="nx">details</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">          <span class="c1">// 可追蹤性：保留轉換資訊
</span></span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="c1"></span>          <span class="nx">migrationInfo</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">            <span class="nx">originalCode</span><span class="o">:</span> <span class="nx">standardError</span><span class="p">.</span><span class="nx">code</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">            <span class="nx">convertedAt</span><span class="o">:</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">            <span class="nx">adapterVersion</span><span class="o">:</span> <span class="s1">&#39;UC01-v1.0&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">            <span class="nx">riskLevel</span><span class="o">:</span> <span class="nx">mapping</span><span class="p">.</span><span class="nx">riskLevel</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">        <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="c1">// 效能監控：確保符合 &lt;1ms 目標
</span></span></span><span class="line"><span class="ln">70</span><span class="cl"><span class="c1"></span>      <span class="kr">const</span> <span class="nx">conversionTime</span> <span class="o">=</span> <span class="nx">performance</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">startTime</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">conversionTime</span> <span class="o">&gt;</span> <span class="mf">1.0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl">        <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="sb">`轉換效能警告: </span><span class="si">${</span><span class="nx">conversionTime</span><span class="si">}</span><span class="sb">ms 超過 1ms 目標`</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></span><span class="line"><span class="ln">75</span><span class="cl">      <span class="k">return</span> <span class="nx">errorCodesInstance</span>
</span></span><span class="line"><span class="ln">76</span><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">77</span><span class="cl">      <span class="c1">// 失敗處理：提供降級機制
</span></span></span><span class="line"><span class="ln">78</span><span class="cl"><span class="c1"></span>      <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">_createFallbackError</span><span class="p">(</span><span class="nx">standardError</span><span class="p">,</span> <span class="nx">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">79</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">80</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">81</span><span class="cl">
</span></span><span class="line"><span class="ln">82</span><span class="cl">  <span class="c1">// 安全機制：轉換失敗時的降級處理
</span></span></span><span class="line"><span class="ln">83</span><span class="cl"><span class="c1"></span>  <span class="kr">static</span> <span class="nx">_createFallbackError</span><span class="p">(</span><span class="nx">originalError</span><span class="p">,</span> <span class="nx">conversionError</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">84</span><span class="cl">    <span class="k">return</span> <span class="k">new</span> <span class="nx">ErrorCodes</span><span class="p">.</span><span class="nx">SYSTEM_ERROR</span><span class="p">(</span><span class="s1">&#39;SYS_999&#39;</span><span class="p">,</span> <span class="s1">&#39;錯誤轉換失敗&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">85</span><span class="cl">      <span class="nx">originalError</span><span class="o">:</span> <span class="nx">originalError</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">86</span><span class="cl">      <span class="nx">conversionError</span><span class="o">:</span> <span class="nx">conversionError</span><span class="p">.</span><span class="nx">message</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">87</span><span class="cl">      <span class="nx">fallbackMode</span><span class="o">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">88</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">89</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">90</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="支柱四監控與早期預警機制---避免問題擴散">支柱四：監控與早期預警機制 - 避免問題擴散</h3>
<p><strong>核心問題：</strong> 「如何在小問題變成大災難之前攔截它們？」</p>
<h4 id="監控盲點的危險模式與預警設計">監控盲點的危險模式與預警設計</h4>
<h4 id="盲點1無聲故障-silent-failure">盲點1：無聲故障 (Silent Failure)</h4>
<ul>
<li>
<p><strong>風險</strong>：錯誤被掩蓋，問題累積到臨界點才爆發</p>
</li>
<li>
<p><strong>危險信號</strong>：測試通過但業務邏輯錯誤</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:monitor --silent-failure-detection</span></span></code></pre></div></li>
</ul>
<h4 id="盲點2級聯故障延遲">盲點2：級聯故障延遲</h4>
<ul>
<li>
<p><strong>風險</strong>：依賴鏈中的問題延遲暴露</p>
</li>
<li>
<p><strong>危險信號</strong>：單一檔案修改後，相關檔案在數小時後才出錯</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:track-cascade --dependency-chain</span></span></code></pre></div></li>
</ul>
<h4 id="盲點3效能劣化累積">盲點3：效能劣化累積</h4>
<ul>
<li>
<p><strong>風險</strong>：小幅效能下降累積成系統瓶頸</p>
</li>
<li>
<p><strong>危險信號</strong>：個別測試通過，但整體效能下降</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:health-check --performance-regression</span></span></code></pre></div></li>
</ul>
<h3 id="支柱五知識管理與錯誤預防---避免重複出問題">支柱五：知識管理與錯誤預防 - 避免重複出問題</h3>
<p><strong>核心問題：</strong> 「如何防止團隊重複犯相同的錯誤？」</p>
<h4 id="知識遺失的危險模式與防護機制">知識遺失的危險模式與防護機制</h4>
<h4 id="危險模式1隱式決策-implicit-decision">危險模式1：隱式決策 (Implicit Decision)</h4>
<ul>
<li>
<p><strong>風險</strong>：關鍵決策沒有記錄，後人重複試錯</p>
</li>
<li>
<p><strong>症狀</strong>：「為什麼當初這樣做？」無人知曉</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:decision-log --why<span class="o">=</span><span class="s2">&#34;reason&#34;</span> --what<span class="o">=</span><span class="s2">&#34;change&#34;</span></span></span></code></pre></div></li>
</ul>
<h4 id="危險模式2錯誤重現-error-repetition">危險模式2：錯誤重現 (Error Repetition)</h4>
<ul>
<li>
<p><strong>風險</strong>：相同的錯誤在不同時間、不同人員間重複發生</p>
</li>
<li>
<p><strong>症狀</strong>：類似的問題反覆出現，解決方案被遺忘</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:check-known-issues --pattern-match</span></span></code></pre></div></li>
</ul>
<h4 id="危險模式3解決方案腐化-solution-decay">危險模式3：解決方案腐化 (Solution Decay)</h4>
<ul>
<li>
<p><strong>風險</strong>：過時的解決方案被盲目應用到新情境</p>
</li>
<li>
<p><strong>症狀</strong>：舊方法在新環境中失效或造成新問題</p>
</li>
<li>
<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"># 實際實作範例（本專案）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:validate-solution --context-check</span></span></code></pre></div></li>
</ul>
<hr>
<h2 id="-方法論詳解思考流程與決策邏輯">📐 方法論詳解：思考流程與決策邏輯</h2>
<h3 id="phase-1-發現與評估---知己知彼">Phase 1: 發現與評估 - 「知己知彼」</h3>
<h4 id="11-實際問題本質挖掘---連接概念卡片a">1.1 實際問題本質挖掘 - 連接【概念卡片A】</h4>
<h5 id="基於實際專案經驗的問題識別流程">基於實際專案經驗的問題識別流程</h5>
<p>如【概念卡片A】所述，我們遭遇的核心問題是<strong>過度工程化危機</strong>。實際的問題挖掘過程：</p>
<h5 id="第一層表面症狀觀察">第一層：表面症狀觀察</h5>





<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">// 實際發現的問題模式 (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">// 症狀1：開發者困惑
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">handleErrorA</span><span class="p">()</span> <span class="p">{</span> <span class="k">throw</span> <span class="s1">&#39;STRING_ERROR&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">function</span> <span class="nx">handleErrorB</span><span class="p">()</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nx">CustomError</span><span class="p">(...)</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">function</span> <span class="nx">handleErrorC</span><span class="p">()</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(...)</span> <span class="p">}</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="c1">// 症狀2：測試複雜化
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="nx">expect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">funcA</span><span class="p">()).</span><span class="nx">toThrow</span><span class="p">(</span><span class="s1">&#39;STRING_ERROR&#39;</span><span class="p">)</span>      <span class="c1">// 字串比對
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">expect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">funcB</span><span class="p">()).</span><span class="nx">toThrow</span><span class="p">(</span><span class="nx">CustomError</span><span class="p">)</span>         <span class="c1">// 類型檢查
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nx">expect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">funcC</span><span class="p">()).</span><span class="nx">toThrow</span><span class="p">(</span><span class="sr">/error message/</span><span class="p">)</span>     <span class="c1">// 正則匹配
</span></span></span></code></pre></div><h5 id="第二層根本原因分析">第二層：根本原因分析</h5>
<ul>
<li><strong>技術債務累積</strong>：30+ 錯誤代碼，每個模組自定義規範</li>
<li><strong>架構分裂</strong>：7 個功能模組使用不同錯誤處理模式</li>
<li><strong>維護成本指數增長</strong>：新增一個錯誤需要修改多個地方</li>
</ul>
<h5 id="第三層量化問題影響">第三層：量化問題影響</h5>





<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"># 實際測量結果 (Sample Code)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:analyze --metrics
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 輸出：</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># - 錯誤處理模式：7 種不同方式</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># - 維護工時：每月 40+ 小時處理錯誤處理相關問題</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># - 新人學習成本：需要 2 週理解全部錯誤處理規範</span></span></span></code></pre></div><h4 id="12-系統現狀深度解析---連接概念卡片b">1.2 系統現狀深度解析 - 連接【概念卡片B】</h4>
<h5 id="實際分散系統混亂狀態分析">實際分散系統混亂狀態分析</h5>
<p>如【概念卡片B】所述的分散系統問題，實際解析流程：</p>
<h5 id="模組錯誤處理現狀盤點">模組錯誤處理現狀盤點</h5>





<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">// 實際發現的系統狀態 (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">systemErrorMappings</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="s1">&#39;UC-01&#39;</span><span class="o">:</span> <span class="s1">&#39;string-based errors&#39;</span><span class="p">,</span>      <span class="c1">// 10 個字串錯誤
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;UC-02&#39;</span><span class="o">:</span> <span class="s1">&#39;StandardError class&#39;</span><span class="p">,</span>      <span class="c1">// 15 個 StandardError
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;UC-03&#39;</span><span class="o">:</span> <span class="s1">&#39;native Error objects&#39;</span><span class="p">,</span>     <span class="c1">// 12 個原生 Error
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;UC-04&#39;</span><span class="o">:</span> <span class="s1">&#39;mixed approaches&#39;</span><span class="p">,</span>         <span class="c1">// 混合模式
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;UC-05&#39;</span><span class="o">:</span> <span class="s1">&#39;custom error classes&#39;</span><span class="p">,</span>     <span class="c1">// 自定義錯誤類
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;UC-06&#39;</span><span class="o">:</span> <span class="s1">&#39;result objects&#39;</span><span class="p">,</span>           <span class="c1">// 結果物件模式
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;UC-07&#39;</span><span class="o">:</span> <span class="s1">&#39;exception throwing&#39;</span>        <span class="c1">// 異常拋出模式
</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><h5 id="跨平台一致性缺失分析">跨平台一致性缺失分析</h5>





<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">// Chrome Extension 環境 (Sample Code)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">class</span> <span class="nx">ChromeErrorHandler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">serialize</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">error</span> <span class="o">===</span> <span class="s1">&#39;string&#39;</span><span class="p">)</span> <span class="k">return</span> <span class="p">{</span><span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;string&#39;</span><span class="p">,</span> <span class="nx">value</span><span class="o">:</span> <span class="nx">error</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">error</span> <span class="k">instanceof</span> <span class="nx">CustomError</span><span class="p">)</span> <span class="k">return</span> <span class="nx">error</span><span class="p">.</span><span class="nx">toJSON</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1">// 每種錯誤類型需要特殊處理...
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="p">}</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">// Flutter 環境 (Sample Code)
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kr">class</span> <span class="nx">FlutterErrorHandler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">handleError</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">error</span> <span class="nx">is</span> <span class="nb">String</span><span class="p">)</span> <span class="k">return</span> <span class="nx">StringError</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">error</span> <span class="nx">is</span> <span class="nx">CustomException</span><span class="p">)</span> <span class="k">return</span> <span class="nx">error</span><span class="p">.</span><span class="nx">toDart</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1">// 完全不同的處理邏輯...
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h5 id="技術債務量化評估">技術債務量化評估</h5>





<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"># 實際執行的分析指令 (Sample Code)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm run migration:debt-analysis
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 結果：</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># - 重複代碼：14 個相似的錯誤處理模式</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># - 測試複雜度：每個模組需要不同的測試策略</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># - 序列化問題：無法統一 JSON 格式</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># - 文檔維護：7 套不同的錯誤處理說明文檔</span></span></span></code></pre></div><h5 id="影響範圍實際測量">影響範圍實際測量</h5>





<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">// 變更影響分析工具結果 (Sample Code)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">impactAnalysis</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">affectedModules</span><span class="o">:</span> <span class="mi">7</span><span class="p">,</span>                    <span class="c1">// 7 個功能模組
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">testFilesRequiringUpdate</span><span class="o">:</span> <span class="mi">45</span><span class="p">,</span>          <span class="c1">// 45 個測試檔案
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">documentationPages</span><span class="o">:</span> <span class="mi">12</span><span class="p">,</span>                <span class="c1">// 12 頁文檔
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">developersAffected</span><span class="o">:</span> <span class="mi">4</span><span class="p">,</span>                 <span class="c1">// 4 位開發者
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nx">estimatedMigrationTime</span><span class="o">:</span> <span class="s1">&#39;3-6 months&#39;</span>   <span class="c1">// 預估遷移時間
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><h4 id="13-可行性多維度評估">1.3 可行性多維度評估</h4>
<h5 id="評估框架">評估框架</h5>





<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="nv">風險可接受性</span> <span class="o">=</span> 整體可行性</span></span></code></pre></div><h5 id="具體評估方法">具體評估方法</h5>
<ol>
<li>
<p><strong>技術可行性</strong>：</p>
<ul>
<li>是否有成熟的遷移路徑？</li>
<li>是否有相似的成功案例？</li>
<li>是否有必要的工具支援？</li>
<li>團隊是否具備相關技能？</li>
</ul>
</li>
<li>
<p><strong>資源可行性</strong>：</p>
<ul>
<li>人力資源：開發人員、測試人員、運營人員</li>
<li>工具資源：開發工具、測試環境、監控系統</li>
<li>預算資源：軟體授權、硬體設備、外部諮詢</li>
</ul>
</li>
<li>
<p><strong>時間可行性</strong>：</p>
<ul>
<li>業務窗口：是否有合適的時間點？</li>
<li>開發週期：預估工作量是否合理？</li>
<li>學習時間：團隊是否有足夠時間適應？</li>
</ul>
</li>
<li>
<p><strong>風險可接受性</strong>：</p>
<ul>
<li>業務風險：對核心業務的影響</li>
<li>技術風險：系統穩定性的影響</li>
<li>組織風險：對團隊效率的影響</li>
</ul>
</li>
</ol>
<h3 id="phase-2-策略設計">Phase 2: 策略設計</h3>
<h4 id="21-遷移策略選擇的決策樹">2.1 遷移策略選擇的決策樹</h4>
<p><strong>核心問題：</strong> 選擇什麼樣的遷移策略？</p>
<h5 id="決策維度">決策維度</h5>





<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="nv">團隊能力</span> <span class="o">=</span> 策略選擇</span></span></code></pre></div><h5 id="策略類型與適用情境">策略類型與適用情境</h5>
<ol>
<li>
<p><strong>大爆炸式遷移 (Big Bang)</strong>：</p>
<ul>
<li><strong>適用條件</strong>：影響範圍小（&lt;50個檔案）、複雜度低、團隊經驗豐富</li>
<li><strong>優勢</strong>：快速完成、狀態單純</li>
<li><strong>風險</strong>：失敗代價高、回滾困難</li>
<li><strong>決策考量</strong>：是否有充分的測試覆蓋？是否有完整的回滾計畫？</li>
</ul>
</li>
<li>
<p><strong>分批次遷移 (Batch Migration)</strong>：</p>
<ul>
<li><strong>適用條件</strong>：中等規模（50-200個檔案）、模組間耦合度低</li>
<li><strong>優勢</strong>：風險可控、可以階段性驗證</li>
<li><strong>風險</strong>：批次間可能有依賴問題</li>
<li><strong>決策考量</strong>：如何劃分批次？如何處理批次間依賴？</li>
</ul>
</li>
<li>
<p><strong>漸進式遷移 (Incremental Migration)</strong>：</p>
<ul>
<li><strong>適用條件</strong>：大規模（&gt;200個檔案）、高複雜度、長期專案</li>
<li><strong>優勢</strong>：風險最低、可以持續優化</li>
<li><strong>風險</strong>：長期維護成本、新舊系統並存複雜度</li>
<li><strong>決策考量</strong>：如何設計相容性？如何管理過渡期？</li>
</ul>
</li>
</ol>
<h4 id="22-風險分級的科學化方法">2.2 風險分級的科學化方法</h4>
<h5 id="風險評估公式設計思考">風險評估公式設計思考</h5>





<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="nv">風險等級</span> <span class="o">=</span> f<span class="o">(</span>技術複雜度, 業務影響度, 使用頻率, 依賴關係<span class="o">)</span></span></span></code></pre></div><h5 id="多維度風險評估模型">多維度風險評估模型</h5>
<ol>
<li>
<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></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></code></pre></div></li>
<li>
<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></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></code></pre></div></li>
<li>
<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></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></code></pre></div></li>
<li>
<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></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></code></pre></div></li>
</ol>
<h4 id="23-相容性策略的設計原則">2.3 相容性策略的設計原則</h4>
<p><strong>核心問題：</strong> 如何讓新舊系統和諧共存？</p>
<h5 id="設計原則">設計原則</h5>
<ol>
<li>
<p><strong>最小驚訝原則</strong>：</p>
<ul>
<li>對使用者：介面儘量保持不變</li>
<li>對開發者：學習成本儘量最小</li>
<li>對系統：行為儘量保持一致</li>
</ul>
</li>
<li>
<p><strong>漸進披露原則</strong>：</p>
<ul>
<li>先暴露基本功能，再暴露進階功能</li>
<li>先支援常用情境，再支援邊緣情境</li>
<li>先確保正確性，再優化效能</li>
</ul>
</li>
<li>
<p><strong>優雅降級原則</strong>：</p>
<ul>
<li>新功能不可用時，能回退到舊功能</li>
<li>部分失敗不影響整體功能</li>
<li>錯誤能被清楚地識別和處理</li>
</ul>
</li>
</ol>
<h5 id="相容性模式選擇邏輯">相容性模式選擇邏輯</h5>





<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">graph TD
    A[需要相容性嗎？] --&gt;|是| B[並行期多長？]
    A --&gt;|否| C[直接替換]

    B --&gt;|短期&lt;3個月| D[包裝器模式]
    B --&gt;|中期3-12個月| E[橋接模式]
    B --&gt;|長期&gt;12個月| F[適配器模式]

    D --&gt; G[重點：保持介面穩定]
    E --&gt; H[重點：雙向轉換精確]
    F --&gt; I[重點：功能完整對等]</code></pre><h3 id="phase-3-實施執行---步步為營">Phase 3: 實施執行 - 「步步為營」</h3>
<h4 id="31-執行階段的設計哲學">3.1 執行階段的設計哲學</h4>
<p><strong>核心問題：</strong> 如何在確保安全的前提下高效執行遷移？</p>
<h5 id="設計哲學">設計哲學</h5>





<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></span></code></pre></div><h5 id="執行原則">執行原則</h5>
<ol>
<li><strong>小步快跑</strong>：每次變更儘量小，但頻率儘量高</li>
<li><strong>快速回饋</strong>：每個變更都有即時的驗證機制</li>
<li><strong>持續監控</strong>：全程監控系統健康狀況</li>
<li><strong>即時調整</strong>：根據回饋快速調整策略</li>
</ol>
<h4 id="32-自動化工具鏈設計">3.2 自動化工具鏈設計</h4>
<h5 id="工具鏈架構思考">工具鏈架構思考</h5>





<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></span></code></pre></div><h5 id="跨語言通用的工具設計模式">跨語言通用的工具設計模式</h5>
<ol>
<li>
<p><strong>程式碼分析器 (Code Analyzer)</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></span><span class="line"><span class="ln">2</span><span class="cl">處理：AST解析 → 模式匹配 → 影響分析
</span></span><span class="line"><span class="ln">3</span><span class="cl">輸出：遷移候選清單 + 風險評估</span></span></code></pre></div><p><strong>設計考量</strong>：</p>
<ul>
<li>如何處理不同語言的語法差異？</li>
<li>如何識別語言特定的慣用模式？</li>
<li>如何處理宏、模板、動態特性？</li>
</ul>
</li>
<li>
<p><strong>程式碼轉換器 (Code Transformer)</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></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></code></pre></div><p><strong>設計考量</strong>：</p>
<ul>
<li>如何確保轉換的正確性？</li>
<li>如何保持原有的程式碼風格？</li>
<li>如何處理複雜的邏輯轉換？</li>
</ul>
</li>
<li>
<p><strong>驗證器 (Validator)</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></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></code></pre></div><p><strong>設計考量</strong>：</p>
<ul>
<li>如何設計全面的驗證標準？</li>
<li>如何平衡驗證嚴格度和執行效率？</li>
<li>如何處理驗證失敗的情況？</li>
</ul>
</li>
</ol>
<h4 id="33-監控與早期預警">3.3 監控與早期預警</h4>
<h5 id="監控體系設計">監控體系設計</h5>
<ol>
<li>
<p><strong>技術指標監控</strong>：</p>
<ul>
<li>編譯成功率、測試通過率</li>
<li>效能指標、錯誤率</li>
<li>程式碼品質指標</li>
</ul>
</li>
<li>
<p><strong>進度指標監控</strong>：</p>
<ul>
<li>遷移完成度、預計完成時間</li>
<li>阻礙問題、風險變化</li>
<li>資源使用情況</li>
</ul>
</li>
<li>
<p><strong>業務指標監控</strong>：</p>
<ul>
<li>系統可用性、使用者滿意度</li>
<li>功能正確性、資料完整性</li>
<li>效能表現、擴展性</li>
</ul>
</li>
</ol>
<h3 id="phase-4-驗證與優化---精益求精">Phase 4: 驗證與優化 - 「精益求精」</h3>
<h4 id="41-多層次驗證策略">4.1 多層次驗證策略</h4>
<h5 id="驗證金字塔">驗證金字塔</h5>





<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="o">(</span>頂層<span class="o">)</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="o">(</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="o">(</span>底層<span class="o">)</span></span></span></code></pre></div><h5 id="各層驗證重點">各層驗證重點</h5>
<ol>
<li>
<p><strong>單元驗證</strong>：</p>
<ul>
<li>功能正確性：每個函數的輸入輸出是否正確</li>
<li>邊界條件：異常情況是否正確處理</li>
<li>效能要求：是否滿足效能標準</li>
</ul>
</li>
<li>
<p><strong>整合驗證</strong>：</p>
<ul>
<li>介面相容：模組間互動是否正常</li>
<li>資料一致：資料傳遞是否完整</li>
<li>流程完整：業務流程是否正確</li>
</ul>
</li>
<li>
<p><strong>業務驗證</strong>：</p>
<ul>
<li>使用者體驗：是否影響使用者操作</li>
<li>業務邏輯：是否保持業務語意</li>
<li>非功能需求：效能、安全性、可用性</li>
</ul>
</li>
</ol>
<h4 id="42-效能最佳化策略">4.2 效能最佳化策略</h4>
<h5 id="最佳化原則">最佳化原則</h5>





<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">測量 → 分析 → 最佳化 → 驗證 → 循環</span></span></code></pre></div><h5 id="跨語言通用的最佳化方向">跨語言通用的最佳化方向</h5>
<ol>
<li>
<p><strong>記憶體最佳化</strong>：</p>
<ul>
<li>減少不必要的物件創建</li>
<li>優化資料結構選擇</li>
<li>改善垃圾回收效率</li>
</ul>
</li>
<li>
<p><strong>CPU最佳化</strong>：</p>
<ul>
<li>減少複雜運算</li>
<li>優化演算法選擇</li>
<li>利用語言特定的最佳化</li>
</ul>
</li>
<li>
<p><strong>IO最佳化</strong>：</p>
<ul>
<li>減少檔案存取次數</li>
<li>優化網路請求策略</li>
<li>改善資料庫查詢效率</li>
</ul>
</li>
</ol>
<h4 id="43-持續改進機制">4.3 持續改進機制</h4>
<h5 id="改進循環">改進循環</h5>





<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></span></code></pre></div><h5 id="持續改進的關鍵">持續改進的關鍵</h5>
<ol>
<li>
<p><strong>問題追蹤</strong>：</p>
<ul>
<li>建立問題回報機制</li>
<li>分類問題嚴重程度</li>
<li>追蹤解決進度</li>
</ul>
</li>
<li>
<p><strong>知識管理</strong>：</p>
<ul>
<li>記錄解決方案</li>
<li>建立最佳實踐庫</li>
<li>分享經驗教訓</li>
</ul>
</li>
<li>
<p><strong>工具進化</strong>：</p>
<ul>
<li>根據實際使用情況改進工具</li>
<li>增加新的功能特性</li>
<li>優化使用者體驗</li>
</ul>
</li>
</ol>
<hr>
<h2 id="跨語言適應性指南">跨語言適應性指南</h2>
<h3 id="語言特性考量矩陣">語言特性考量矩陣</h3>
<table>
  <thead>
      <tr>
          <th>語言特性</th>
          <th>JavaScript</th>
          <th>Dart</th>
          <th>PHP</th>
          <th>Go</th>
          <th>Laravel</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>類型系統</strong></td>
          <td>動態類型</td>
          <td>靜態類型</td>
          <td>動態類型</td>
          <td>靜態類型</td>
          <td>動態類型(PHP)</td>
      </tr>
      <tr>
          <td><strong>編譯方式</strong></td>
          <td>解釋執行</td>
          <td>AOT/JIT</td>
          <td>解釋執行</td>
          <td>編譯</td>
          <td>解釋執行</td>
      </tr>
      <tr>
          <td><strong>錯誤處理</strong></td>
          <td>Exception</td>
          <td>Exception</td>
          <td>Exception/Return</td>
          <td>Error值</td>
          <td>Exception</td>
      </tr>
      <tr>
          <td><strong>並發模型</strong></td>
          <td>Event Loop</td>
          <td>Isolate</td>
          <td>多執行緒</td>
          <td>Goroutine</td>
          <td>多執行緒</td>
      </tr>
      <tr>
          <td><strong>生態系統</strong></td>
          <td>NPM</td>
          <td>Pub</td>
          <td>Composer</td>
          <td>Module</td>
          <td>Composer</td>
      </tr>
  </tbody>
</table>
<h3 id="語言特定的遷移考量">語言特定的遷移考量</h3>
<h4 id="javascript-生態系統">JavaScript 生態系統</h4>
<h5 id="javascript-特殊考量">JavaScript 特殊考量</h5>
<ul>
<li><strong>原型鏈影響</strong>：物件屬性的動態性</li>
<li><strong>非同步模式</strong>：Promise/async-await的錯誤傳播</li>
<li><strong>模組系統</strong>：CommonJS vs ES Module的差異</li>
<li><strong>執行環境</strong>：Node.js vs Browser的API差異</li>
</ul>
<h4 id="javascript-遷移策略調整">JavaScript 遷移策略調整</h4>
<ul>
<li>重點關注執行時錯誤檢測</li>
<li>加強型別檢查（使用TypeScript或Flow）</li>
<li>特別注意非同步錯誤處理</li>
</ul>
<h4 id="dart-生態系統">Dart 生態系統</h4>
<h5 id="dart-特殊考量">Dart 特殊考量</h5>
<ul>
<li><strong>Null Safety</strong>：空值安全的型別系統</li>
<li><strong>Future/Stream</strong>：異步程式設計模式</li>
<li><strong>Flutter相依性</strong>：UI框架的特殊需求</li>
<li><strong>AOT編譯</strong>：編譯時最佳化考量</li>
</ul>
<h4 id="dart-遷移策略調整">Dart 遷移策略調整</h4>
<ul>
<li>利用靜態型別檢查提早發現問題</li>
<li>重點關注Null Safety的遷移路徑</li>
<li>考慮Flutter widget的生命週期影響</li>
</ul>
<h4 id="php-生態系統">PHP 生態系統</h4>
<h5 id="php-特殊考量">PHP 特殊考量</h5>
<ul>
<li><strong>弱型別轉換</strong>：隱式型別轉換的陷阱</li>
<li><strong>全域狀態</strong>：全域變數和函數的影響</li>
<li><strong>Include/Require</strong>：檔案載入的依賴關係</li>
<li><strong>版本相容性</strong>：PHP版本間的差異</li>
</ul>
<h4 id="php-遷移策略調整">PHP 遷移策略調整</h4>
<ul>
<li>加強執行時驗證</li>
<li>特別關注型別相關的錯誤</li>
<li>考慮使用靜態分析工具（如PHPStan）</li>
</ul>
<h4 id="go-生態系統">Go 生態系統</h4>
<h5 id="go-特殊考量">Go 特殊考量</h5>
<ul>
<li><strong>錯誤值返回</strong>：與exception機制的根本差異</li>
<li><strong>介面導向</strong>：duck typing的設計哲學</li>
<li><strong>Goroutine</strong>：並發安全考量</li>
<li><strong>模組系統</strong>：Go modules的依賴管理</li>
</ul>
<h4 id="go-遷移策略調整">Go 遷移策略調整</h4>
<ul>
<li>重新設計錯誤處理流程</li>
<li>重點關注並發安全性</li>
<li>利用靜態型別和編譯檢查</li>
</ul>
<h4 id="laravel-框架">Laravel 框架</h4>
<h5 id="laravel-特殊考量">Laravel 特殊考量</h5>
<ul>
<li><strong>Facade模式</strong>：靜態呼叫的動態解析</li>
<li><strong>服務容器</strong>：依賴注入的複雜性</li>
<li><strong>Eloquent ORM</strong>：資料存取層的抽象</li>
<li><strong>中間件機制</strong>：請求處理流水線</li>
</ul>
<h4 id="laravel-遷移策略調整">Laravel 遷移策略調整</h4>
<ul>
<li>特別關注框架層的錯誤處理</li>
<li>考慮Facade背後的服務實例</li>
<li>重點驗證資料庫互動的正確性</li>
</ul>
<h3 id="通用適應原則">通用適應原則</h3>
<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">概念層：錯誤類型、處理流程、驗證規則
</span></span><span class="line"><span class="ln">2</span><span class="cl">實現層：語言特定的語法、API、慣例</span></span></code></pre></div><h4 id="2-可配置的規則引擎">2. 可配置的規則引擎</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></span></code></pre></div><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">Phase 1：基本語法適配
</span></span><span class="line"><span class="ln">2</span><span class="cl">Phase 2：語言特性適配
</span></span><span class="line"><span class="ln">3</span><span class="cl">Phase 3：生態系統適配
</span></span><span class="line"><span class="ln">4</span><span class="cl">Phase 4：效能最佳化適配</span></span></code></pre></div><hr>
<h2 id="成功評估標準與度量指標">成功評估標準與度量指標</h2>
<h3 id="定量指標體系">定量指標體系</h3>
<h5 id="技術指標">技術指標</h5>





<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="nv">程式碼品質改善率</span> <span class="o">=</span> <span class="o">(</span>遷移後品質分數 - 遷移前品質分數<span class="o">)</span> / 遷移前品質分數 × 100%
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">遷移覆蓋率</span> <span class="o">=</span> 已遷移項目數 / 總項目數 × 100%
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">自動化率</span> <span class="o">=</span> 自動處理項目數 / 總項目數 × 100%
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nv">錯誤修復效率</span> <span class="o">=</span> 平均修復時間<span class="o">(</span>遷移後<span class="o">)</span> / 平均修復時間<span class="o">(</span>遷移前<span class="o">)</span></span></span></code></pre></div><h5 id="效率指標">效率指標</h5>





<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="nv">遷移速度</span> <span class="o">=</span> 每週完成的遷移項目數
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nv">問題解決速度</span> <span class="o">=</span> 平均問題解決時間
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">工具效率</span> <span class="o">=</span> 工具節省的人工時間 / 工具開發時間
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">學習效率</span> <span class="o">=</span> 團隊熟練度提升速度
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="sb">```</span>text
</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">##### 風險指標</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="sb">```</span>bash
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">風險實現率</span> <span class="o">=</span> 實際發生的風險數 / 預計風險數 × 100%
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nv">影響範圍控制</span> <span class="o">=</span> 實際影響範圍 / 預估影響範圍
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nv">回滾次數</span> <span class="o">=</span> 總回滾次數（越少越好）
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nv">嚴重事故率</span> <span class="o">=</span> 嚴重事故次數 / 遷移週期數</span></span></code></pre></div><h3 id="定性評估框架">定性評估框架</h3>
<h5 id="團隊滿意度">團隊滿意度</h5>
<ul>
<li><strong>開發體驗</strong>：工具易用性、文件完整性、支援及時性</li>
<li><strong>學習成本</strong>：新技術掌握難度、培訓效果、適應時間</li>
<li><strong>工作效率</strong>：日常開發效率、除錯便利性、部署簡化度</li>
</ul>
<h5 id="業務價值">業務價值</h5>
<ul>
<li><strong>功能改善</strong>：新功能可用性、效能提升、穩定性改善</li>
<li><strong>維護成本</strong>：長期維護難度、技術債務減少、擴展性提升</li>
<li><strong>競爭優勢</strong>：技術領先度、創新能力、市場響應速度</li>
</ul>
<hr>
<h2 id="最佳實踐與經驗總結">最佳實踐與經驗總結</h2>
<h3 id="成功關鍵因素">成功關鍵因素</h3>
<h5 id="1-充分的前期準備">1. 充分的前期準備</h5>
<ul>
<li><strong>深度調研</strong>：充分了解現狀和目標狀態</li>
<li><strong>風險評估</strong>：識別所有可能的風險點</li>
<li><strong>資源規劃</strong>：確保有足夠的時間和人力</li>
</ul>
<h5 id="2-強有力的工具支援">2. 強有力的工具支援</h5>
<ul>
<li><strong>自動化優先</strong>：能自動化的絕不手工</li>
<li><strong>驗證完整</strong>：多層次、全方位的驗證</li>
<li><strong>監控及時</strong>：即時發現問題並快速響應</li>
</ul>
<h5 id="3-有效的團隊協作">3. 有效的團隊協作</h5>
<ul>
<li><strong>責任明確</strong>：每個人都知道自己的職責</li>
<li><strong>溝通順暢</strong>：問題能快速上報和解決</li>
<li><strong>知識共享</strong>：經驗和教訓能及時分享</li>
</ul>
<h3 id="常見陷阱與避免方法">常見陷阱與避免方法</h3>
<h5 id="1-低估複雜度">1. 低估複雜度</h5>
<p><strong>陷阱：</strong> 認為簡單的語法替換就能完成遷移</p>
<p><strong>避免：</strong> 充分的現狀分析，考慮語意變化和邊界情況</p>
<h5 id="2-忽視相容性">2. 忽視相容性</h5>
<p><strong>陷阱：</strong> 急於求成，忽視向後相容性需求</p>
<p><strong>避免：</strong> 設計完整的相容性策略，考慮過渡期需求</p>
<h5 id="3-工具過度依賴">3. 工具過度依賴</h5>
<p><strong>陷阱：</strong> 期望工具能解決所有問題</p>
<p><strong>避免：</strong> 正確認識工具的能力邊界，準備人工處理方案</p>
<h5 id="4-缺乏回滾計畫">4. 缺乏回滾計畫</h5>
<p><strong>陷阱：</strong> 只考慮成功情況，沒有失敗應對方案</p>
<p><strong>避免：</strong> 每個階段都準備回滾方案，並定期演練</p>]]></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>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>