<?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>Pagefind on Tarragon</title><link>https://tarrragon.github.io/blog/tags/pagefind/</link><description>Recent content in Pagefind on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/pagefind/index.xml" rel="self" type="application/rss+xml"/><item><title>在外部組件上加客製功能：以邊界為中心的方法選擇</title><link>https://tarrragon.github.io/blog/report/external-component-customization/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/external-component-customization/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>客製外部組件的穩定性與「離組件邊界多近」成正比。&lt;/strong> 組件作者維護組件內部的一致性；客製貼著組件的對外介面（CLI 參數、CSS reset 邊界、CSS layer）做，組件升級不會打到客製。深入組件內部（注入子節點、改 source 行為）的客製則仰賴組件的內部實作，作者不保證下個版本還相容。&lt;/p>
&lt;p>這條原則對應到 Pagefind 整合時的三個具體情境，本文逐一拆解。&lt;/p>
&lt;hr>
&lt;h2 id="邊界概念為什麼用邊界看外部組件">邊界概念：為什麼用「邊界」看外部組件&lt;/h2>
&lt;h3 id="這類問題的本質商業邏輯">這類問題的本質（商業邏輯）&lt;/h3>
&lt;p>組件 = 一組對外契約 + 內部實作。對外契約是作者保證的東西（CLI 參數、CSS class name、CSS variable hooks），內部實作是達成契約的手段、可能在版本之間變動。&lt;/p>
&lt;p>客製貼著對外契約做，等於跟作者站在同一邊；客製動到內部實作，等於跟作者每個版本對抗。&lt;/p>
&lt;p>「邊界」這個概念把組件分成「契約面」與「實作面」 — 客製選位置時，依離邊界遠近排序：&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>介面契約（穩定）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件 DOM 邊界外&lt;/td>
 &lt;td>邊界元素的 class / id 名稱（半穩定）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件 DOM 邊界內&lt;/td>
 &lt;td>內部 DOM 結構（隨框架渲染週期變動）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件內部邏輯&lt;/td>
 &lt;td>source code（升級時必衝突）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="這次任務涉及的邊界case">這次任務涉及的邊界（CASE）&lt;/h3>
&lt;p>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;/td>
 &lt;td>&lt;code>--root-selector&lt;/code> 參數&lt;/td>
 &lt;td>「組件看到什麼資料」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重置邊界&lt;/td>
 &lt;td>&lt;code>.pagefind-ui--reset&lt;/code> class&lt;/td>
 &lt;td>對 UA 樣式的覆寫範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>specificity 邊界&lt;/td>
 &lt;td>svelte hash class（雙寫到 30）&lt;/td>
 &lt;td>組件 CSS 的權重起點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每條邊界各有對應的客製介面 — 緊貼這些介面比繞道進內部安全得多。&lt;/p>
&lt;hr>
&lt;h2 id="索引邊界用-root-selector-限縮搜尋範圍">索引邊界：用 root-selector 限縮搜尋範圍&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：Pagefind 的索引流程透過 &lt;code>--root-selector&lt;/code> 參數控制「哪些 HTML 進索引」。這是組件預期的客製介面。&lt;/p>
&lt;h3 id="這次的觀察">這次的觀察&lt;/h3>
&lt;p>theme 的 site header 包含 &lt;code>&amp;lt;h1&amp;gt;{{ .Site.Title }}&amp;lt;/h1&amp;gt;&lt;/code>，每一頁 DOM 上的第一個 h1 都是站名「Tarragon」。Pagefind 預設取頁面第一個 h1 當搜尋結果 title — 結果所有結果都顯示「Tarragon」。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>第一個 h1 是站名而非文章名 — 索引「看到」的範圍跟我們以為的不同。要修的不是 theme（影響其他頁面），是 Pagefind 的 input 範圍。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>





&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">npx -y pagefind --site public --root-selector main&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>&amp;lt;main&amp;gt;&lt;/code> 內才包含文章 h1；site header 在 &lt;code>&amp;lt;body&amp;gt;&lt;/code> 直接子節點、不在 &lt;code>&amp;lt;main&amp;gt;&lt;/code> 內，自然被排除。&lt;/p>
&lt;p>&lt;strong>完成標準&lt;/strong>：每一筆結果的 title 顯示文章 h1，而非站名。&lt;/p>
&lt;hr>
&lt;h2 id="重置邊界在落腳處重建-ua-樣式">重置邊界：在落腳處重建 UA 樣式&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：CSS reset 的有效範圍與 ancestor class 綁定。元素換位置就換 reset context — 這是 CSS 的基本機制，不是 bug。&lt;/p>
&lt;h3 id="這次的觀察-1">這次的觀察&lt;/h3>
&lt;p>Filter UI（&lt;code>.pagefind-ui__filter-panel&lt;/code>，本質是 &lt;code>&amp;lt;fieldset&amp;gt;&lt;/code>）需要從 &lt;code>.pagefind-ui&lt;/code> 搬到外部 aside（左側 sidebar）。搬完後 fieldset 的瀏覽器預設邊框冒出來。&lt;/p>
&lt;h3 id="判讀-1">判讀&lt;/h3>
&lt;p>&lt;code>.pagefind-ui--reset&lt;/code> 用 &lt;code>all: unset&lt;/code> 重置所有後代元素的 UA 樣式。fieldset 搬出 &lt;code>.pagefind-ui&lt;/code> 後，這個 ancestor 不在了，UA 樣式（包括 fieldset 預設邊框）回到原樣。&lt;/p>
&lt;h3 id="執行-1">執行&lt;/h3>
&lt;p>在 fieldset 落腳處（aside 內）重新關掉 UA 樣式：&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">search-filter-slot&lt;/span> &lt;span class="nt">fieldset&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="k">border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&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="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&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="k">margin&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>完成標準&lt;/strong>：fieldset 在新位置看起來跟在 &lt;code>.pagefind-ui&lt;/code> 內一樣 — 沒有多餘邊框。&lt;/p>
&lt;hr>
&lt;h2 id="specificity-邊界跳出線性比較戰場">specificity 邊界：跳出線性比較戰場&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：當組件透過 hash class 把 specificity 拉高到一般客製寫法蓋不過時，邊界落在 CSS 樣式分層機制 — 不在個別 selector 數字。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>客製外部組件的穩定性與「離組件邊界多近」成正比。</strong> 組件作者維護組件內部的一致性；客製貼著組件的對外介面（CLI 參數、CSS reset 邊界、CSS layer）做，組件升級不會打到客製。深入組件內部（注入子節點、改 source 行為）的客製則仰賴組件的內部實作，作者不保證下個版本還相容。</p>
<p>這條原則對應到 Pagefind 整合時的三個具體情境，本文逐一拆解。</p>
<hr>
<h2 id="邊界概念為什麼用邊界看外部組件">邊界概念：為什麼用「邊界」看外部組件</h2>
<h3 id="這類問題的本質商業邏輯">這類問題的本質（商業邏輯）</h3>
<p>組件 = 一組對外契約 + 內部實作。對外契約是作者保證的東西（CLI 參數、CSS class name、CSS variable hooks），內部實作是達成契約的手段、可能在版本之間變動。</p>
<p>客製貼著對外契約做，等於跟作者站在同一邊；客製動到內部實作，等於跟作者每個版本對抗。</p>
<p>「邊界」這個概念把組件分成「契約面」與「實作面」 — 客製選位置時，依離邊界遠近排序：</p>
<table>
  <thead>
      <tr>
          <th>客製位置</th>
          <th>跨越時的依賴</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件對外介面</td>
          <td>介面契約（穩定）</td>
      </tr>
      <tr>
          <td>組件 DOM 邊界外</td>
          <td>邊界元素的 class / id 名稱（半穩定）</td>
      </tr>
      <tr>
          <td>組件 DOM 邊界內</td>
          <td>內部 DOM 結構（隨框架渲染週期變動）</td>
      </tr>
      <tr>
          <td>組件內部邏輯</td>
          <td>source code（升級時必衝突）</td>
      </tr>
  </tbody>
</table>
<h3 id="這次任務涉及的邊界case">這次任務涉及的邊界（CASE）</h3>
<p>Pagefind 提供三條值得辨識的邊界：</p>
<table>
  <thead>
      <tr>
          <th>邊界名稱</th>
          <th>介面形式</th>
          <th>範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引邊界</td>
          <td><code>--root-selector</code> 參數</td>
          <td>「組件看到什麼資料」</td>
      </tr>
      <tr>
          <td>重置邊界</td>
          <td><code>.pagefind-ui--reset</code> class</td>
          <td>對 UA 樣式的覆寫範圍</td>
      </tr>
      <tr>
          <td>specificity 邊界</td>
          <td>svelte hash class（雙寫到 30）</td>
          <td>組件 CSS 的權重起點</td>
      </tr>
  </tbody>
</table>
<p>每條邊界各有對應的客製介面 — 緊貼這些介面比繞道進內部安全得多。</p>
<hr>
<h2 id="索引邊界用-root-selector-限縮搜尋範圍">索引邊界：用 root-selector 限縮搜尋範圍</h2>
<p><strong>核心定義</strong>：Pagefind 的索引流程透過 <code>--root-selector</code> 參數控制「哪些 HTML 進索引」。這是組件預期的客製介面。</p>
<h3 id="這次的觀察">這次的觀察</h3>
<p>theme 的 site header 包含 <code>&lt;h1&gt;{{ .Site.Title }}&lt;/h1&gt;</code>，每一頁 DOM 上的第一個 h1 都是站名「Tarragon」。Pagefind 預設取頁面第一個 h1 當搜尋結果 title — 結果所有結果都顯示「Tarragon」。</p>
<h3 id="判讀">判讀</h3>
<p>第一個 h1 是站名而非文章名 — 索引「看到」的範圍跟我們以為的不同。要修的不是 theme（影響其他頁面），是 Pagefind 的 input 範圍。</p>
<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">npx -y pagefind --site public --root-selector main</span></span></code></pre></div><p><code>&lt;main&gt;</code> 內才包含文章 h1；site header 在 <code>&lt;body&gt;</code> 直接子節點、不在 <code>&lt;main&gt;</code> 內，自然被排除。</p>
<p><strong>完成標準</strong>：每一筆結果的 title 顯示文章 h1，而非站名。</p>
<hr>
<h2 id="重置邊界在落腳處重建-ua-樣式">重置邊界：在落腳處重建 UA 樣式</h2>
<p><strong>核心定義</strong>：CSS reset 的有效範圍與 ancestor class 綁定。元素換位置就換 reset context — 這是 CSS 的基本機制，不是 bug。</p>
<h3 id="這次的觀察-1">這次的觀察</h3>
<p>Filter UI（<code>.pagefind-ui__filter-panel</code>，本質是 <code>&lt;fieldset&gt;</code>）需要從 <code>.pagefind-ui</code> 搬到外部 aside（左側 sidebar）。搬完後 fieldset 的瀏覽器預設邊框冒出來。</p>
<h3 id="判讀-1">判讀</h3>
<p><code>.pagefind-ui--reset</code> 用 <code>all: unset</code> 重置所有後代元素的 UA 樣式。fieldset 搬出 <code>.pagefind-ui</code> 後，這個 ancestor 不在了，UA 樣式（包括 fieldset 預設邊框）回到原樣。</p>
<h3 id="執行-1">執行</h3>
<p>在 fieldset 落腳處（aside 內）重新關掉 UA 樣式：</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">search-filter-slot</span> <span class="nt">fieldset</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">padding</span><span class="p">:</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">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>完成標準</strong>：fieldset 在新位置看起來跟在 <code>.pagefind-ui</code> 內一樣 — 沒有多餘邊框。</p>
<hr>
<h2 id="specificity-邊界跳出線性比較戰場">specificity 邊界：跳出線性比較戰場</h2>
<p><strong>核心定義</strong>：當組件透過 hash class 把 specificity 拉高到一般客製寫法蓋不過時，邊界落在 CSS 樣式分層機制 — 不在個別 selector 數字。</p>
<h3 id="這次的觀察-2">這次的觀察</h3>
<p>Pagefind 透過 svelte 把 class name 加 hash 重複寫進 selector（<code>.x.svelte-y.svelte-y</code>），把 specificity 從 10 提升到 30。一般客製 CSS specificity 10-20、蓋不過去。</p>
<h3 id="判讀-2">判讀</h3>
<p>Specificity 是線性數字比較 — 跟組件作者比 specificity 是無贏的軍備競賽。要真正解這類覆寫戰、需要跳出「線性比較」這個維度本身。</p>
<h3 id="執行-2">執行</h3>
<p>具體做法（<code>@import url(...) layer(...)</code>）與升級兼容性、其他外部組件的 layer 策略，由 <a href="../css-layers-over-specificity/">#24 CSS Layers 取代 specificity 戰</a> 完整展開。在邊界辨識上、本篇要記住的是：<strong>遇到 specificity 30+ 的覆寫戰、不要往 <code>!important</code> / <code>.x.x</code> 雙寫的方向加碼、改去看 layer 維度</strong>。</p>
<p><strong>完成標準</strong>：所有原本需要 <code>!important</code> 或 <code>.x.x</code> 雙寫的覆寫，可以用單純的 class selector 寫。</p>
<hr>
<h2 id="客製深度的內在屬性比較">客製深度的內在屬性比較</h2>
<p>選擇客製位置時，用三個<strong>內在屬性</strong>比較 — 不用「實作要多久」這類時間維度：</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>依賴前提</th>
          <th>升級風險</th>
          <th>可逆性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件對外介面</td>
          <td>介面 stable across 版本</td>
          <td>低 — 介面是公開契約</td>
          <td>高 — 改一個參數即可還原</td>
      </tr>
      <tr>
          <td>DOM 邊界外（class hooks）</td>
          <td>邊界元素 class / id 穩定</td>
          <td>中 — class 改名才打破</td>
          <td>中 — 改選擇器即可</td>
      </tr>
      <tr>
          <td>DOM 邊界內</td>
          <td>內部 DOM 結構穩定</td>
          <td>高 — 框架重繪可能即時打破</td>
          <td>低 — 客製跟內部結構深度耦合</td>
      </tr>
      <tr>
          <td>內部邏輯（fork / patch）</td>
          <td>整個組件 source</td>
          <td>最高 — 升級必衝突</td>
          <td>最低 — 重新 merge</td>
      </tr>
  </tbody>
