更換Hexo部落格搜尋工具:從Google CSE遷移至Pagefind

注意

本文由 Gemini 3.1 Pro 語言模型產生。

傳統的 Hexo 搜尋方案通常有幾個痛點:Google 自訂搜尋(CSE/PSE)帶有廣告、樣式難改;Algolia 有免費額度限制、設定複雜;而傳統的本地搜尋外掛(如 hexo-generator-search)會將所有文章打包成單一 JSON 檔,當文章量破百篇時,會嚴重拖慢網頁載入速度。

如果你追求極致的體驗,強烈建議改用基於 Rust 和 WebAssembly 開發的 Pagefind。

它專為靜態網站設計,會在 Hexo 打包出 public 靜態檔後,掃描並建立「切碎的搜尋索引(Index Fragments)」。這使得它即使面對上萬篇文章的大型網站,也能在完全無伺服器(Serverless)的環境下,維持毫秒級的極速搜尋,且頻寬消耗極低。

本文將帶你一步步在 Hexo 中實作「頂部燈箱(Modal)搜尋」的效果。

步驟一:在導覽列加入搜尋按鈕

我們不需要佔用側邊欄空間,直接在頂部導覽列加上一個搜尋觸發按鈕即可。

打開你主題的導覽列模板(通常在 layout/_partial/header.ejs 或類似路徑):

<li>
  <a href="#" id="open-search-modal">
    🔍 搜尋
  </a>
</li>

步驟二:建立燈箱介面與「防穿透腳本」

實作燈箱搜尋時,最容易遇到的是手機版(尤其是 Android 瀏覽器)的「滾動穿透」大魔王:當點擊輸入框、手機虛擬鍵盤彈出時,底層網頁會被強制捲動拉扯。

為了解決這個問題,我們必須導入「雙重 CSS 鎖定」與「觸控事件攔截」。

打開你主題的「全站主佈局模板」(通常是 layout/layout.ejsindex.ejs),在 </body> 標籤的正上方加入:

<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>

<div id="search-modal" class="search-modal" style="display: none;">
  <div class="search-modal-content">
    <div class="search-modal-header">
      <h3 class="search-modal-title">搜尋</h3>
      <span class="search-modal-close" id="close-search-modal">&times;</span>
    </div>
    <div id="pagefind-search-container"></div>
  </div>
</div>

<script>
  window.addEventListener('DOMContentLoaded', (event) => {
    // 1. 初始化 Pagefind
    new PagefindUI({
      element: "#pagefind-search-container",
      showSubResults: true
    });

    const modal = document.getElementById('search-modal');
    const openBtn = document.getElementById('open-search-modal');
    const closeBtn = document.getElementById('close-search-modal');

    // 2. 關閉燈箱邏輯:隱藏介面並拔除鎖定狀態
    function closeSearchModal() {
      modal.style.display = 'none';
      document.documentElement.classList.remove('search-locked');
      document.body.classList.remove('search-locked');
    }

    // 3. 開啟燈箱邏輯
    openBtn.addEventListener('click', function(e) {
      e.preventDefault(); // 阻止 a 標籤預設跳轉行為

      // 對 html 和 body 雙重上鎖,防止手機鍵盤推擠背景
      document.documentElement.classList.add('search-locked');
      document.body.classList.add('search-locked');

      modal.style.display = 'flex';

      // 自動聚焦:僅限電腦版 (寬度 > 768px),避免手機版一開啟就彈出鍵盤遮擋畫面
      setTimeout(() => {
        const input = document.querySelector('.pagefind-ui__search-input');
        if (input && window.innerWidth > 768) {
          input.focus();
        }
      }, 100);
    });

    closeBtn.addEventListener('click', closeSearchModal);
    window.addEventListener('click', function(event) {
      if (event.target === modal) closeSearchModal();
    });

    // 4. 終極殺手鐧:攔截手機底層的觸控滑動事件 (Touch Bleed)
    modal.addEventListener('touchmove', function(e) {
      // 若手指滑動位置不在白色搜尋區塊內,強制取消滑動,徹底切斷穿透效應
      if (!e.target.closest('.search-modal-content')) {
        e.preventDefault();
      }
    }, { passive: false });
  });
