TOC
更新网站搜索
网站菜单栏的搜索原本是全站所有页面都搜索,搜索内容包括:标题,内容。
要更新的原因:
- 我注意到index.json这个文件比较大,这才没几篇文章呢,这个文件就有90kb了;
- 索引包括了如 home, tags 这些页面,这部分没有意义,需要去掉;
计划更新后的全站搜索内容包括:标题,summary。基于此,需要额外编辑内容的summary,因为直接取前summaryLength定义的数量不准确。
注意
summaryLength
和之前的不太一样,以前是字符数量,现在变成了段落数量(下方的说明还是说的是字词数量,但是我实际使用的时候发现用的确实是段落数量,可能是一个bug,也可能是我自己使用不对 —— 这不太可能,毕竟我就是正常在用,我的hugo版本是: hugo v0.145.0+extended+withdeploy darwin/arm64 BuildDate=2025-02-26T15:41:25Z VendorInfo=brew 。
(int) Applicable to automatic summaries, the minimum number of words returned by the Summary method on a Page object. The Summary method will return content truncated at the paragraph boundary closest to the specified summaryLength, but at least this minimum number of words.
建立全站搜索 - 本次更新前
在 /themes/blog/layouts/_default/index.json:
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.Pages -}}
{{- $.Scratch.Add "index" (dict
"title" .Title
"content" .Plain
"url" .Permalink
) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
在 main.js 中搜索相关的内容:
// 初始化搜索功能
function initSearch() {
const searchToggle = document.getElementById('search-toggle');
const searchBox = document.getElementById('search-box');
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const clearSearchButton = document.getElementById('clear-search');
if (searchToggle && searchBox && searchInput && searchResults && clearSearchButton) {
// 显示/隐藏搜索框
searchToggle.addEventListener('click', () => {
searchBox.style.display = searchBox.style.display === 'block' ? 'none' : 'block';
searchInput.focus();
});
// 点击页面其他区域隐藏搜索框
document.addEventListener('click', (event) => {
const isClickInsideSearchBox = searchBox.contains(event.target); // 判断点击目标是否在搜索框内部
const isClickOnSearchToggle = searchToggle.contains(event.target); // 判断点击目标是否是搜索图标
// 如果点击的目标不在搜索框内部,也不是搜索图标,则隐藏搜索框
if (!isClickInsideSearchBox && !isClickOnSearchToggle) {
searchBox.style.display = 'none';
}
});
// 加载搜索索引
let searchIndex = [];
fetch('/index.json')
.then(response => response.json())
.then(data => {
searchIndex = data;
});
// 搜索功能
searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase();
// 筛选结果
const results = searchIndex.filter(item => {
return (
item.title.toLowerCase().includes(query) ||
item.content.toLowerCase().includes(query)
);
});
// 去重:基于 URL 去重
const uniqueResults = [];
const seenUrls = new Set(); // 用于记录已经处理过的 URL
for (const item of results) {
if (!seenUrls.has(item.url)) {
seenUrls.add(item.url); // 将 URL 添加到 Set 中
uniqueResults.push(item); // 将去重后的结果添加到数组中
}
}
// 按匹配度排序(标题匹配的优先级高于内容匹配)
uniqueResults.sort((a, b) => {
const aTitleMatch = a.title.toLowerCase().includes(query);
const bTitleMatch = b.title.toLowerCase().includes(query);
if (aTitleMatch && !bTitleMatch) return -1; // a 优先
if (!aTitleMatch && bTitleMatch) return 1; // b 优先
return 0; // 保持顺序
});
// 显示搜索结果
searchResults.innerHTML = uniqueResults
.map(item => `<li><a href="${item.url}">${item.title}</a></li>`)
.join('');
});
// 清除搜索内容
clearSearchButton.addEventListener('click', () => {
searchInput.value = ''; // 清空输入框内容
searchResults.innerHTML = ''; // 清空搜索结果
searchInput.focus(); // 聚焦到输入框
});
}
}
建立全站搜索 - 本次更新后 - 20250326
首先,确定需要索引的section, 为了便于后期维护,我在hugo.yaml定义了这部分:
params:
searchInSection:
- posts
- books
- english
然后编辑 /themes/blog/layouts/_default/index.json:
{{- $sectionsToIndex := .Site.Params.searchInSection | default (slice "posts") -}}
{{- $.Scratch.Add "index" slice -}}
{{- range where .Site.Pages "Section" "in" $sectionsToIndex -}}
{{- $.Scratch.Add "index" (dict
"title" .Title
"summary" (.Summary | plainify)
"url" .Permalink
) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
- 如果没有配置,默认只索引 “posts” section;
- 索引内容移除了页面的内容,增加了页面的摘要;
- 摘要净化处理: (.Summary | plainify) ,移除摘要中的 HTML 标签
相关的 main.js:
searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase();
const results = searchIndex.filter(item => {
return (
item.title.toLowerCase().includes(query) ||
(item.summary && item.summary.toLowerCase().includes(query))
);
});
// 显示结果...
});
至此,全站的搜索就更新完成了,优点是index.json的内容减少了,缺点当然就是搜索的范围缩小了,不过通过认为定义summary 也可以浓缩页面的主旨。
建立section搜索
如果不要全站搜索,只要比如posts/下边的内容,当然可以只保留posts,就可以了。
但是我考虑的是后期要给 books, english 建立单独的搜索,并且同时要保留全站搜索,所以,需要新建。
这里记录一下deepseek给的回复,例子用的是只要posts/的内容。
以下内容应该被删除,因为实践不可行 - 20250329
创建专用的 posts.json 索引文件
在 layouts/_default/ 目录下创建一个新的模板文件 posts.json,只包含 /posts/ 下的内容。
{{- $.Scratch.Add "index" slice -}} {{- range where .Site.Pages "Section" "posts" -}} {{- $.Scratch.Add "index" (dict "title" .Title "content" .Plain "url" .Permalink ) -}} {{- end -}} {{- $.Scratch.Get "index" | jsonify -}}这里需要注意搜索的范围是:标题,内容。如果需要可以自己改。
修改 JavaScript 代码,加载 /posts.json 而非 /index.json:
fetch('/posts.json') // 替换原来的 fetch('/index.json') .then(response => response.json()) .then(data => { searchIndex = data; });
总结:先说下上边的方法为什么不可行,首先创建的posts.json文件压根不会生成,根据deepseek的说明,我反复尝试过修改hugo.yaml,调整posts.json到posts/下等,结果都不可以,所以改版成现在这样:整个网站只有一个index.json文件,在文件索引部分加上一个section,然后各个 Section 的 list 页面可以查询本 Section 相关的内容,这种在我看来其实才是最简单实用的。
这次(2025-03-29)改动后实现的效果是上边的总结所说的,具体的实现方法和需要注意的地方在这里记录一下:
index.json需要手动调整:
{{- $index := slice -}}
{{- $sections := .Site.Params.searchInSection | default (slice "posts") -}}
{{- range .Site.RegularPages -}}
{{- if in $sections .Section -}}
{{- $item := dict "section" .Section "url" .RelPermalink -}}
{{/* 调试信息 */}}
{{/*
{{- warnf "Processing page: %s (Section: %s)" .File.Path .Section -}}
*/}}
{{/* 公共字段 */}}
{{- $item = merge $item (dict "title" (.Title | default "Untitled")) -}}
{{/* Section特定字段 */}}
{{- if eq .Section "posts" -}}
{{- $item = merge $item (dict "summary" (.Summary | plainify | default "")) -}}
{{- else if eq .Section "books" -}}
{{- $item = merge $item (dict
"author" (.Params.author | default "Unknown")
"isbn" (.Params.isbn | default "")
) -}}
{{- end -}}
{{- $index = $index | append $item -}}
{{- end -}}
{{- end -}}
{{/*
{{- warnf "Generated %d search items" (len $index) -}}
*/}}
{{- $index | jsonify -}}
各个section因为各自内容设计的不同,所以需要索引的也不一样。
main.js 需要手动调整
const results = searchIndex.filter(item => {
// 根据当前 section 决定搜索哪些字段
if (currentSection === 'posts') {
return (
item.title.toLowerCase().includes(query) ||
(item.summary && item.summary.toLowerCase().includes(query))
);
} else if (currentSection === 'books') {
return (
item.title.toLowerCase().includes(query) ||
(item.author && item.author.toLowerCase().includes(query)) ||
(item.isbn && item.isbn.toLowerCase().includes(query))
);
}
return false;
});
// 按匹配度排序
results.sort((a, b) => {
// 标题匹配优先
const aTitleMatch = a.title.toLowerCase().includes(query);
const bTitleMatch = b.title.toLowerCase().includes(query);
if (aTitleMatch && !bTitleMatch) return -1;
if (!aTitleMatch && bTitleMatch) return 1;
// 其次摘要/作者匹配
const aSecondaryMatch = currentSection === 'posts'
? a.summary?.toLowerCase().includes(query)
: a.author?.toLowerCase().includes(query);
const bSecondaryMatch = currentSection === 'posts'
? b.summary?.toLowerCase().includes(query)
: b.author?.toLowerCase().includes(query);
if (aSecondaryMatch && !bSecondaryMatch) return -1;
if (!aSecondaryMatch && bSecondaryMatch) return 1;
return 0;
});
可以看到这里的搜索是写明了posts和books相关的内容,后期如果要修改或者新增,也需要在这里再次编辑。
另外增加了一个搜索结果匹配高亮的效果。
新增/books/ 20250330
新增了一个section: books. 用来记录看过的书以及对应的读后感和总结。
前后还是折腾了一下,最后才改成现在这种最简单的形式,这里记录一下整个过程。
最开始计划的是要能实现直接阅读,保存阅读进度,下载书籍,链接读后感,书籍分类,书籍信息…
现在:显示书籍基本信息,读后感。
我觉得,网站还是得轻巧才行,如果要实现直接阅读,那么加载一本书(或者实现分页),还要考虑到书的格式(EPUB 还需要调用外部库),想想都庞杂,所以,结合我自己的实际情况,我就用苹果自带的图书app看书,看完我就记录自己的想法就可以了,书籍基本信息里有一个isbn,需要的书的话自己查,而且最不济的情况下:大不了我后期把这些书都上传到github上去,需要的时候直接去下载,倒也放心。
在这个过程中查到几个开源的,可以在线使用的电子书阅读应用: github topic ebook
添加页面隐藏 20250331
新增了一个.Params.hide, 用来实现页面隐藏。有些时候不想删除一篇文章,但是出于一些原因不想显示在网站上,就在front matter里加上这个。
目前已经实现了在 home, list, index.json 都可以隐藏,没有实现的是 /tags/页面的tag count, 也就是说如果一个页面被隐藏了,但是有tag,那么在统计这个tag的数量的时候还是会把这篇文章给算上,不过你点进去找不到这个文章,所以会造成一个现象:比如你看 tools 这个tag显示有5篇文章,但是你点进去会发现只有4个链接,原因可能就是其中一篇文章被隐藏了,但是它的tags包含了tools 这个词。
我倒想直接改进实现统一,但是deepseek给的解决方法我认为比较麻烦,所以先搁置,以后再说。
总结: 在这个过程中,我还改了网站的图片存储位置,都放到了static文件夹下,这就导致了一个问题:以前放在其他地方的,比如图片,现在他们的路径都变了,那么使用这些图片的文件又有哪些,具体又在这些文件的什么位置,我是不是还得一个个去找到然后修改了,要是我足够细心加耐心,把他们一个个手动改正并且改对了,那还好,那如果我接下来又换了一次这些图片的路径,或者改了一个图片的名称,那我…
想想都庆幸自己一开始的时候就把这些变量放到hugo.yaml这个配置文件中,使用的时候都是来引用这个文件的内容,所以如果要改动什么文件的位置或者名称,在这里改动一次,其他引用的地方自己就更新了。
想想这个过程,逻辑,升华一下…