AI 正在绞尽脑汁想思路 ING···
AI 摘要
DeepSeek & Kimi

§ 背景

博客此前仅使用 Algolia 作为搜索后端,通过 hexo-algoliasearch 插件在构建时将索引推送到 Algolia 云端,前端使用 InstantSearch.js 组件渲染搜索结果。Algolia 虽然搜索体验出色(毫秒级响应、模糊匹配、Typo Tolerance),但存在以下问题:

  1. 外部依赖:依赖第三方 SaaS 服务,每月有免费额度限制(通过 pnpm run algolia-usage 查询用量)
  2. 网络延迟:国内访问 Algolia 服务器有时不稳定
  3. 离线不可用:无法在本地开发环境或无网络时使用

为解决这些问题,决定新增纯前端的本地搜索功能,与 Algolia 形成互补——用户可一键在两种模式间切换。

§ 修改的文件与目录

§ 一、博客仓库(llxlr/blog

#文件/目录改动内容
1_config.yml新增 local_search 配置段,包含 enablepathtop_n_per_articleunescapepreloadper_page 六个参数
2themes\shoka\scripts\generaters\search-data.js构建脚本自动将主题的 search-data.js 注册为 Hexo generator,无需手动配置

配置说明_config.ymllocal_searchenable: true 时,scripts/generaters/script.js 条件注入本地搜索 JS,scripts/generaters/search-data.js 条件生成 search.json。两者通过同一开关联动,无需分别控制。

§ 二、主题仓库(llxlr/hexo-theme-shoka,子模块)

tree .
themes/shoka/
├── _config.yml                                  ─ [无变更] 本地搜索配置在博客 _config.yml 中
├── languages/
│   ├── en.yml                                   ─ 新增 search.mode 多语言键
│   ├── ja.yml                                   ─ 同上
│   ├── zh-CN.yml                                ─ 同上
│   ├── zh-HK.yml                                ─ 同上
│   └── zh-TW.yml                                ─ 同上
├── layout/
│   └── _partials/
│       └── layout.njk                           ─ LOCAL.search 注入搜索模式标签
├── scripts/generaters/
│   ├── script.js                                ─ 条件注入 local-search.js + CONFIG.localSearch 全局对象
│   └── search-data.js                           ─ [新建] 生成 search.json 的 Hexo generator
└── source/
    ├── css/_common/components/third-party/
    │   └── search.styl                          ─ 搜索模式标签样式 + 本地搜索结果样式 + 关键词高亮 + spinner 动画
    └── js/_app/
        ├── local-search.js                      ─ [新建] LocalSearch 类(核心搜索引擎,435行)
        ├── page.js                              ─ 重构搜索入口:algoliaSearch() → searchController(),支持双模式
        └── pjax.js                              ─ 函数名更新:algoliaSearch(pjax) → searchController(pjax)

§ 架构设计

Error: Evaluation failed: DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
    at __puppeteer_evaluation_script__:2:37

§ 实现细节

§ 1. 搜索数据生成(search-data.js

在 Hexo generate 阶段,注册一个自定义 generator 生成 /search.json

// 核心逻辑
const posts = locals.posts.sort("-date").filter((p) => p.published !== false);
const data = posts.map((post) => ({
    title: post.title,
    url: "/" + post.path,
    content: post.content
        .replace(/<[^>]+>/g, "") // HTML → 纯文本
        .replace(/&amp;/g, "&") // 反转义 HTML 实体
        // ... 其他实体
        .replace(/\s+/g, " ") // 压缩空白
        .trim(),
    categories: post.categories.data.map((c) => c.name),
}));

与现有的 hexo-generator-searchdb 插件不同,这个自定义 generator 更轻量——直接输出文章路径(而非编码后的 URI),前端无需额外解码;categories 以数组形式保留结构,便于渲染面包屑导航。

§ 2. 前端搜索引擎(local-search.js

LocalSearch 类的核心算法参考了 hexo-generator-searchdb 的前端实现,但针对 shoka 主题做了适配和优化。

搜索流程

用户输入 → 分词 (按空格/连字符) → 关键词数组
    ↓
getIndexByWord() → 在 title + content 中定位每个关键词的位置
    ↓
getResultItems() → 对每篇文章计算匹配度
    ↓
mergeIntoSlice() → 以匹配位置为中心截取上下文片段(前20字符 + 后100字符)
    ↓
排序 (includedCount ↓ → hitCount ↓ → id ↓)
    ↓
highlightKeyword() → 用 <mark class="search-keyword"> 包裹关键词
    ↓
分页渲染 (per_page 控制每页条数)

关键设计

  • top_n_per_article:限制每篇文章最多展示的匹配片段数。设为 1 时每篇文章只显示最相关的一个片段,避免结果列表冗长
  • unescape:开启时对搜索词进行 HTML 反转义,允许搜索如 &amp; 等特殊字符
  • 预加载preload: true):页面加载时即 fetch search.json,用户打开搜索框时无需等待
  • 延迟加载:默认不预取,仅在用户首次打开搜索框时异步 fetch,避免影响首屏加载速度

关键词高亮(URL 参数传递):

搜索结果页面 URL 携带 ?highlight=关键词 参数。文章页面加载时,highlightSearchWords() 使用 TreeWalker 遍历正文文本节点,对匹配的关键词包裹 <mark class="search-keyword">,同时跳过 <button><select><textarea>.mermaid 图表等不应被高亮的元素。

§ 3. 搜索模式切换(page.js 重构)

将原有的 algoliaSearch() 重构为 searchController(),统一管理两种搜索模式:

  • 构建阶段:根据 CONFIG.search(Algolia)和 CONFIG.localSearch(本地)存在性决定可用模式
  • DOM 结构:在搜索弹窗中添加 .search-mode-tabs 标签栏(仅当两种模式都可用时显示),Algolia 和本地结果分别放在 #search-algolia#search-local 两个独立容器中
  • 模式切换switchMode() 调用 activate()/deactivate() 控制本地搜索的生命周期;切换到 Algolia 前会清空其查询状态,避免上一次搜索的旧结果残留
  • 打开搜索弹窗:焦点自动落在 .search-input,两种模式共用同一个输入框

§ 4. 样式(search.styl

新增样式涵盖:

  • 模式切换标签.search-mode-tabs):胶囊式按钮,active 状态填充主题色,hover 时边框变色,带过渡动画
  • 搜索结果.local-search-hit-item):标题、分类面包屑、匹配片段的分层展示;分类面包屑用 i-angle-right 图标分隔
  • 关键词高亮mark.search-keyword):主题色背景 + 白色文字 + 圆角
  • Spinner.search-spinner):纯 CSS 旋转动画,在数据加载和搜索执行时显示
  • 分页:复用 Algolia 分页的 .pagination 样式,#local-search-pagination#search-pagination 共用同一套 CSS