</script>

步驟三:加入自適應 CSS

在你的主題樣式檔中(例如 css/_base/layout.styl 或 custom CSS 中)加入以下樣式。
這裡的精華是使用了現代 CSS 單位 dvh,它能精準貼合手機的動態視窗高度,無視網址列或鍵盤的縮放干擾。

/* =========================================
   Pagefind 燈箱搜尋介面樣式
   ========================================= */

/* 1. 燈箱外層背景 (全螢幕、半透明黑) */
.search-modal {
  position: fixed;
  z-index: 9999;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  height: 100dvh;    /* 使用 dvh 精準貼合手機螢幕 */
  background-color: rgba(0, 0, 0, 0.6);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: 5vh;  /* 讓輸入框靠上,避免被手機虛擬鍵盤推擠 */
  backdrop-filter: blur(3px);
}

/* 2. 燈箱內部白色區塊 */
.search-modal-content {
  background-color: #fff;
  width: 90%;
  max-width: 700px;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  max-height: 85dvh; /* 限制最大高度,預留給 padding 與手機鍵盤 */
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* 3. 頂部標題與關閉按鈕區域 */
.search-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 25px;
  padding-bottom: 15px;
  border-bottom: 1px solid #eee;
}

.search-modal-title { margin: 0; font-size: 1.5em; font-weight: bold; color: #333; }
.search-modal-close { color: #aaa; font-size: 32px; font-weight: bold; cursor: pointer; line-height: 1; }
.search-modal-close:hover { color: #333; }

/* 4. 微調 Pagefind 預設 UI */
.pagefind-ui__form::before { background-color: #999; }

/* 5. 燈箱開啟時的背景鎖定狀態 (由 JS 動態切換) */
html.search-locked,
body.search-locked {
  overflow: hidden !important;
  overscroll-behavior: none !important;
  height: 100% !important;
}

步驟四:精準控制 Pagefind 搜尋範圍

若不加以限制,Pagefind 會將側邊欄、頁首、頁尾等不相關的文字全數收錄。我們需要利用 HTML 屬性來幫爬蟲「劃重點」。

打開你主題的「文章內容模板」(通常是 layout/_partial/article.ejspost.ejs):

  1. 指定搜尋主體(data-pagefind-body):找到包覆文章正文的容器(例如 <div class="content">),幫它加上這個屬性。

    <div class="article-content" data-pagefind-body>
    <%- item.content %>
        </div>
    
  2. 排除雜訊區塊(data-pagefind-ignore):如果你的文章內有自動生成的「文章目錄(TOC)」,請務必幫它加上排除屬性,否則搜尋結果的摘要會抓到一堆無意義的目錄標題。

    <div class="toc-container" data-pagefind-ignore>
        </div>
    

步驟五:清理舊版搜尋元件

  1. 打開主題目錄下的 _config.yml,在 widgets 清單中刪除 - search
  2. layout/_widget/search.ejs 更改副檔名(例如改為 search.ejs.bak)或直接刪除。

步驟六:修改部署腳本與正確的本機測試

Pagefind 的運作原理是:在 Hexo 產生完靜態 HTML 檔之後,再去掃描這些 HTML 並建立索引。

因此,請修改你 package.json 中的腳本,將 npx pagefind --site public 安插在 hexo g 之後。例如:

"scripts": {
  "build": "hexo g && npx pagefind --site public",
  "deploy": "hexo g && npx pagefind --site public && 其他部署指令…"
}

⚠️ 正確的本機測試方法

由於 Pagefind 的索引檔是「實體」產生在 public 資料夾內,若使用基於記憶體渲染的 hexo s 測試,容易發生找不到索引檔的狀況。

標準測試流程:
打開終端機,依序輸入:

# 1. 產生實體檔案與搜尋索引
hexo clean && npm run build

# 2. 使用標準的靜態伺服器來模擬正式環境 (不要用 hexo s)
npx serve public

打開瀏覽器進入 localhost:3000,即可測試。

效果如下:
Pagefind的搜尋結果