之前用 Hugo-theme-stack 主题,自带了搜索功能,我曾以为这种功能是 Hugo 默认就有,后来才知道,是外挂。
在更换 Bear cub 主题后,之前也没觉得这功能有多重要,但实际使用中发现,有时候记不起来一些事情,还得借助搜索才行。于是研究了下怎么外挂搜索。其实也很简单,只需四步。
在 content 目录下新建 search.md 页面
如果不需要放置在导航栏,内容填入默认信息即可,需要进导航栏的,效仿其他 md 文件设置。
1
2
3
4
5
6
7
| ---
title: "搜索"
date: 2025-11-20T00:00:00+08:00
type: "search"
layout: "search"
---
在此搜索本博客文章。
|
在 hugo 配置文件中添加 json 输出
比如我的hugo配置文件是 toml 格式,内容如下。
1
2
3
4
5
6
| [outputs]
home = ["HTML", "RSS", "JSON"] # 添加 JSON 输出
[outputFormats.JSON]
baseName = "index"
mediaType = "application/json"
|
在 layouts 目录下新建 /search/single.html 模板
模板内容可以参照其他模板修改,搜索功能可定制精确搜索或模糊搜索,大致内容如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
| {{ 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 }}
|
在 layouts 中新建 index.json 模板
主要就是将整个博客文档内容输出到 index.json 文件,搜索时直接在该文件内搜索,可依照自己需求进行修改,比如只搜索标题、摘要、标签等信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| [
{{- $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 模板中也可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
| /* ====== 搜索框区域布局 ====== */
.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;
}
}
|