</table>
<p><strong>選擇順序</strong>：先試最外層；不夠用再往內推一層。每往內一層、依賴前提增加、升級風險上升、可逆性下降。</p>
<hr>
<h2 id="設計取捨客製位置的層次選擇">設計取捨：客製位置的層次選擇</h2>
<p>四種做法、各自機會成本不同。預設選最外層（A）、不夠用才往內推一層。</p>
<blockquote>
<p>本篇是 <a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a> 抽象原則在「客製位置選擇」這個面向的應用。</p></blockquote>
<h3 id="a組件公開介面層最佳">A：組件公開介面層（最佳）</h3>
<ul>
<li><strong>機制</strong>：用組件提供的 CLI 參數、props、CSS variable hook、event API 客製</li>
<li><strong>選 A 的理由</strong>：作者保證跨版本相容、客製跟組件升級無關</li>
<li><strong>適合</strong>：能被介面覆蓋的客製需求（多數常見場景）</li>
<li><strong>代價</strong>：受介面範圍限制、超出範圍只能往內推</li>
</ul>
<h3 id="b鄰接層class-hooksreset-邊界這個專案的次選">B：鄰接層（class hooks、reset 邊界）（這個專案的次選）</h3>
<ul>
<li><strong>機制</strong>：用組件邊界元素的 class / id / data attribute 寫客製 CSS、注意 CSS reset 邊界</li>
<li><strong>跟 A 的取捨</strong>：B 介面外但仍貼著組件邊界、A 在介面內；B 在 class 名穩定時 OK、改名就壞</li>
<li><strong>B 比 A 好的情境</strong>：組件未提供對應介面、但有穩定的 class 邊界可掛勾</li>
</ul>
<h3 id="c邊界內-dom-操作">C：邊界內 DOM 操作</h3>
<ul>
<li><strong>機制</strong>：JS 改組件內部 DOM 結構 / 節點屬性 / 注入元素</li>
<li><strong>跟 A/B 的取捨</strong>：C 直接操控、跟 framework reconciliation 競爭；風險顯著上升</li>
<li><strong>C 才合理的情境</strong>：A/B 都無法達成、且能容忍每次升級重做（<a href="../component-boundary-and-js-impact/">#13</a> 的安全規則必看）</li>
</ul>
<h3 id="dfork--patch-組件-source">D：Fork / patch 組件 source</h3>
<ul>
<li><strong>機制</strong>：fork 整個組件、改 source code、自家維護版本</li>
<li><strong>成本特別高的原因</strong>：每次升級都要 merge upstream、客製永久綁在 fork</li>
<li><strong>D 才合理的情境</strong>：客製需求超過所有其他選項、且願意承擔 fork 維護成本</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>下次遇到組件整合任務、看到這些訊號就走「找邊界」的路：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>對應邊界類型</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件抓到的資料超過你想要的範圍</td>
          <td>索引邊界</td>
          <td>找組件的 <code>root</code> / <code>scope</code> / <code>selector</code> 參數</td>
      </tr>
      <tr>
          <td>同一段 CSS 在不同位置表現不一致</td>
          <td>重置邊界</td>
          <td>比對兩個位置的 ancestor class、看 reset 是否還在</td>
      </tr>
      <tr>
          <td><code>!important</code> 寫了還是沒蓋過</td>
          <td>specificity 邊界</td>
          <td>看組件 CSS 是否用 hash class 提升 specificity；考慮 CSS Layers</td>
      </tr>
      <tr>
          <td>組件渲染後客製 UI 消失</td>
          <td>DOM 結構邊界</td>
          <td>把客製 UI 搬出組件、用 CSS 控制視覺位置</td>
      </tr>
      <tr>
          <td>組件升級後客製失效</td>
          <td>內部邏輯邊界</td>
          <td>把客製重寫到組件介面層</td>
      </tr>
  </tbody>
</table>
<p><strong>使用順序</strong>：訊號出現 → 對應邊界 → 嘗試該邊界提供的介面 → 介面不夠用、才考慮往內推一層。</p>
]]></content:encoded></item><item><title>資源載入時序：lazy chunk 與 critical path</title><link>https://tarrragon.github.io/blog/report/lazy-loading-and-critical-path/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/lazy-loading-and-critical-path/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資源載入時序的設計選擇是「首次渲染速度」與「首次互動延遲」的權衡 — 不是越早載越好。&lt;/strong> 把不影響首次渲染的資源延後（lazy load）、首屏更快；但延後的資源在使用者真正需要時可能還沒到、互動延遲。盤點時兩者一起看。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼載入時序需要設計">為什麼載入時序需要設計&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>每個資源都有兩個時點：&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>在 critical path（首屏）還是 lazy（首次互動才下載）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用&lt;/td>
 &lt;td>下載完 + parse + 執行完&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把資源放 critical path = 阻塞首屏渲染；放 lazy = 首屏更快但首次互動可能等。&lt;/p>
&lt;p>對搜尋頁：使用者打開 &lt;code>/search/&lt;/code> 但可能不立刻搜尋 — pagefind index lazy load 是合理選擇。但若打開後立刻打字、index 還沒載完、第一次搜尋有明顯延遲。&lt;/p>
&lt;h3 id="critical-path-vs-lazy-的標準">Critical path vs lazy 的標準&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資源類型&lt;/th>
 &lt;th>通常的選擇&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>視覺主體 CSS（首屏看到的）&lt;/td>
 &lt;td>Critical path&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>互動 JS（事件處理）&lt;/td>
 &lt;td>DOMContentLoaded 後即可&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大型功能模組（搜尋 index）&lt;/td>
 &lt;td>Lazy、使用者觸發才載&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>圖片 / 影片&lt;/td>
 &lt;td>Lazy 視可見性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇原則：&lt;strong>「首屏渲染需要嗎？」是 → critical；「使用者一定會用嗎？」否 → lazy&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1pagefind-index-下載延遲">風險 1：Pagefind index 下載延遲&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：PagefindUI 在 mount 時開始下載 entry chunk、之後才能搜尋。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>entry chunk（&lt;code>pagefind-entry.json&lt;/code>）~ 10KB&lt;/li>
&lt;li>下載 + parse 約 100-500ms（看網路）&lt;/li>
&lt;li>使用者打開搜尋頁立刻打字時、第一個字可能還沒搜尋&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者打開 /search/ 立刻打字、第一個字沒回應、過 200-500ms 才開始搜尋。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：DevTools Network 看 entry chunk 下載時間。&amp;gt; 500ms 考慮 preload 機制。&lt;/p>
&lt;h3 id="風險-2個別-search-chunk-的-lazy-load">風險 2：個別 search chunk 的 lazy load&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：使用者搜尋特定 term 時、pagefind 動態下載對應 chunk。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：每個搜尋 term 對應一個 chunk（依 term 前綴分）。第一次搜尋某個 prefix 要下載對應 chunk、之後同 prefix 搜尋走 cache。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：搜尋特定字時稍有延遲（200-500ms）、之後就快了。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：Pagefind 內建 cache 機制、多數情境表現可接受。若極慢可考慮 service worker preload chunk。&lt;/p>
&lt;h3 id="風險-3pagefind-ui-script-下載">風險 3：Pagefind UI script 下載&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;code>&amp;lt;script src=&amp;quot;/blog/pagefind/pagefind-ui.js&amp;quot;&amp;gt;&lt;/code>。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>~ 50KB minified、需在使用者打字前載完&lt;/li>
&lt;li>有 &lt;code>defer&lt;/code> 不阻塞 HTML parsing、但仍占 critical path 寬度&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：搜尋頁初次載入比一般頁慢。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：確認 &lt;code>&amp;lt;script&amp;gt;&lt;/code> 有 &lt;code>defer&lt;/code> attribute、使用者開啟搜尋頁後背景下載、不阻塞 HTML 渲染。&lt;/p>
&lt;h3 id="風險-4assetssearchcss-與-pagefind-uicss-載入順序">風險 4：assets/search.css 與 pagefind-ui.css 載入順序&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：兩個 stylesheet 都在 &lt;code>&amp;lt;head&amp;gt;&lt;/code> 載入。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>pagefind-ui.css 5-10KB、search.css（拆檔後）3-5KB&lt;/li>
&lt;li>兩者都阻塞首屏渲染（CSS render-blocking）&lt;/li>
&lt;li>加總 &amp;lt; 20KB、影響輕微&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：rare、僅在極慢網路下感受到。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：DevTools Network 看 CSS 下載時間。考慮：&lt;/p>
&lt;ul>
&lt;li>把 critical CSS inline（首屏需要的部分）、其他 lazy&lt;/li>
&lt;li>用 Hugo &lt;code>resources.Get | minify | fingerprint&lt;/code> 確保最小化&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="內在屬性比較四種載入策略">內在屬性比較：四種載入策略&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>策略&lt;/th>
 &lt;th>首屏速度&lt;/th>
 &lt;th>首次互動延遲&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全 critical path&lt;/td>
 &lt;td>慢&lt;/td>
 &lt;td>0（即可用）&lt;/td>
 &lt;td>小型站、所有資源都重要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lazy load 大型模組&lt;/td>
 &lt;td>快&lt;/td>
 &lt;td>中 — 使用者觸發才下載&lt;/td>
 &lt;td>搜尋、富互動模組&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Critical path + lazy mix&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>一般情境（pagefind 走這條）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service Worker preload&lt;/td>
 &lt;td>中 — 首次載完後永久快&lt;/td>
 &lt;td>0 — 從 cache 取&lt;/td>
 &lt;td>高頻使用者、PWA&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對搜尋頁的場景：&lt;strong>Lazy load 大型模組&lt;/strong>是 pagefind 預設行為、合理；考慮再進一步可以 preload entry chunk 在 idle 時。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資源載入時序的設計選擇是「首次渲染速度」與「首次互動延遲」的權衡 — 不是越早載越好。</strong> 把不影響首次渲染的資源延後（lazy load）、首屏更快；但延後的資源在使用者真正需要時可能還沒到、互動延遲。盤點時兩者一起看。</p>
<hr>
<h2 id="為什麼載入時序需要設計">為什麼載入時序需要設計</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>每個資源都有兩個時點：</p>
<table>
  <thead>
      <tr>
          <th>時點</th>
          <th>含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開始下載</td>
          <td>在 critical path（首屏）還是 lazy（首次互動才下載）</td>
      </tr>
      <tr>
          <td>可用</td>
          <td>下載完 + parse + 執行完</td>
      </tr>
  </tbody>
</table>
<p>把資源放 critical path = 阻塞首屏渲染；放 lazy = 首屏更快但首次互動可能等。</p>
<p>對搜尋頁：使用者打開 <code>/search/</code> 但可能不立刻搜尋 — pagefind index lazy load 是合理選擇。但若打開後立刻打字、index 還沒載完、第一次搜尋有明顯延遲。</p>
<h3 id="critical-path-vs-lazy-的標準">Critical path vs lazy 的標準</h3>
<table>
  <thead>
      <tr>
          <th>資源類型</th>
          <th>通常的選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視覺主體 CSS（首屏看到的）</td>
          <td>Critical path</td>
      </tr>
      <tr>
          <td>互動 JS（事件處理）</td>
          <td>DOMContentLoaded 後即可</td>
      </tr>
      <tr>
          <td>大型功能模組（搜尋 index）</td>
          <td>Lazy、使用者觸發才載</td>
      </tr>
      <tr>
          <td>圖片 / 影片</td>
          <td>Lazy 視可見性</td>
      </tr>
  </tbody>