§ 5. 多语言支持

在 5 个语言文件中新增 search.mode 键:

# zh-CN.yml
search:
  mode:
    algolia: Algolia
    local: 本地
# en.yml
search:
  mode:
    algolia: Algolia
    local: Local

layout.njk 将这些标签注入 LOCAL.search.mode,由前端 searchController() 读取并渲染模式切换标签。

§ 关键设计决策

  1. 双模式而非替代:本地搜索不是 Algolia 的替代品,而是互补选项。Algolia 提供模糊匹配、Typo Tolerance 等高级特性;本地搜索提供离线可用、零延迟的体验。用户可根据场景自由切换

  2. 自定义 generator 而非第三方插件:不依赖 hexo-generator-searchdb,而是在主题内实现 search-data.js。这样做的好处:(1)输出格式与 shoka 主题的 page.js 完全耦合,无需插件间的格式适配;(2)categories 以数组形式保留,避免前端再做字符串解析;(3)避免额外的 npm 依赖

  3. 懒加载优先:默认 preload: false,仅在用户首次打开搜索弹窗时才 fetch search.json。这保证了首屏加载不受搜索数据影响(search.json 通常有数百 KB 到数 MB)

  4. Pjax 兼容

    • 搜索实例在 siteInit() 时创建一次,Pjax 导航时不重复初始化
    • Pjax 成功后自动执行 deactivate(),关闭搜索弹窗
    • 搜索结果中的链接经由 Pjax 处理(pjax.refresh(resultsListEl)),保持无刷新导航体验
    • 监听 pjax:success 事件,自动对新页面执行 URL 关键词高亮
  5. 搜索词高亮的两种场景

    • 搜索弹窗内highlightKeyword() 在渲染结果 HTML 时包裹 <mark> 标签
    • 文章页面内highlightSearchWords() 通过 DOM TreeWalker 在正文中定位并包裹,且排除交互元素和图表

§ 相关提交

仓库Commit日期说明
主题c270bc32026-07-10✨ 添加本地搜索功能
博客00b99dd2026-07-11✨ 更新配置文件,新增 Meting API

§ 使用方法

在博客 _config.yml 中配置:

local_search:
    enable: true # 启用本地搜索
    path: /search.json # 搜索数据文件路径
    top_n_per_article: 1 # 每篇文章最多展示的匹配片段数
    unescape: false # 是否对搜索词进行 HTML 反转义
    preload: false # 是否在页面加载时预取搜索数据
    per_page: 10 # 搜索结果每页条数

配置完成后执行 pnpm run build,构建过程会自动:

  1. 生成 public/search.json
  2. 注入 local-search.js 到全局脚本
  3. 生成 CONFIG.localSearch 全局配置对象

前端会自动检测 CONFIG.search(Algolia)和 CONFIG.localSearch 的可用性,并在搜索弹窗中渲染对应的模式切换标签。

§ 参考链接


在提问之前,你应该学会如何提问