Repository Wiki
LyraVoid/Mizuki

Pagefind 站内搜索

Mizuki 基于 Pagefind(^1.5.2)实现的静态站内全文搜索能力:构建期由 pagefind --site dist 扫描 Astro 构建产物生成索引分片,运行时由 Search.svelte 懒加载搜索引擎并调用 window.pagefind.search() 完成检索。本页覆盖从构建流水线、索引范围控制到浏览器端搜索交互的完整链路。

目的与范围

本页(site-services.pagefind-search)覆盖 Mizuki 中 Pagefind 站内搜索这一能力的全部实现面:

  • 构建流水线中索引生成步骤的位置与执行顺序(package.json 的 build 脚本)
  • 索引范围与内容控制:pagefind.yml 的排除选择器、页面模板中的 data-pagefind-body / data-pagefind-weight / data-pagefind-meta / data-pagefind-ignore 标记
  • 运行时搜索交互:src/components/organisms/navigation/Search.svelte 的懒加载策略、环境守卫、搜索调用与状态管理
  • 全局类型契约:src/global.d.ts 中的 window.pagefind 与 window.loadPagefind 声明
  • 搜索面板与站点统一浮层面板的集成点(src/utils/panel-manager.ts 与相关样式)

以下内容有意留给兄弟页面,本页只做边界性提示:

  • 搜索面板的浮层开合、互斥、动画等通用面板机制由 panel-manager.ts 统一管理 —— 该机制本身属于面板管理主题,本页仅说明 search-panel 如何接入
  • 壁纸透明 / 移动端导航栏等视觉样式细节(wallpaper-navbar-transparent.css、mobile-navbar.css)属于站点样式主题
  • Markdown 渲染与语法高亮属于 Markdown 扩展主题,本页只关注其容器上的索引标记

概述

Pagefind 是面向静态站点的搜索引擎:它在构建完成后扫描 dist/ 目录中的静态 HTML,把正文文本切分为索引分片(fragment)并连同一段浏览器端 JS 一起写入 dist/pagefind/。用户首次触发搜索时才加载这段 JS 与相关分片,因此对站点首屏体积几乎零成本。这解释了 Mizuki 中的两个核心设计决策:

  1. 索引在构建链中后置:build 脚本把 pagefind --site dist 排在 astro build 之后,确保扫描的是最终 HTML(见「构建期索引流水线」)。
  2. 引擎在浏览器端懒加载:Search.svelte 不在页面加载时引入 Pagefind,而是在搜索面板展开或桌面搜索框展开时才调用 window.loadPagefind()(见「运行时搜索流程」)。

README 将其列为站点特性之一:「基于 Pagefind 的高级搜索功能」。

Source: README.md

架构

整体能力横跨构建期与运行时两个阶段,共四类参与者:

Loading diagram...

要点说明:

  • 页面模板(Markdown.astro、[...permalink].astro、astro.config.mjs) 只负责在产物 HTML 上打标记(data-pagefind-* 属性);它们不参与索引计算本身,真正的索引由 Pagefind CLI 在 dist/ 上完成。
  • pagefind.yml 提供仓库级排除规则,防止公式、锚点图标、搜索面板自身等内容污染索引(详见「索引范围与内容控制」)。
  • Search.svelte 是唯一的运行时消费者:它通过 window.loadPagefind() 触发懒加载,通过 window.pagefind.search() 执行查询,并在开发模式下退化为 mock 数据(详见「运行时搜索流程」)。
  • panel-manager.ts 把 search-panel 纳入站点统一浮层管理,与导航菜单、移动端目录等面板共享同一套开合行为。

构建期索引流水线

索引生成是构建链中独立的一步。package.json 的 build 脚本如下:

json
"build": "node scripts/update-anime.mjs && astro build && node scripts/check-global-style-loading.mjs && pagefind --site dist && node scripts/check-font-loading.mjs",

Source: package.json

执行顺序及其设计意图:

顺序命令与搜索能力的关系
1node scripts/update-anime.mjs前置数据更新,与搜索无直接关系
2astro build产出 dist/ 静态 HTML,其中已携带 data-pagefind-* 标记
3node scripts/check-global-style-loading.mjs样式完整性校验
4pagefind --site dist索引生成:扫描 dist/,写入 dist/pagefind/
5node scripts/check-font-loading.mjs字体校验(在索引生成之后执行)

pagefind --site dist 必须位于 astro build 之后——这是硬性顺序约束:Pagefind 只能索引已渲染的静态 HTML,而不是源码中的 Markdown。仓库同时声明了 CLI 依赖:

json
"pagefind": "^1.5.2",

Source: package.json

运维含义:部署产物必须包含 dist/pagefind/ 目录,且每次内容变更后需要重新执行完整 build 链才能让索引与页面同步。

索引范围与内容控制

索引「收什么、不收什么」由两层机制决定:仓库级排除选择器(pagefind.yml)与页面级正内容标记(data-pagefind-*)。

仓库级排除规则:pagefind.yml

yaml
1exclude_selectors: 2 - "span.katex" 3 - "span.katex-display" 4 - "[data-pagefind-ignore]" 5 - ".search-panel" 6 - "#search-panel"

Source: pagefind.yml

选择器排除对象设计意图
span.katexKaTeX 行内公式节点公式的内部渲染结构(字体定位、重复符号)对全文检索无意义,只会污染索引
span.katex-displayKaTeX 块级公式节点同上,针对展示公式
[data-pagefind-ignore]显式标记为忽略的任意元素通用逃生门;当前用于标题锚点图标(见下文)
.search-panel / #search-panel搜索面板自身防止搜索面板内的占位文本、历史关键词等被索引,造成「搜索框搜到自己」的自我指涉

页面级正内容标记

Mizuki 使用 data-pagefind-body 圈定正文索引边界。Markdown 内容容器上:

astro
1<div 2 data-pagefind-body 3 class={`prose dark:prose-invert prose-base !max-w-none custom-md ${className}`} 4>

Source: Markdown.astro

文章页标题([...permalink].astro)在 data-pagefind-body 基础上叠加了权重与元数据:

astro
1<div 2 data-pagefind-body 3 data-pagefind-weight="10" 4 data-pagefind-meta="title" 5 class="transition w-full block font-bold mb-3 ..."

Source: [...permalink].astro

属性含义:

  • data-pagefind-body:声明该元素内部才是可索引正文;页面其余部分(导航、侧栏、页脚)被排除在索引之外——这是控制索引噪音最关键的一道闸门。
  • data-pagefind-weight="10":提升该元素文本的匹配权重,使命中标题关键词的页面在结果中更靠前。
  • data-pagefind-meta="title":把该元素的文本登记为结果元数据 title,供结果列表渲染标题,而不是拿正文片段充当标题。

反向排除:锚点图标

astro.config.mjs 中标题锚点图标显式声明忽略,与 pagefind.yml 的 [data-pagefind-ignore] 规则形成配合:

js
className: ["anchor-icon"], "data-pagefind-ignore": true,

Source: astro.config.mjs

这是「通用选择器 + 具体标记」的组合模式:pagefind.yml 定义规则,任意模板只需输出 data-pagefind-ignore 属性即可把噪声元素移出索引,无需修改仓库级配置。

运行时搜索流程

运行时的搜索交互集中在 Search.svelte。以下序列图展示一次完整搜索的时序:

Loading diagram...

关键代码证据(节选自 grep 结果,行号为该文件真实行号):

ts
1// 面板展开时懒加载引擎 2if ( 3 !panel?.classList.contains("float-panel-closed") && 4 typeof window.loadPagefind === "function" 5) { 6 window.loadPagefind(); 7}

Source: Search.svelte

ts
1// 执行搜索,带三重环境守卫 2let searchResults: SearchResult[] = []; 3if (import.meta.env.PROD && pagefindLoaded && window.pagefind) { 4 const response = await window.pagefind.search(keyword); 5 // ... 映射结果 6} else { 7 searchResults = []; 8 console.error("Pagefind is not available in production environment."); 9}

Source: Search.svelte

ts
1// 初始化时探测引擎可用性 2initialized = true; 3pagefindLoaded = 4 typeof window !== "undefined" && 5 !!window.pagefind && 6 typeof window.pagefind.search === "function"; 7console.log("Pagefind status on init:", pagefindLoaded);

Source: Search.svelte

ts
1// 开发模式降级为 mock 数据,并监听就绪事件 2console.log( 3 "Pagefind is not available in development mode. Using mock data.", 4); 5... 6const handlePagefindReady = () => { 7 console.log("Pagefind ready event received."); 8 ... 9};

Source: Search.svelte

懒加载触发点

Search.svelte 中有两个触发 window.loadPagefind() 的路径:

  1. 搜索面板展开(第 44–48 行):面板脱离 float-panel-closed 状态即触发。
  2. 桌面端搜索框展开(第 57–59 行):isDesktopSearchExpanded 为真且 window.loadPagefind 为函数时触发。

两条路径都先用 typeof window.loadPagefind === "function" 判空,这是一种防御式写法:loadPagefind 由外部脚本注入,若全局脚本缺失(例如本地开发未跑索引),搜索组件不应抛出 TypeError。

类型契约:global.d.ts

运行时依赖两个全局注入点,其类型在 src/global.d.ts 中声明:

ts
pagefind: { search: (query: string) => Promise<{ ...

Source: global.d.ts

ts
loadPagefind?: () => Promise<void>;

Source: global.d.ts

要点:

  • window.pagefind.search(query: string) 返回 Promise<{...}>,是搜索的唯一运行时查询入口;组件不直接 import Pagefind 模块,而是消费全局对象——这与「索引产物由构建期 CLI 写入 dist/pagefind/、由其中的脚本挂载全局对象」的机制一致。
  • window.loadPagefind 被声明为可选(loadPagefind?:),Search.svelte 中所有调用点都先做 typeof === "function" 检查,类型契约与运行时守卫互相印证。
  • 由于本页源码工具预算限制,search() 返回结构中字段(如 results 数组)的完整展开未能在本次收集范围内逐字段核对;完整结构可参见 global.d.ts 第 45–52 行。

配置选项参考

配置项位置类型 / 取值默认说明
exclude_selectorspagefind.ymlstring[]无(文件中列出 5 条)从索引中排除的选择器列表;命中即不进入索引
data-pagefind-body模板属性boolean attribute未标记则按默认策略声明可索引正文边界
data-pagefind-weight[...permalink].astro 标题string(数值 "10")未设置按默认权重提升元素内文本的匹配权重
data-pagefind-meta[...permalink].astro 标题"title"无把元素文本登记为结果元数据 title
data-pagefind-ignoreastro.config.mjs 锚点图标true无元素级排除标记,配合 pagefind.yml 的 [data-pagefind-ignore] 规则
pagefind 依赖版本package.json^1.5.2—CLI 及二进制版本(pnpm-lock.yaml 中含 @pagefind/darwin-arm64@1.5.2 等平台包)

API 参考

window.pagefind.search(query: string): Promise<{...}>

运行时搜索唯一入口,由 dist/pagefind/ 产物脚本注入到全局对象。

参数:

  • query(string):用户输入的搜索关键词。

返回: Promise<{...}> —— 结果集合;Search.svelte 在第 126 行 await window.pagefind.search(keyword) 后消费它。返回结构的完整字段定义见 global.d.ts 第 45–52 行(本页未逐字段展开)。

前置条件:

  • 生产构建(import.meta.env.PROD)
  • 引擎已就绪(pagefindLoaded === true,即 window.pagefind 存在且 search 为函数)

window.loadPagefind(): Promise<void>(可选)

懒加载入口,可选注入。参数: 无。返回: Promise<void>,完成时 window.pagefind 应已挂载。

调用方: 仅 Search.svelte(两处触发点:搜索面板展开、桌面搜索框展开),且每次调用前先做 typeof window.loadPagefind === "function" 检查。

故障模式、边界情况与并发

依据已核实的源码证据:

场景代码行为涉及位置
开发模式(无索引产物)打印「Using mock data」日志,使用 mock 数据,不调用引擎Search.svelte L156–158
生产模式但引擎缺失结果置空 [],console.error("Pagefind is not available in production environment.")Search.svelte L133–134
window.loadPagefind 未注入调用前 typeof 判空,静默跳过,不抛 TypeErrorSearch.svelte L45、L58
window.pagefind 半就绪判定 pagefindLoaded 时同时校验 !!window.pagefind 与 search 是否为函数,避免拿到不完整对象Search.svelte L148–151
异步竞态search() 为 await 调用;组件内 pagefindLoaded 为普通布尔标志(非响应式 $state),初始化一次后不再变更Search.svelte L14、L147–152
索引自指涉pagefind.yml 排除 .search-panel / #search-panel,防止面板文本进入索引pagefind.yml L5–L6
公式噪声排除 span.katex / span.katex-displaypagefind.yml L2–L3
锚点图标噪声输出 data-pagefind-ignore: true,配合排除规则astro.config.mjs L305

并发/一致性要点:索引与页面 HTML 在同一条 build 链中先后生成(astro build → pagefind --site dist),因此不存在跨构建的索引漂移窗口;但跳过 pagefind 步骤的增量部署会导致索引落后于页面内容,属于部署层约束而非代码层防护。

性能与运维要点

  • 首屏零成本:引擎 JS 与分片不在初始页面加载,首次搜索面板展开才经 window.loadPagefind() 拉取。
  • 首查询延迟:懒加载把首次搜索的代价推迟到交互时,属显式取舍(换取更快的页面加载)。
  • 索引体积随内容增长:data-pagefind-body 边界 + exclude_selectors 共同压低索引规模,是主要的体积控制手段。
  • 部署约束:产物必须携带 dist/pagefind/;内容更新后必须重跑完整 build 链(见「构建期索引流水线」)。
  • 多平台二进制:pnpm-lock.yaml 中存在 @pagefind/darwin-arm64@1.5.2、@pagefind/darwin-x64@1.5.2、@pagefind/freebsd-x64@1.5.2 等平台包,说明 CI/本地产物构建需保证对应平台的 Pagefind 二进制可用。

扩展点

  • 新增排除项:优先在模板上输出 data-pagefind-ignore(复用 pagefind.yml 已有规则);仅当需要按选择器批量排除时才改 pagefind.yml。
  • 收录新内容类型:在对应组件容器上加 data-pagefind-body;若需要标题元数据/权重,参考 [...permalink].astro 的 data-pagefind-meta="title" 与 data-pagefind-weight="10" 组合。
  • 消费搜索结果:遵循 Search.svelte 的模式——读取 global.d.ts 声明的 window.pagefind.search(),并保留「PROD 判定 + pagefindLoaded 探测」的双重守卫;新代码不应直接 import Pagefind 模块,以维持懒加载边界。
  • 面板行为接入:search-panel 已在 panel-manager.ts 的面板类型联合中("search-panel"),与导航菜单等浮层共享开合、互斥逻辑;修改面板行为应在面板管理机制内进行。

相关链接

Sources

(1 files)