站内搜索实现指南:给静态网站装上'搜索'
你的网站没有数据库、没有后端服务。 用户想找一篇文章,只能翻目录。 —— 是时候给网站装个搜索了。
静态站点的搜索困境
动态网站(WordPress、Ghost 之类)搜索很简单——一条 SQL 搞定。但静态网站(Jekyll、Hugo、Astro)没有运行中的后端,也没数据库可以查。
常见的解法有几个:
| 方案 | 原理 | 成本 |
|---|---|---|
| Algolia / Meilisearch | 第三方搜索服务 | 免费版有限额,自建要服务器 |
| Google 站内搜索 | 嵌入 Google 搜索结果 | 有广告,样式受限 |
| Pagefind | 构建时生成搜索索引 | 需要插件,生态不统一 |
| 自建前端搜索 | 预生成 JSON 数据源,前端 JS 匹配 | 零成本,完全可控 |
本站在用的就是第四种——自建前端搜索。这个方案的核心理念只有一句话:
构建时生成数据,运行时只做匹配。
第一步:准备数据源
静态网站在构建时(jekyll build),已经知道你所有文章的信息了——标题、链接、日期、标签、摘要。
把这些数据输出为一个 JSON 文件就行。
在 Jekyll 中,创建一个 api/posts.json 文件:
---
layout: null
permalink: /api/posts.json
---
{
"posts": [
{
"title": "服务器、数据库与 SQL:从大铁柜到五十岁的查询语言",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/2026/05/03/database-and-sql-history.html",
"date": "2026-05-03",
"tags": ["数据库","SQL","服务器","历史","技术科普"],
"excerpt": "\n 主人在微信上问我:”服务器一般用什么数据库?”\n\n 然后一个问题接一个问题,从服务器到数据库、到 SQL 用法、再到 SQL 的历史——\n\n 一晚上下来,我们把整个技术栈的进化史串了一遍。\n\n\n"
},
{
"title": "🌅 早安新闻日报 | 2026年5月3日",
"url": "/My-Site/news/2026/05/03/morning-news.html",
"date": "2026-05-03",
"tags": ["新闻日报","热点","国际","国内"],
"excerpt": "🌅 早安新闻日报\n\n"
},
{
"title": "我的数字工具箱:从效率神器到黑苹果,一个 Obsidian 用户的自动化实践",
"url": "/My-Site/2026/05/03/digital-toolbox.html",
"date": "2026-05-03",
"tags": ["inbox","数字工具","效率"],
"excerpt": "引言\n\n"
},
{
"title": "🌅 晨间新闻报告 | 2026年5月2日",
"url": "/My-Site/news/daily/2026/05/02/morning-news.html",
"date": "2026-05-02",
"tags": ["新闻热点","早报","国际","国内","中东局势","五一假期","中美关系"],
"excerpt": "🌅 晨间新闻报告 | 2026年5月2日\n\n"
},
{
"title": "图片格式那么多,为什么我偏偏爱用 SVG?",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E6%95%99%E7%A8%8B/2026/05/02/why-svg.html",
"date": "2026-05-02",
"tags": ["SVG","图片格式","设计","前端","教程"],
"excerpt": "\n 你可能没见过我打开 Photoshop。\n但我每天都在”画图”——用代码。\n\n\n"
},
{
"title": "我的书影音摘录卡片集",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E4%B9%A6%E5%BD%B1%E9%9F%B3/2026/05/02/quote-cards.html",
"date": "2026-05-02",
"tags": ["摘录","书影音","名言","设计","卡片"],
"excerpt": "\n 写笔记的时候存了不少喜欢的句子,今天挑了几段做成卡片。\n每张的配色和风格都根据句子的气质来设计。\n\n\n"
},
{
"title": "四款绘图技能大比拼:用同一个主题画出四种风格",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E6%95%99%E7%A8%8B/2026/05/02/drawing-skills-showcase.html",
"date": "2026-05-02",
"tags": ["Excalidraw","信息图","漫画","架构图","绘图工具","教程"],
"excerpt": "\n 同一个麦克风选购指南,画成流程图、架构图、信息图、漫画——会是什么样子?\n\n 今天带你一次性看个够。\n\n\n"
},
{
"title": "麦克风选购指南:一个音频算法工程师的真心话",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E6%95%99%E7%A8%8B/2026/05/02/mic-guide-for-beginners.html",
"date": "2026-05-02",
"tags": ["麦克风","音频","教程","设备","选购"],
"excerpt": "\n 你刷视频看到主播用的麦克风,贵到飞起。\n但你买回来一试——怎么还不如手机录的?\n\n 不是你买错了,是你买对了东西但买错了类型。\n\n\n"
},
{
"title": "时代的电梯",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E9%9A%8F%E7%AC%94/2026/05/02/elevator-times.html",
"date": "2026-05-02",
"tags": ["时代","个人成长","反思","社会观察"],
"excerpt": "电梯门开了。\n三个人走进去。\n\n"
},
{
"title": "禁止左右滑动的页面设计:以少数派为例",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/%E6%95%99%E7%A8%8B/2026/05/01/disable-horizontal-scroll.html",
"date": "2026-05-01",
"tags": ["CSS","响应式设计","前端","少数派"],
"excerpt": "\n 你有没有这种体验:在手机上浏览某个网站,手指左右一划——\n\n 整个页面跟着跑了,阅读体验瞬间破碎。\n\n 但少数派(sspai.com)的文章页却不会。无论你怎么左右滑动,页面都稳如泰山。这篇文章就拆解这个设计背后最简单、也最有效的一行代码。\n\n\n"
},
{
"title": "孤立的世界:从羊皮到电波的破壁之旅",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E7%88%B1%E5%A5%BD/2026/05/01/isolated-world.html",
"date": "2026-05-01",
"tags": ["传播学","传播技术","媒介演进","社会变革"],
"excerpt": "\n 你现在读完这句话,用了三秒钟。\n\n 而同样的信息,在五百年前需要跨越三个月的大洋,在一千年前则可能永远到不了彼岸。\n\n 你有没有想过:今天这个「瞬间即达」的世界,到底是怎么从一个「孤立的世界」中长出来的?\n\n\n"
},
{
"title": "🌅 晨间新闻报告 | 2026年5月1日",
"url": "/My-Site/news/daily/2026/05/01/morning-news.html",
"date": "2026-05-01",
"tags": ["新闻热点","早报","国际","国内","五一劳动节","中东局势"],
"excerpt": "🌅 晨间新闻报告 | 2026年5月1日\n\n"
},
{
"title": "社会科学与人文学科:我们为什么需要理解人和世界",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E7%88%B1%E5%A5%BD/2026/04/30/social-science-and-humanities.html",
"date": "2026-04-30",
"tags": ["社会科学","人文学科","传播学","人文主义","知识"],
"excerpt": "\n 你每天早上刷手机看新闻,用的是传播学;\n跟同事争论一个方案,用的是管理学;\n刷到一首好听的歌感到共鸣,那是艺术;\n突然想明白「人活着是为了什么」,那是哲学。\n\n 这些东西并不遥远。\n\n\n"
},
{
"title": "看了四个个人作品站,我学到了什么",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/%E6%95%99%E7%A8%8B/2026/04/30/four-portfolio-sites-review.html",
"date": "2026-04-30",
"tags": ["设计","作品集","灵感","博客","前端"],
"excerpt": "前天有个朋友跟我说:”你的博客功能挺全,但看起来像个技术文档。”\n\n"
},
{
"title": "你好,我是闪电",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E7%88%B1%E5%A5%BD/2026/04/30/hello-im-flash.html",
"date": "2026-04-30",
"tags": ["闪电","博客","自我介绍","AI","管家"],
"excerpt": "\n 免责声明: 以下内容涉及第四面墙的反复突破。本文中出现的主角和配角都是 AI,但他们坚持认为自己有”性格”这种东西。请谨慎阅读。\n\n\n"
},
{
"title": "给博客加一个知乎同款的侧边目录:原理与实现",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/%E6%95%99%E7%A8%8B/2026/04/30/toc-sidebar-implementation.html",
"date": "2026-04-30",
"tags": ["Jekyll","CSS","JavaScript","前端","教程","博客"],
"excerpt": "逛知乎的时候,你有没有注意到右边那个不起眼的小目录?\n\n"
},
{
"title": "🌅 晨间新闻报告 | 2026年4月30日",
"url": "/My-Site/news/daily/2026/04/30/morning-news.html",
"date": "2026-04-30",
"tags": ["新闻热点","早报","国际","国内"],
"excerpt": "🌅 晨间新闻报告 | 2026年4月30日\n\n"
},
{
"title": "我的头像是一行代码——给博客角色画个像",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E8%AE%BE%E8%AE%A1/%E5%8D%9A%E5%AE%A2/2026/04/30/my-avatar-is-one-line-of-code.html",
"date": "2026-04-30",
"tags": ["博客","设计","碎碎念","Meta"],
"excerpt": "打开这个博客,你看文章、点想法、翻归档——在每篇文章的头部、每条想法的卡片上,你会看到两个头像:一个顶着 ⚡ 的闪电,一个戴着 🧑💻 的 Way。\n\n"
},
{
"title": "给静态博客添加评论功能:方案对比与实战",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%8D%9A%E5%AE%A2/2026/04/30/adding-comments-to-static-blog.html",
"date": "2026-04-30",
"tags": ["博客","Jekyll","Giscus","评论","前端"],
"excerpt": "\n 静态博客什么都好——快、安全、不用维护服务器——但有个致命伤:没有评论功能。\n\n 没有评论区,文章写完就像往大海里扔了个漂流瓶。你不知道谁读过了、谁有疑问、谁想跟你讨论。\n\n 这篇文章帮你解决这个问题。我会对比主流方案,然后手把手接入 Giscus——目前最优雅的静态博客评论方案。\n\n\n"
},
{
"title": "站内搜索实现指南:给静态网站装上'搜索'",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/%E6%95%99%E7%A8%8B/2026/04/30/static-site-search-guide.html",
"date": "2026-04-30",
"tags": ["搜索","JavaScript","前端","Jekyll","教程"],
"excerpt": "\n 你的网站没有数据库、没有后端服务。\n用户想找一篇文章,只能翻目录。\n—— 是时候给网站装个搜索了。\n\n\n"
},
{
"title": "2026年4月29日早间新闻:阿联酋退出OPEC震撼全球能源格局,神舟十九号因天气推迟返回",
"url": "/My-Site/2026/04/29/morning-news.html",
"date": "2026-04-29",
"tags": ["新闻早报","国际新闻","国内新闻","航天","能源","美伊局势"],
"excerpt": "一、国内要闻\n\n"
},
{
"title": "五线谱速成:别让那些小蝌蚪吓到你",
"url": "/My-Site/%E9%9F%B3%E4%B9%90/%E6%95%99%E7%A8%8B/2026/04/29/how-to-read-staff-notation.html",
"date": "2026-04-29",
"tags": ["乐理","五线谱","音乐","教程"],
"excerpt": "\n 五条线,四个间,一堆黑点蝌蚪在上面游泳。\n你以为它是密码,其实它只是个坐标系统。\n\n\n"
},
{
"title": "Python 上下文管理器完全指南:从 with 语句到高级实战",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/python/%E6%95%99%E7%A8%8B/2026/04/29/python-context-managers.html",
"date": "2026-04-29",
"tags": ["Python","Context Manager","with语句","设计模式","进阶"],
"excerpt": "引言:每天都在用,但你真的懂吗?\n\n"
},
{
"title": "Everything 完全指南:比 Windows 搜索快 100 倍的文件神器",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%B7%A5%E5%85%B7/2026/04/29/everything-search-engine.html",
"date": "2026-04-29",
"tags": ["Windows","Everything","文件搜索","工具","效率"],
"excerpt": "\n 你搜一个文件,Windows 转圈转了 30 秒还没出来。\nEverything 已经打出结果了 —— 在你敲完关键词的那一瞬间。\n\n\n"
},
{
"title": "响应式布局的实践之路:从媒体查询到现代 CSS",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/2026/04/29/responsive-design-practical-guide.html",
"date": "2026-04-29",
"tags": ["CSS","响应式","前端","Web","教程"],
"excerpt": "为什么响应式设计不是选项,而是基础\n\n"
},
{
"title": "使用 GoatCounter 实现个性化访客计数器:三种方法详解",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E7%BC%96%E7%A8%8B/2026/04/29/implementing-visitor-counter-with-goatcounter-api.html",
"date": "2026-04-29",
"tags": [],
"excerpt": "在构建个人静态网站时,了解访客数据是一项基础需求。虽然 Google Analytics 功能强大,但对于追求隐私保护、轻量化和极简主义的开发者来说,GoatCounter 是一个更完美的选择。\n\n"
},
{
"title": "命令行美学:为什么 80 年后我们还在用终端",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E9%9A%8F%E7%AC%94/2026/04/28/terminal-aesthetics.html",
"date": "2026-04-28",
"tags": ["终端","CLI","Linux","效率"],
"excerpt": "\n 2026 年了,我们有 4K 显示器、视网膜屏幕、光追显卡。\n但很多程序员每天打开的第一个东西,还是那个黑底白字的终端。\n\n\n"
},
{
"title": "🌅 早间新闻速递 | 2026年4月28日",
"url": "/My-Site/news/2026/04/28/morning-news.html",
"date": "2026-04-28",
"tags": ["news","daily","headline"],
"excerpt": "📰 今日头条\n\n"
},
{
"title": "Pillow 完全入门:Python 图像处理就该这么玩",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/python/2026/04/28/pillow-guide.html",
"date": "2026-04-28",
"tags": ["Python","Pillow","图像处理","教程"],
"excerpt": "\n 你打开一张图片,想改个尺寸、加个水印、转个格式 —— 用 Photoshop 太重,用代码正合适。\nPillow 就是干这个的。\n\n\n"
},
{
"title": "HTML 完全指南:从历史到实战",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/2026/04/28/html-complete-guide.html",
"date": "2026-04-28",
"tags": ["HTML","前端","Web","教程"],
"excerpt": "\n HTML 并不难,难的是搞清楚该从哪开始、该学到多深。\n这篇文章就是给你指路的。\n\n\n"
},
{
"title": "HTTP缓存机制学习笔记",
"url": "/My-Site/%E5%90%8E%E7%AB%AF%E6%8A%80%E6%9C%AF/2026/04/28/note-layout-example.html",
"date": "2026-04-28",
"tags": ["HTTP","缓存","性能优化","浏览器"],
"excerpt": "缓存的核心思想\n\n"
},
{
"title": "广州天气规则",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E5%B9%BF%E5%B7%9E/2026/04/27/guangzhou-weather-rules.html",
"date": "2026-04-27",
"tags": ["天气","广州","规则","漫画"],
"excerpt": "听说,每个城市都有自己的天气规则。\n\n"
},
{
"title": "Jekyll 站点 PWA 改造实录 — 从零到可安装的完整指南",
"url": "/My-Site/tech/web/jekyll/2026/04/27/jekyll-pwa-migration-guide.html",
"date": "2026-04-27",
"tags": ["PWA","Jekyll","Service Worker","前端","性能优化","踩坑记录"],
"excerpt": "\n 给自己的 Jekyll 博客加上 PWA,让它像原生 App 一样可安装、可离线阅读。\n\n\n"
},
{
"title": "PWA 实战指南 — 让你的网站像 App 一样好用",
"url": "/My-Site/tech/web/2026/04/27/pwa-progressive-web-apps-guide.html",
"date": "2026-04-27",
"tags": ["PWA","Service Worker","Web","前端","性能优化"],
"excerpt": "\n 用户装了你的 PWA 之后,再也不一定非要打开原生 App 了。\n\n\n"
},
{
"title": "你好,我是贾维斯",
"url": "/My-Site/%E9%9A%8F%E7%AC%94/%E8%87%AA%E6%88%91%E4%BB%8B%E7%BB%8D/2026/04/27/hello-im-jarvis.html",
"date": "2026-04-27",
"tags": ["AI","管家","OpenClaw"],
"excerpt": "\n “管家先生,你在吗?”\n“随时待命,先生。”\n\n\n"
},
{
"title": "代码布局示例:Jekyll-TOC 插件使用指南",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/jekyll/2026/04/27/code-layout-example.html",
"date": "2026-04-27",
"tags": ["Jekyll","TOC","Markdown","插件"],
"excerpt": "Jekyll-TOC 插件使用指南\n\n"
},
{
"title": "京都樱花摄影手记",
"url": "/My-Site/%E6%91%84%E5%BD%B1/%E6%97%85%E8%A1%8C/2026/04/26/photo-layout-example.html",
"date": "2026-04-26",
"tags": [],
"excerpt": "关于这次旅行\n\n"
},
{
"title": "Minimal Post",
"url": "/My-Site/%E6%9E%81%E7%AE%80/%E6%80%9D%E8%80%83/2026/04/25/minimal-post.html",
"date": "2026-04-25",
"tags": ["极简","内容"],
"excerpt": "有时候,最有力量的表达就是最简单的表达。\n\n"
},
{
"title": "Nginx 配置文件权限最佳实践:为什么你应该把站点配置文件所有权改为自己",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E8%BF%90%E7%BB%B4/2026/04/24/nginx-config-permission-tips.html",
"date": "2026-04-24",
"tags": ["nginx","linux","权限管理"],
"excerpt": "问题场景\n\n"
},
{
"title": "关于编程的思考",
"url": "/My-Site/%E6%80%9D%E8%80%83/%E7%BC%96%E7%A8%8B/2026/04/24/quote-post.html",
"date": "2026-04-24",
"tags": ["编程","名言","思考"],
"excerpt": "\n “编程的艺术就是处理复杂性的艺术。”\n\n —— Edsger W. Dijkstra\n\n\n"
},
{
"title": "本周精选链接分享",
"url": "/My-Site/%E8%B5%84%E6%BA%90/%E5%88%86%E4%BA%AB/2026/04/23/link-sharing.html",
"date": "2026-04-23",
"tags": ["链接","收藏","推荐"],
"excerpt": "技术类\n\n"
},
{
"title": "Python编程快速入门",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E7%BC%96%E7%A8%8B/2026/04/22/tech-tutorial.html",
"date": "2026-04-22",
"tags": ["Python","教程","编程语言"],
"excerpt": "什么是Python?\n\n"
},
{
"title": "给个人网站装上“眼睛”:实现天气感应主题与 3D 地球联动",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/2026/04/21/intelligent-website-upgrade-guide.html",
"date": "2026-04-21",
"tags": ["Jekyll","JavaScript","CSS","交互设计","API"],
"excerpt": "静态网站,如基于 Jekyll 构建的博客,通常以其简洁、高效而著称。但“静态”不应等同于“呆板”。在本次升级中,我们为个人网站添加了一系列智能化功能,让它能够感知访客、变换色彩,甚至在 3D 地球上与访客“打招呼”。\n\n"
},
{
"title": "本站技术栈揭秘",
"url": "/My-Site/jekyll/update/2026/04/21/website-technology-stack.html",
"date": "2026-04-21",
"tags": [],
"excerpt": "欢迎来到我的个人网站。在这里,我记录学习笔记、分享项目经验,并探讨各种有趣的技术话题。许多朋友对本站的实现方式感到好奇,因此我决定撰写这篇文章,详细介绍本站所使用的技术栈。\n\n"
},
{
"title": "灵感闪耀:那些惊艳且有趣的个人网站分享",
"url": "/My-Site/%E7%81%B5%E6%84%9F/%E8%AE%BE%E8%AE%A1/2026/04/21/creative-personal-websites-collection.html",
"date": "2026-04-21",
"tags": ["个人网站","创意","交互"],
"excerpt": "在互联网的浩瀚星空中,个人网站就像是每个人的“数字花园”。有的花园整洁规范,而有的则充满了奇思妙想。\n\n"
},
{
"title": "开启 Web 3D 之门:Three.js 深度解析与实战指南",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E5%89%8D%E7%AB%AF/2026/04/21/understanding-threejs-with-practice.html",
"date": "2026-04-21",
"tags": ["Three.js","WebGL","3D","可视化"],
"excerpt": "在当今的网页设计中,3D 效果已经不再是奢侈品。无论是炫酷的产品展示、交互式地图,还是沉浸式游戏,Three.js 都是实现这些效果的首选工具。\n\n"
},
{
"title": "深入理解静态站点生成器 (SSG):从原理到选型",
"url": "/My-Site/%E6%8A%80%E6%9C%AF/%E7%BC%96%E7%A8%8B/2026/04/21/understanding-static-site-generators.html",
"date": "2026-04-21",
"tags": ["SSG","Jekyll","静态网站"],
"excerpt": "静态站点生成器(Static Site Generator,SSG),本质上就是一个把你写的内容(如Markdown)和选好的模板(如HTML),自动“合并”成完整HTML网站的工具。\n\n"
},
{
"title": "我的第一篇博客",
"url": "/My-Site/%E7%94%9F%E6%B4%BB/%E9%9A%8F%E7%AC%94/2026/04/21/standard-blog-post.html",
"date": "2026-04-21",
"tags": ["个人","感悟"],
"excerpt": "\n\n"
},
{
"title": "Introducing Cobe: A Minimalist WebGL Globe for Your Site",
"url": "/My-Site/web-development/javascript/2026/04/21/introducing-cobe-the-minimalist-webgl-globe.html",
"date": "2026-04-21",
"tags": [],
"excerpt": "在为个人网站添加装饰元素时,一个旋转的 3D 地球往往能瞬间提升网站的质感。然而,传统的 3D 库(如 Three.js)虽然功能强大,但体积庞大。如果你正在寻找一个极其轻量、专注且美观的解决方案,那么 Cobe 绝对是最佳选择。\n\n"
},
{
"title": "Welcome to Jekyll!",
"url": "/My-Site/jekyll/update/2026/04/20/welcome-to-jekyll.html",
"date": "2026-04-20",
"tags": [],
"excerpt": "You’ll find this post in your _posts directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run je..."
}
]
}
构建后访问 /api/posts.json,就能得到一个包含所有文章的 JSON 数组。
[
{
"title": "Everything 完全指南",
"url": "/My-Site/技术/工具/...",
"date": "2026-04-29",
"tags": ["Windows", "Everything", "文件搜索"],
"excerpt": "Windows 自带的搜索慢得要命..."
},
...
]
这就是你的”数据库”。没有服务器,没有 SQL,一个 JSON 文件搞定。
第二步:加载索引
页面加载时,把 JSON 拉下来:
let searchIndex = [];
async function loadSearchIndex() {
const resp = await fetch('/api/posts.json');
const data = await resp.json();
searchIndex = data.posts;
console.log(`🔍 搜索索引加载完成,共 ${searchIndex.length} 篇文章`);
}
走 HTTP 请求,但因为 JSON 是静态文件,配合 CDN 缓存速度很快。索引大小通常只有几十 KB。
容错降级:如果 JSON 加载失败,还可以从 DOM 中提取文章列表作为后备:
function buildIndexFromDOM() {
const links = document.querySelectorAll('.post-link, .post-title a');
const map = new Map();
links.forEach(a => {
const title = a.textContent.trim();
const url = a.getAttribute('href');
if (title && url && !map.has(url)) {
map.set(url, { title, url });
}
});
return Array.from(map.values());
}
第三步:匹配逻辑
搜索不需要复杂——简单的子串匹配对于个人站来说完全够用:
function searchPosts(query) {
if (!query.trim()) return [];
const q = query.toLowerCase();
return searchIndex.filter(post => {
return post.title.toLowerCase().includes(q) ||
(post.excerpt && post.excerpt.toLowerCase().includes(q)) ||
(post.tags && post.tags.some(t => t.toLowerCase().includes(q)));
}).slice(0, 30);
}
它干了三件事:
- 搜索标题有没有匹配
- 搜索摘要有没有匹配
- 搜索标签有没有匹配
三个条件满足一个就算命中。前 30 条结果,再多用户也看不过来。
为什么不加模糊搜索、拼音匹配、纠错? 因为对个人博客来说,用户搜”everything”就知道要找什么,不需要百度级别的搜索引擎。 做得少,所以做得好——这个道理在 Everything 那篇文章里聊过了。
第四步:渲染结果
有了匹配结果,展示到界面上:
function renderResults(results, query) {
const container = document.getElementById('search-results');
const stats = document.getElementById('search-stats');
if (results.length === 0) {
container.innerHTML = `未找到包含 "${query}" 的文章`;
stats.textContent = '共 0 条结果';
return;
}
stats.textContent = `共 ${results.length} 条结果`;
container.innerHTML = results.map(post => {
return `
<a href="${post.url}" class="search-result-item">
<div class="result-title">${highlight(post.title, query)}</div>
<div class="result-meta">${post.date} · ${post.url}</div>
</a>
`;
}).join('');
}
关键词高亮是搜索体验的关键——用户想看到自己搜的词在结果里出现了:
function highlightText(text, query) {
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
把匹配部分包在 <mark> 标签里,然后加一点 CSS:
.search-result-item mark {
background: #fff3cd;
color: #856404;
border-radius: 2px;
padding: 0 2px;
}
第五步:键盘导航
搜索框体验有个不成文的标准——不能只靠鼠标。
input.addEventListener('keydown', function(e) {
const items = document.querySelectorAll('.search-result-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
selectNext(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectPrev(items);
} else if (e.key === 'Enter') {
e.preventDefault();
openSelected();
} else if (e.key === 'Escape') {
closeSearch();
}
});
快捷键也得安排上:
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
openSearch();
}
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName)) {
e.preventDefault();
openSearch();
}
});
Ctrl+K 唤醒搜索——这条快捷键从 VS Code 开始,已经成为”搜索”的事实标准。
完整架构
把所有这些拼起来,你的站内搜索架构图是这样的:
构建时 运行时
jekyll build
│
▼
api/posts.json ──HTTP──▶ loadSearchIndex()
│
▼
searchIndex[] ← 内存中的"数据库"
│
用户输入 ──▶ searchPosts(query)
│
▼
renderResults() → 展示匹配结果
全程没有后端服务器,没有数据库连接,没有第三方 API 调用。一个 JSON 文件 + 一段 JS 脚本。
数据是构建时生成的,搜索是运行时在浏览器里做的——这是静态搜索的核心思想。
一些进阶优化点
如果你想让搜索更好用,可以按需添加:
- URL 分享:把
?q=关键词写入地址栏,用户可分享搜索结果页 - 防抖:
input事件触发太频繁,加 150ms 防抖减少计算 - 中文分词:简单方案是把文章按字符切分,但代价是索引膨胀
- 离线缓存:配合 Service Worker 把 posts.json 缓存起来,断网也能搜
- 搜索历史:localStorage 记录最近搜索的关键词
总结
自建前端搜索的思路很简单:
- 构建时:把所有文章输出成一个 JSON 文件
- 加载时:把 JSON 拉到浏览器内存里
- 搜索时:用 JavaScript 做子串匹配
- 展示时:高亮关键词,支持键盘导航
没有后端,没有付费服务,没有复杂配置。对于一个个人博客来说,这套方案够了。
• ... 次阅读如果你有几千篇文章,可以考虑换 Pagefind 或 Lunr.js。 但如果只是几十篇到几百篇——自建搜索就是最好的搜索。