</table>
<p>選擇原則：<strong>「首屏渲染需要嗎？」是 → critical；「使用者一定會用嗎？」否 → lazy</strong>。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1pagefind-index-下載延遲">風險 1：Pagefind index 下載延遲</h3>
<p><strong>位置</strong>：PagefindUI 在 mount 時開始下載 entry chunk、之後才能搜尋。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>entry chunk（<code>pagefind-entry.json</code>）~ 10KB</li>
<li>下載 + parse 約 100-500ms（看網路）</li>
<li>使用者打開搜尋頁立刻打字時、第一個字可能還沒搜尋</li>
</ul>
<p><strong>症狀</strong>：使用者打開 /search/ 立刻打字、第一個字沒回應、過 200-500ms 才開始搜尋。</p>
<p><strong>第一個該查的</strong>：DevTools Network 看 entry chunk 下載時間。&gt; 500ms 考慮 preload 機制。</p>
<h3 id="風險-2個別-search-chunk-的-lazy-load">風險 2：個別 search chunk 的 lazy load</h3>
<p><strong>位置</strong>：使用者搜尋特定 term 時、pagefind 動態下載對應 chunk。</p>
<p><strong>判讀</strong>：每個搜尋 term 對應一個 chunk（依 term 前綴分）。第一次搜尋某個 prefix 要下載對應 chunk、之後同 prefix 搜尋走 cache。</p>
<p><strong>症狀</strong>：搜尋特定字時稍有延遲（200-500ms）、之後就快了。</p>
<p><strong>第一個該查的</strong>：Pagefind 內建 cache 機制、多數情境表現可接受。若極慢可考慮 service worker preload chunk。</p>
<h3 id="風險-3pagefind-ui-script-下載">風險 3：Pagefind UI script 下載</h3>
<p><strong>位置</strong>：<code>&lt;script src=&quot;/blog/pagefind/pagefind-ui.js&quot;&gt;</code>。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>~ 50KB minified、需在使用者打字前載完</li>
<li>有 <code>defer</code> 不阻塞 HTML parsing、但仍占 critical path 寬度</li>
</ul>
<p><strong>症狀</strong>：搜尋頁初次載入比一般頁慢。</p>
<p><strong>第一個該查的</strong>：確認 <code>&lt;script&gt;</code> 有 <code>defer</code> attribute、使用者開啟搜尋頁後背景下載、不阻塞 HTML 渲染。</p>
<h3 id="風險-4assetssearchcss-與-pagefind-uicss-載入順序">風險 4：assets/search.css 與 pagefind-ui.css 載入順序</h3>
<p><strong>位置</strong>：兩個 stylesheet 都在 <code>&lt;head&gt;</code> 載入。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>pagefind-ui.css 5-10KB、search.css（拆檔後）3-5KB</li>
<li>兩者都阻塞首屏渲染（CSS render-blocking）</li>
<li>加總 &lt; 20KB、影響輕微</li>
</ul>
<p><strong>症狀</strong>：rare、僅在極慢網路下感受到。</p>
<p><strong>第一個該查的</strong>：DevTools Network 看 CSS 下載時間。考慮：</p>
<ul>
<li>把 critical CSS inline（首屏需要的部分）、其他 lazy</li>
<li>用 Hugo <code>resources.Get | minify | fingerprint</code> 確保最小化</li>
</ul>
<hr>
<h2 id="內在屬性比較四種載入策略">內在屬性比較：四種載入策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>首屏速度</th>
          <th>首次互動延遲</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全 critical path</td>
          <td>慢</td>
          <td>0（即可用）</td>
          <td>小型站、所有資源都重要</td>
      </tr>
      <tr>
          <td>Lazy load 大型模組</td>
          <td>快</td>
          <td>中 — 使用者觸發才下載</td>
          <td>搜尋、富互動模組</td>
      </tr>
      <tr>
          <td>Critical path + lazy mix</td>
          <td>中</td>
          <td>低</td>
          <td>一般情境（pagefind 走這條）</td>
      </tr>
      <tr>
          <td>Service Worker preload</td>
          <td>中 — 首次載完後永久快</td>
          <td>0 — 從 cache 取</td>
          <td>高頻使用者、PWA</td>
      </tr>
  </tbody>
</table>
<p>對搜尋頁的場景：<strong>Lazy load 大型模組</strong>是 pagefind 預設行為、合理；考慮再進一步可以 preload entry chunk 在 idle 時。</p>
<hr>
<h2 id="preload-的取捨">Preload 的取捨</h2>
<p>預先載入下一步可能需要的資源 — 加快互動、但浪費頻寬（若使用者最終沒用）。</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">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;preload&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/blog/pagefind/pagefind-entry.json&#34;</span> <span class="na">as</span><span class="o">=</span><span class="s">&#34;fetch&#34;</span> <span class="na">crossorigin</span><span class="p">&gt;</span></span></span></code></pre></div><p>放 head、瀏覽器在 critical path 完成後 idle 時開始下載。</p>
<p><strong>值得做的條件</strong>：</p>
<ul>
<li>使用者進入此頁的明確意圖會觸發該資源（搜尋頁進入 = 會搜尋）</li>
<li>資源不大（entry chunk &lt; 10KB OK）</li>
</ul>
<p><strong>不值得</strong>：</p>
<ul>
<li>使用者可能只看不用（首頁載 search index 通常不值得）</li>
<li>資源很大（不要 preload 整個 search index）</li>
</ul>
<hr>
<h2 id="設計取捨資源載入時序的策略">設計取捨：資源載入時序的策略</h2>
<p>四種做法、各自機會成本不同。預設按資源性質選 — 影響首屏 → A、使用者必用大型模組 → B、進入此頁必觸發 → C。</p>
<h3 id="acritical-path首屏阻塞">A：Critical path（首屏阻塞）</h3>
<ul>
<li><strong>機制</strong>：CSS <code>&lt;link&gt;</code> 在 head、JS 用 <code>defer</code> 在 head 或 body 末</li>
<li><strong>選 A 的理由</strong>：首屏渲染就需要、不能延後</li>
<li><strong>適合</strong>：視覺主體 CSS（首屏可見）、互動處理 JS（DOMContentLoaded 後即用）</li>
<li><strong>代價</strong>：阻塞首屏渲染、加總大小要控制（&lt; 50KB 為佳）</li>
</ul>
<h3 id="blazy-load使用者觸發才載">B：Lazy load（使用者觸發才載）</h3>
<ul>
<li><strong>機制</strong>：用動態 import / IntersectionObserver / 按 click 載入</li>
<li><strong>跟 A 的取捨</strong>：B 首屏快、A 首次互動快；B 在使用者必用時造成互動延遲</li>
<li><strong>B 比 A 好的情境</strong>：大型功能模組（搜尋 index、富文字編輯器）、使用者可能不用</li>
</ul>
<h3 id="cpreload打賭使用者會用">C：Preload（打賭使用者會用）</h3>
<ul>
<li><strong>機制</strong>：<code>&lt;link rel=&quot;preload&quot;&gt;</code> 在 idle 時下載、需要時從 cache 取</li>
<li><strong>跟 A/B 的取捨</strong>：C 不阻塞首屏（idle 下載）、需要時無延遲；但賭錯（使用者不用）就浪費頻寬</li>
<li><strong>C 比 A/B 好的情境</strong>：進入此頁的明確意圖會觸發該資源（搜尋頁進入 = 必搜尋）+ 資源不大（&lt; 10KB）</li>
</ul>
<h3 id="dservice-worker-預先-cache">D：Service Worker 預先 cache</h3>
<ul>
<li><strong>機制</strong>：第一次造訪時 cache 進 SW、之後從 cache 取</li>
<li><strong>跟 C 的取捨</strong>：D 第一次造訪後永久快、C 每次都要重新 preload；D 適合 PWA 等「重複造訪」場景</li>
<li><strong>D 比 C 好的情境</strong>：高頻使用者、PWA 應用、需要 offline 支援</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者打開頁面立刻互動有明顯延遲</td>
          <td>該互動依賴的資源是否 lazy、是否值得 preload</td>
      </tr>
      <tr>
          <td>首屏渲染慢、CSS / JS 阻塞</td>
          <td>DevTools Network 找 critical path 中可拆 lazy 的資源</td>
      </tr>
      <tr>
          <td>Lazy 資源永遠不被觸發</td>
          <td>該資源預設或許不必 lazy（不會 lazy 也不會貴）</td>
      </tr>
      <tr>
          <td>慢網路 / 行動裝置使用者抱怨</td>
          <td>用 DevTools Network throttling 模擬、量首屏與首次互動</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：載入時序是設計決定、不是預設。每個資源「critical / lazy / preload」三選一明確選、不要全部丟 critical path。</p>
]]></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></channel></rss>