给HUGO添加文章搜索功能
之前用 Hugo-theme-stack 主题,自带了搜索功能,我以为这种功能是 Hugo 默认就有,后来才知道,是外挂。
在更换 Bear cub 主题后,之前也没觉得这功能有多重要,但实际使用中发现,有时候记不起来一些事情,还得借助搜索才行。于是研究了下怎么外挂搜索。其实也很简单,只需四步。
在 content 目录下新建 search.md 页面
如果不需要放置在导航栏,内容填入默认信息即可,需要进导航栏的,效仿其他 md 文件设置。
---
title: "搜索"
date: 2025-11-20T00:00:00+08:00
type: "search"
layout: "search"
---
在此搜索本博客文章。在 layouts 目录下新建 /search/single.html 模板
模板内容可以参照其他模板修改,大致内容如下。
{{ define "main" }}
<content>
<h1>{{ .Title }}</h1>
<div class="search-box">
<input
id="search-input"
class="search-input"
type="text"
placeholder="输入关键词搜索…"
autocomplete="off"
/>
</div>
<ul id="results" class="search-results">
<li style="color:#666">请输入关键词搜索</li>
</ul>
</content>
<script>
// ========== 工具函数 ==========
function escapeHtml(str) {
if (!str) return "";
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
// ========== 精确搜索 ==========
function exactMatch(haystack, needle) {
if (!haystack || !needle) return false;
return haystack.toLowerCase().includes(needle.toLowerCase());
}
// ========== 渲染结果 ==========
function renderResults(list) {
const resultsEl = document.getElementById("results");
if (!list || list.length === 0) {
resultsEl.innerHTML = '<li style="color:#666">未找到结果。</li>';
return;
}
const itemsHtml = list.map(item => {
const title = escapeHtml(item.title || "(无标题)");
// 外链或本地链接
const url = escapeHtml(item.link || item.url || "#");
const isExternal = !!item.link;
const linkAttrs = isExternal
? ' target="_blank" rel="noopener noreferrer"'
: "";
// 摘要:取 summary 或 content 开头一段
const summaryRaw =
item.summary ||
(item.content ? item.content.slice(0, 200) + "…" : "");
const summary = escapeHtml(summaryRaw);
return `
<li>
<div class="sr-title-col">
<a class="sr-title" href="${url}"${linkAttrs}>${title}</a>
</div>
<div class="sr-snippet-col">
<div class="sr-snippet">${summary}</div>
</div>
</li>
`;
}).join("");
resultsEl.innerHTML = itemsHtml;
}
// ========== 加载 index.json 数据 ==========
async function loadIndex() {
try {
const res = await fetch("/index.json");
return await res.json();
} catch (err) {
console.error("加载 index.json 失败:", err);
return [];
}
}
// ========== 主逻辑 ==========
(async function () {
const data = await loadIndex();
const input = document.getElementById("search-input");
input.addEventListener("input", () => {
const q = input.value.trim();
if (!q) {
renderResults([]);
return;
}
// 精确搜索:title / content / tags 均可匹配
const result = data.filter(item =>
exactMatch(item.title, q) ||
exactMatch(item.content, q) ||
(item.tags || []).some(t => exactMatch(t, q))
);
renderResults(result);
});
})();
</script>
{{ end }}在 hugo 配置文件中添加 json 输出
比如我的hugo配置文件是 toml 格式,内容如下。
[outputs]
home = ["HTML", "RSS", "JSON"] # 添加 JSON 输出
[outputFormats.JSON]
baseName = "index"
mediaType = "application/json"在 layouts 中新建 index.json 模板
主要就是将整个博客文档内容输出到 index.json 文件,搜索时直接在该文件内搜索,可依照自己需求进行修改,比如只搜索标题、摘要、标签等信息。
[
{{- $pages := where .Site.RegularPages "Type" "not in" (slice "page" "something-you-want-to-exclude") -}}
{{- $first := true -}}
{{- range $i, $p := $pages -}}
{{- if not $first }},{{ end -}}
{
"title": {{ $p.Title | jsonify }},
"url": {{ $p.RelPermalink | absURL | jsonify }},
"date": {{ $p.Date.Format "2006-01-02" | jsonify }},
"summary": {{ with $p.Params.description }}{{ . | jsonify }}{{ else }}{{ $p.Summary | plainify | jsonify }}{{ end }},
"content": {{ $p.Plain | chomp | jsonify }},
"tags": {{ $p.Params.tags | jsonify }},
"categories": {{ $p.Params.categories | jsonify }}
}
{{- $first = false -}}
{{- end -}}
]其他 CSS 配置
如果需要自定义搜索页面的 CSS,可以直接在主题 CSS 或自定义 custom.css 文件中添加,或者写在前边 /search/single.html 模板中也可以。
/* ====== 搜索框区域布局 ====== */
.search-box {
max-width: 720px;
margin: 24px 0 32px;
}
.search-input {
width: 100%;
padding: 10px 14px;
font-size: 16px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--entry);
color: var(--primary);
outline: none;
transition: border-color .15s ease, box-shadow .15s ease;
}
.search-box {
max-width: 720px;
margin: 24px 0 32px;
}
.search-input {
width: 100%;
padding: 10px 14px;
font-size: 16px;
border: 1px solid rgba(150, 150, 150, 0.35);
border-radius: 8px;
background: var(--entry);
color: var(--primary);
outline: none;
transition: border-color .15s ease, box-shadow .15s ease;
}
.search-input:focus {
border-color: var(--text-highlight);
box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.20);
}
.search-results {
list-style: none;
padding: 0;
margin: 0;
}
.search-results li {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid rgba(0,0,0,0.04);
}
.search-results .sr-title-col {
flex: 0 0 40%;
min-width: 180px;
max-width: 420px;
}
.search-results .sr-title {
font-size: 1.02rem;
line-height: 1.3;
text-decoration: none;
color: var(--primary);
}
.search-results .sr-title[target="_blank"]::after {
content: " ↪";
font-weight: 400;
}
.search-results .sr-snippet-col {
flex: 1 1 60%;
}
.search-results .sr-snippet {
color: var(--secondary);
font-size: 0.95rem;
line-height: 1.5;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
@media (max-width: 500px) {
.search-results li {
flex-direction: column;
gap: 8px;
}
.search-results .sr-title-col {
max-width: none;
}
}广电手机卡最近每天都会遇到QOS限速
每天早上测速时基本都是满速500Mbps,但一到下午,就直接限速到1Mbps。不知道是不是因为我长期挂广电卡当wifi热点的缘故。但一查流量情况,这个月才用了15GB,套餐内还剩200GB没用,也是醉了。
真是巧得很
刚才想在博客上搜个东西,因为新主题不带搜索功能,所以点开旧版查看。没想到意外看到字数统计 1088888。
这么吉祥的数字,必须纪念一下。
稀奇古怪的墙
我这个 vmiss 香港小鸡,大概十几天前突然三网断联,只能套 Cloudflare 使用,但刚才随手一刷,居然发现又回来了。断的莫名其妙,回来的也稀奇古怪。
CS游戏时长破3000小时
不知不觉,CS游戏时长达到3000小时。这个游戏是我2012年9月份内测时候购买的,主要的游戏时长都是刚工作那几年。生小孩后,连续好几年没玩,去年开始,闲来无事就做做每周任务,也拿到了去年和今年的纪念币。
昨天偶尔发现,卡片下方有一个评分展示,但需要完成十场比赛胜利才行。于是花了10来个小时,取得10胜4负战绩,终于是把这任务给刷完。过程中,很心酸,反应慢、枪法差,最终只定级6600分。
Git 下载速度死活只有20K
今晚遇到个稀奇古怪的事,以往我从Github拉个仓库到本地,都是50Mbps起步,但也不知道是不是因为最近将博客写作路径调整为 submodule 的原因,当随主仓库克隆到本地时,死活只有200Kbps速度,也就是20多KB/s。一开始我也没当回事,就让它在后台下载,然后打开Steam做本周CS2的任务,结果玩了四局游戏两三个小时过去后,居然一个250MB的仓库还没下载完,只到70%。
然后撤销重新试了多次,发现主仓库下载时都有100Mbps,而一旦拉取submodule,立马降速到200Kbps。网上一查,说可能是submodule协议问题。的确,我这个submodule为了配合一些workflows功能,在设置中使用SSH连接,也就是 git@github.com:user/repo.git 这种链接模式。然后git这边,可能会使用 git:// 协议,通过9418端口进行通信,而这个端口很可能被运营商限速了。
解决办法也比较简单,为git中这个端口添加一个全局代理或者将ssh协议改写为https即可。
# 创建全局协议转换规则
git config --global url."https://github.com/".insteadOf "git://github.com/"
git config --global url."https://github.com/".insteadOf "git@github.com:"
# 强制转换所有子模块
git submodule foreach 'git config url.https://github.com/.insteadOf git@github.com:'微信公众号上谜之私信
去年写了篇关于 小米智能门锁故障修复文章,发在前些天搬到微信公众号上,虽然只有300多阅读,但有读者反馈按我说的方法修复成功,还是很欣慰。不过今天收到稀奇古怪的私信,请我上门去维修,这就有点迷了。仔细看了下对方账号基本信息,是江苏南通的,从头像来看,可能是一位年长的女性。我在公众号后台数据发现她应该是通过微信搜一搜功能找到了我这篇文章。
按道理来说,会用微信搜一搜功能,应该也有点基本常识吧,但却出现这种情况,也是个迷。
给博客更换主题 Hugo Bear
我很早之前就一直在用 Hugo theme Stack主题写东西。这款主题挺好的,但我觉得它更适合图片比较多的博客。而我现在的网站上面已经积攒了1700多张照片,在Stack主题的处理下,生成网站时会生成7000到8000个文件!这就导致整个网站的仓库体积特别大,想部署到第三方免费平台(比如Cloudflare Pages)就变得很困难。
现在的问题很明确了:我得换一个更轻便的主场,最好主要面向纯文字的。于是我在Hugo主题网站上找来找去,最后选中了这款叫Bear(熊)的主题。下载下来一看,这主题确实轻量,总共才几十KB,改起来也很方便。它本身是没有任何CSS的,只有最基础的HTML结构。不过我在实际用的时候,发现还是需要加点CSS的,就又对这个主题“魔改”了一番。后来又发现其实官方主题库有多个 Bear 版本,其中 Bear Cub 就带了 CSS,于是又换成了这个。
迁移到这个新主题的过程中,最难搞定的问题是我原来Hugo项目的文件组织结构。
我所有的内容,都是放在项目的content目录下面的。但这里边,我还分了两个文件夹:一个叫post,另一个叫posts(就是post加了个s)。之所以要分两个,最主要的原因是:文章数量实在太多了。如果都放在一个文件夹里,平常写东西或者整理的时候,很容易手滑把文件复制到错误的地方。我的策略是:把“已归档”的历史文章放在一个文件夹里(比如posts),然后新写的文章就放到另一个文件夹(比如post)。等post这个文件夹里的文章积累到一定数量了,我再手动把它们挪进posts文件夹去归档。这样用下来感觉就比较清晰可控,不容易出错。
另一个特点是,我原来的做法是:每篇博客文章的Markdown文件和它用到的图片都放在同一个文件夹里。比如写一篇叫《猫》的文章,那对应的路径可能是/content/posts/猫/,里面直接放着index.md、index.en.md(因为我博客是双语的)和所有这篇用到的图片(比如cat1.jpg)。
这种方式其实我自己觉得是优点:
- 本地写作超方便:要插图的时候直接粘贴进来就行,图片瞬间就存到文章的文件夹里了。
- 非常便于预览和管理:图片就在Markdown文件旁边,用任何支持本地图片的预览器都能直接显示。
- 几乎不会丢图片:路径相对简单且固定,文件和图总是捆在一起。
结果换主题就撞墙了!因为绝大多数流行的主题,都默认使用“文字和图片分离”的模式——Markdown文件放在content/...下面,而图片统一放在像static/images/这样的目录里。可能是为了仓库管理更清晰或者缓存优化吧?但我就是觉得我原来那种“图片同目录”的方式更顺手方便。所以这次换主题,最大的挑战就在于:怎么让我原有的这套“图片同目录”的文件结构,能完美兼容新主题的需求?
这个适配过程……那自然是反反复复测试了很多次。最后我选择的解决方案是:把我存放博客文章的核心文件夹(也就是content下面那些post/posts目录们)做成一个Git Submodule(子模块)。
具体操作是:
- 我把存放所有文章内容的文件夹分离出来,形成了一个独立的Git仓库(我们姑且叫它“写作仓库”)。
- 然后我在主题仓库(无论是原来的Stack仓库还是新的Bear主题仓库)里,把这个独立的“写作仓库”作为
Git Submodule引入。
这么做的好处是:
- 写作最简化:以后写新的博客文章、添加图片,我只需要在我的这个独立的“写作仓库”里操作就行了。它们天然维持着我习惯的路径结构(图文同目录)。
- 主题适配灵活:现在,我同时在用两套主题!原来的Semi仓库在用这个内容子模块,新换上的Bear仓库也在用这个内容子模块。这样,我只需要更新一次这个“写作仓库”,然后分别构建Semi和Bear两个主题的仓库,就能同时生成为两个主题适配的网站静态文件了。
- 便于使用图库:我已经将这个写作的仓库同步到Cloudflare R2存储,如果后续有需要,随时可以将整个图片设置成远程链接的模式,避免很快就用尽Vercel这类免费服务的资源。
- 为了解决两个主题仓库的自动化构建问题,我还给GitHub Actions那边的配置加了好几个工作流(Workflow),确保当写作仓库更新后,两个主题仓库都能被自动触发构建并部署。
选择现在这种“双主题并存 + 内容子模块”的方案,除了技术适配的考量,还有一个很现实的原因:我现在的阿里云VPS快要到期了。目前博客主要还是托管在VPS上,相对来说比较稳定、速度也快。但长远考虑,我很可能会把整个网站完全托管到Vercel或Cloudflare Pages这类平台上去。要把内容全部迁移过去,网站的体积就必须得尽可能精简优化。这次选择超级轻量的Bear主题,很大程度上也是为未来的这一步部署策略在做准备。
如何为静态博客设置一个说说页面
刚看到博友圈微信群中有群友问如何在静态博客添加朋友圈/说说这类功能,我第一反应是可以用一个无头CMS单独管理和发表这个页面,但打开PageCMS试了下,发现其实也很麻烦。于是想到跨站引用。这不,试了下 Github Issue 发现很完美。
使用教程
只需 2 步简单操作,一劳永逸解决静态博客添加朋友圈、说说之类的功能
注意事项
在设置模板过程中,需要留意以下几个问题:
- 页面构建缓存。可能导致页面内容可能无法更新。
1 {{ $url := "https://api.github.com/repos/user/moments/issues/1/comments" }}
2 {{ $opts := dict
3 "headers" (dict "User-Agent" "Hugo Static Site Generator")
4 "cache" 300
5 "cacheKey" (printf "gh-comments-%s" (now.Format "2006-01-02-15:04"))
6 }}- 内容排序。 github issue api 输出数据是最新的内容在后边,需要倒过来。
1 {{ with resources.GetRemote $url $opts }}
2 {{ if and .Content (ne .Content "") }}
3 {{ $comments := .Content | transform.Unmarshal (dict "format" "json") }}
4 {{ $sortedComments := sort $comments "created_at" "desc" }} - 时间格式。github issue 默认使用 UTC 时间,中国的话,需要在基准上加8个小时。
1<time>
2 {{ (.created_at | time.AsTime).Add 28800e9 | time.Format "2006-01-02 15:04" }}
3</time>本页面说明
为减少 Github 图片调用,本页面内容通常只保留最近一个月更新,查看往期内容,可点击下方链接查看。 说说