搜 | 终于安排上站内搜索功能啦!
站内搜索功能
Hugo官方提供了多种站内搜索的解决方案:https://gohugo.io/tools/search/,不过好像由于文档比较晦涩而且很久没人维护的原因,上手起来都不是很方便。algolia可以尝试一下,但是对引导文件的大小有限制,涉及到付费升级,也不想用。
这里我使用的是中文分词器,在此借鉴了这个项目:https://github.com/naah69/Hugo-Algolia-Chinese-Builder
根据项目的readme,下载到hugo的项目目录,不需要编写config.yaml
文件,需要在config.toml
添加:
[outputs]
# 增加 JSON 配置
home = ["HTML", "RSS", "JSON"]
即可根据日常打包的命令,如hugo -D
,去生成对应的静态文件,接着即可在生成的public目录下找到生成的index.json
文件。找到本地生成的这个json文件,即标志此步骤顺利。
添加业务代码
1. html
/theme/cactus/layouts/partials/mysearch.html
<a id="search-click" class="search-sou" href="#">搜</a>
<div id="fastSearch" class="search-container" style="display: none;">
<div>
<input class="aa-Input" aria-autocomplete="both" aria-labelledby="autocomplete-0-label"
id="searchInput" autocomplete="off" autocorrect="off" autocapitalize="off" enterkeyhint="go"
spellcheck="false" placeholder="Search..." maxlength="512" type="search">
<!-- <input type="text" id="searchInput" placeholder="Search..."/>-->
</div>
<ul id="searchResults"></ul>
</div>
<!-- search -->
<link rel="stylesheet" href="{{ "css/mysearch.css" | relURL }}" />
<script src="/js/fuse.min.js"></script> <!-- download and copy over fuse.min.js file from fusejs.io -->
<script src="/js/mysearch.js"></script>
2. css
/theme/cactus/layouts/static/css/mysearch.css
/* 默认样式 */
#search-click {
position: fixed;
top: 20px;
right: 20px;
font-size: 24px;
width: 30px;
}
/*.search-sou {*/
/* position: fixed !important;*/
/* top: 10px !important; !* adjust this value to move the button up or down *!*/
/* right: 20px !important; !* adjust this value to move the button left or right *!*/
/* z-index: 100; !* ensure the button is always on top *!*/
/*}*/
.search-container {
position: fixed;
top: 20px;
right: 20px;
width: 400px;
z-index: 9999;
background-color: transparent ; /* 博客背景颜色 */
/*opacity: 0; !* 完全透明 *!*/
}
/*e2e0de*/
.search-highlight {
background-color: #ffc107; /* 高亮背景颜色 */
}
.search-highlight-a {
background-color: #ffe44d; /* 高亮背景颜色 */
}
.search-highlight-b {
background-color: #ffc107; /* 高亮背景颜色 */
}
.search-input-wrapper {
position: relative;
}
#searchInput {
height: 32px; /* adjust this value as needed */
width: 100%;
padding: 10px;
font-size: 16px;
outline: none;
background: #e2e0de;
font-weight: bold; /* 设置粗体 */
border: 0px solid #ccc;;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.search-icon {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 20px;
height: 20px;
fill: #888;
pointer-events: none;
}
#searchResults {
/*list-style-type: none;*/
padding: 0;
margin: 0;
max-height: 900px; /* 设置最大高度 */
overflow-y: auto; /* 显示垂直滚动条 */
-webkit-overflow-scrolling: touch; /* 启用平滑滚动 */
margin-top: 10px !important; /* 你可以调整这个值来改变间距 */
border: 0px solid #ccc;;
border-radius: 5px;
box-shadow: 2px 2px 5px rgba(0.8, 0, 0, 0.1);
background-color: #e2e0de;
}
/* 移动设备样式 */
@media (max-width: 768px) {
#searchResults {
max-height: 500px;
}
}
#searchResults::-webkit-scrollbar {
width: 10px; /* 设置滚动条宽度 */
}
#searchResults::-webkit-scrollbar-track {
background-color: #f1f1f1; /* 滚动条轨道背景颜色 */
}
#searchResults::-webkit-scrollbar-thumb {
background-color: #888; /* 滚动条拖动块背景颜色 */
border-radius: 5px; /* 滚动条拖动块边框半径 */
}
#searchResults::-webkit-scrollbar-thumb:hover {
background-color: #555; /* 鼠标悬停时的滚动条拖动块背景颜色 */
}
#searchResults li {
padding: 10px;
/*background-color: #f8f9fa;*/
transition: background-color 0.3s ease; /* 添加过渡效果 */
}
#searchResults li:hover {
background-color: #e9ecef; /* 设置悬停时的背景颜色 */
}
#searchResults li a {
color: #333;
text-decoration: none;
}
#searchResults li a:hover {
text-decoration: underline;
}
/* 移动设备样式 */
@media (max-width: 768px) {
.search-container {
width: 100%;
max-width: 300px;
left: 50%;
transform: translateX(-50%);
}
}
3. js
/theme/cactus/layouts/static/js/mysearch.js
// loadSearch()
// 初始化全局变量
var fuse; // holds our search engine
var searchVisible = false;
var firstRun = true; // allow us to delay loading json data unless search activated
var list = document.getElementById('searchResults'); // targets the <ul>
var first = list.firstChild; // first child of search list
var last = list.lastChild; // last child of search list
var maininput = document.getElementById('searchInput'); // input box for search
var resultsAvailable = false; // Did we get any search results?
// 主键盘事件监听器
document.addEventListener('keydown', function (event) {
// CMD-/ 切换搜索可见性
if (event.metaKey && event.which === 191) {
showSearch();
}
// ESC 关闭搜索框
if (event.keyCode == 27) {
if (searchVisible) {
document.getElementById("fastSearch").style.display = "none";
document.activeElement.blur();
searchVisible = false;
}
}
// 方向键上下移动
if (event.keyCode == 38 || event.keyCode == 40) {
if (searchVisible && resultsAvailable) {
event.preventDefault();
var active = document.activeElement;
if (event.keyCode == 40) {
if (active == maininput) {
first.focus();
} else if (active == last) {
last.focus();
} else {
active.parentElement.nextSibling.firstElementChild.focus();
}
} else if (event.keyCode == 38) {
if (active == maininput) {
maininput.focus();
} else if (active == first) {
maininput.focus();
} else {
active.parentElement.previousSibling.firstElementChild.focus();
}
}
}
}
});
// 当输入时执行搜索
document.getElementById("searchInput").onkeyup = function (e) {
executeSearch(this.value);
}
// 点击事件监听器,检查点击位置并隐藏搜索框
document.addEventListener('click', function (event) {
var searchContainer = document.getElementById('fastSearch');
var searchInput = document.getElementById('searchInput');
var sDom = document.getElementById('search-click');
// 检查点击事件发生的位置
var isClickedOutsideSearch = !searchContainer.contains(event.target);
if (isClickedOutsideSearch) {
// 隐藏搜索框
searchContainer.style.display = 'none';
searchInput.value = '';
searchVisible = false;
// 显示 搜
sDom.style.display = 'block'; // 隐藏
}
});
// 点击 搜 时,展示搜索框,隐藏 搜
document.addEventListener("click", event => {
var cDom = document.getElementById("fastSearch");
var sDom = document.getElementById('search-click');
var tDom = event.target;
if (sDom == tDom || sDom.contains(tDom)) {
sDom.style.display = 'none'; // 隐藏 搜
showSearch();
} else if (cDom == tDom || cDom.contains(tDom)) {
// ...
} else if (searchVisible) {
cDom.style.display = "none"
searchVisible = false;
}
});
// 显示搜索框,并在首次运行时加载搜索引擎
function showSearch() {
if (firstRun) {
loadSearch();
firstRun = false;
}
if (!searchVisible) {
document.getElementById("fastSearch").style.display = "block";
document.getElementById("searchInput").focus();
searchVisible = true;
} else {
document.getElementById("fastSearch").style.display = "none";
document.activeElement.blur();
searchVisible = false;
}
}
// 使用 XMLHttpRequest 获取 JSON 文件
function fetchJSONFile(path, callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 200) {
var data = JSON.parse(httpRequest.responseText);
if (callback) callback(data);
}
}
};
httpRequest.open('GET', path);
httpRequest.send();
}
// 加载搜索引擎
function loadSearch() {
fetchJSONFile('/index.json', function (data) {
var options = {
includeMatches: true,
shouldSort: true,
ignoreLocation: true,
keys: [{
name: 'title',
weight: 1,
}, {
name: 'content',
weight: 0.6,
}],
};
fuse = new Fuse(data, options);
});
}
// 执行搜索
function executeSearch(term) {
if (term.length == 0) {
document.getElementById("searchResults").setAttribute("style", "");
return;
}
let results = fuse.search(term);
// ...
let searchItems = '';
if (results.length === 0) {
resultsAvailable = false;
searchItems = '<li class="noSearchResult">无结果</li>';
} else {
permalinkList = []
searchItemCount = 0
for (let item in results) {
if (permalinkList.includes(results[item].item.permalink)) {
continue;
}
permalinkList.push(results[item].item.permalink);
searchItemCount += 1;
title = '<span class="search-highlight-a">' + results[item].item.title + '</span>';
content = results[item].item.content.slice(0, 50);
for (const match of results[item].matches) {
if (match.key == 'title') {
startIndex = match.indices[0][0];
endIndex = match.indices[0][1] + 1;
highText = '<span class="search-highlight">' + match.value.slice(startIndex, endIndex) + '</span>';
// title = match.value.slice(0, startIndex) + highText + match.value.slice(endIndex);
} else if (match.key == 'content') {
startIndex = match.indices[0][0];
endIndex = match.indices[0][1] + 1;
highText = '<span class="search-highlight-b">' + match.value.slice(startIndex, endIndex) + '</span>';
content = match.value.slice(Math.max(0, startIndex - 30), startIndex) + highText + match.value.slice(endIndex, endIndex + 30);
}
}
searchItems = searchItems + '<li><a href="' + results[item].item.permalink + '">' + '<span class="title">' + title + '</span><br /> <span class="sc">' + content + '</span></a></li>';
// 只显示前 5 个结果
if (searchItemCount >= 1000) {
break;
}
}
resultsAvailable = true;
}
document.getElementById("searchResults").setAttribute("style", "display: block;");
document.getElementById("searchResults").innerHTML = searchItems;
if (results.length > 0) {
first = list.firstChild.firstElementChild;
last = list.lastChild.firstElementChild;
}
}
最后在这个文件中插入{{ partial "mysearch.html" . }}
theme/cactus/layouts/_default/baseof.html
<!DOCTYPE html>
<!--<html lang="{{ .Site.LanguageCode }}">-->
<html>
{{ partial "head.html" . }}
<body class="max-width mx-auto px3 ltr">
<div class="content index py4">
{{ partial "mysearch.html" . }}
{{ partial "header.html" . }}
{{ block "main" . }}
{{ end }}
{{ partial "footer.html" . }}
</div>
{{ if .Site.GoogleAnalytics }}
{{ if .Site.Params.googleAnalyticsAsync }}
{{ template "_internal/google_analytics_async.html" . }}
{{ else }}
{{ template "_internal/google_analytics.html" . }}
{{ end }}
{{ end }}
</body>
</html>
这些工作都做好之后,来看看效果吧!在主页右上角的地方就可以找到这个「搜」的功能啦,我个人觉得这样特别有味道,简单直接但富有内涵,且耐看。
移动端则是这样的
后记
这个功能的设计还有美化,受到这位无灯老师(https://ttys3.dev/)的启发,很多界面的设计都大程度地模仿了他的。
另外本次开发这个站内搜索的功能,得到了chatgpt的倾力协助,整个开发过程的很多功能设计都是它帮我生成的,给了很多代码实例,比如很多js、css的代码。通常情况就是我提出想要达到一个什么样的功能,然后让他一什么样的要求去提供代码,在面对错误的答案时提醒自纠,改进代码的质量。在给出代码的基础上,再加上自己的修改。
当然整个使用的过程,也发现了他很多的局限性,比如给出的代码其实不能做到马上应用,需要你来我往地多次提交反馈,让他不断自我纠错和改进去达到我想要的效果。
如果你是一个有一定相关的开发基础,但离单独开发一个功能还有距离,那么其实根据自己的知识,加上gpt的帮助,确实能够帮你快速去实现这样一个你不太熟悉的功能设计。
更多关于博客的精彩文章请戳: