Theme-M 最近进度不错,就打算把旧版的本地搜索移植过来再加个 Worker 搜索。
用 hexo-generator-search 生成索引
剩下的内容都需要用到它。在 Hexo 目录下用 npm 安装:
npm i hexo-generator-search然后在站点配置文件 _config.yml 写入如下配置:
search:
path: search.json # 生成 JSON 文件
content: true # 包括文章内容执行一下 hexo g,public 目录里出现 search.json 就代表正常工作。
本地搜索
还是那么些东西,但我真的很不想在自己的主题里放一个带一大串注释的 search.js,于是索性重新写了一份。
ES6+ 香到我直接不用 mdui.JQ 了。
<form onkeydown="if (event.keyCode == 13) return false" class="mdui-textfield mdui-m-b-2">
<i class="mdui-icon material-icons">search</i>
<input id="local-input" type="search" name="q" class="mdui-textfield-input"
placeholder="<%= __('common.search') %>" disabled>
</form>
<div id="local-result" style="min-height:100vh; transition: all .4s" class="mdui-list">
</div>
<script>
fetch(`<% if(theme.search.local.url) { %><%- theme.search.local.url %><% } else { %><%- url_for('search.json') %><% } %>`)
.then(res => res.json().then(data => {
document.getElementById('local-input').disabled = false
document.getElementById('local-input').addEventListener('input', () => {
let keyword = document.getElementById('local-input').value.trim().toLowerCase()
document.getElementById('local-result').innerHTML = ''
if (keyword.length <= 0) return
data.forEach(({title, content, url}) => {
const append = excerpt => document.getElementById('local-result').insertAdjacentHTML('beforeend', `
<a href=${url} class="mdui-list-item mdui-ripple">
<div class="mdui-list-item-content">
<div class="mdui-list-item-title mdui-list-item-one-line">${title}</div>
<div class="mdui-list-item-text mdui-list-item-two-line">${excerpt}</div>
</div>
</a>`)
if (content.toLowerCase().includes(keyword)) append(content.substring((content.toLowerCase().indexOf(keyword) - 9), (content.toLowerCase().indexOf(keyword) + 130)))
else if (title.toLowerCase().includes(keyword)) append(content.substring(0, 139))
})
})
}))
</script>完成。逻辑基本和原来差别不大,但 JS 只有不到 20 行。
Worker 搜索
灵感来自 cloudflare workers实现静态网站全站搜索 - zcmimi’s blog,针对 Hexo 进行了重写。
后来想起来旧主题自己曾经写过一个 Google Custom Search JSON API (以下简称 google-json) 的模板,就有了一个想法:为什么我不能直接拿来用呢?
所以 Worker 后端的目标就是和 google-json 格式一致,并且能处理多个 Hexo 站点的搜索。
格式
请求地址:
https://*.workers.dev/?siteSearch=站点&q=关键字返回 JSON:
{
"items": [
{
"title": "标题1",
"link": "链接1",
"snippet": "描述1"
},
{
"title": "标题2",
"link": "链接2",
"snippet": "描述2"
}
]
}只需要这点内容。
index.js
那么首先定义一下 json 文件常量和存储,
如果 URL 参数填写了 siteSearch 则直接使用,没有就用对象里第一个值。
const file = {
"kwaa.dev": "https://kwaa.dev/search.json",
"https://kwaa.dev": "https://kwaa.dev/search.json"
}
let data = {}
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})定义 getdata 和 search 函数,搜索部分我直接把上面本地搜索部分移植了一下。
async function getdata(searchSite) {
await fetch(searchSite).
then(res => res.json()).
then(json => data[searchSite] = json)
}
async function search(searchTerm, searchSite) {
searchTerm = JSON.parse('"' + searchTerm.trim().toLowerCase() + '"')
if (!data[searchSite]) await getdata(searchSite)
let res = { "items": [] }
data[searchSite].forEach(({ title, content, url }) => {
const push = (content) => res.items.push({
"title": title,
"link": url,
"snippet": content
})
if (content.toLowerCase().includes(searchTerm)) push(content.replace(/<[^>]+>/g, "").substring((content.toLowerCase().indexOf(searchTerm) -9), (content.toLowerCase().indexOf(searchTerm) + 130)))
else if (title.toLowerCase().includes(searchTerm)) push(content.replace(/<[^>]+>/g, "").substring(0, 139));
})
return JSON.stringify(res);
}最后是 handleRequest:
async function handleRequest(request) {
const { searchParams } = new URL(request.url)
let searchTerm = searchParams.get('q'), searchSite
if (searchTerm == undefined) {
return new Response("usage:\n\
?siteSearch=<site>&q=<keyword>\n\
required: q", { status: 404 })
}
if (searchParams.get('siteSearch')) {
searchSite = site[searchParams.get('siteSearch')]
} else {
searchSite = Object.values(site)[0]
}
return new Response(await search(searchTerm, searchSite), {
status: 200,
headers: new Headers({
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
'access-control-max-age': '1728000',
}),
})
}在页面里显示结果:
<form action="servlet" method="post" onsubmit="return searchAPI(this.searchTerm.value);" class="mdui-textfield mdui-m-b-2">
<i class="mdui-icon material-icons">search</i>
<input id="searchTerm" type="search" name="q" class="mdui-textfield-input"
placeholder="<%= __('common.search') %>">
</form>
<div id="api-result" style="min-height:100vh; transition: all .4s" class="mdui-list">
</div>
<script>
function searchAPI(searchTerm) {
fetch(`https://search.kwaa.workers.dev/?q=${searchTerm}<% if(theme.search.api.site !== false) { %>&siteSearch=<% if(theme.search.api.site == '') { %><%= config.root %><% } else { %><%= theme.search.api.site %><% }} if (theme.search.api.key && theme.search.api.id) { %>&key=<%= theme.search.api.key %>&cx=<%= theme.search.api.id %><% } %>`)
.then(res => res.json().then(json => json.items.forEach(({title, link, snippet}) => document.getElementById('api-result').insertAdjacentHTML('beforeend', `
<a class="mdui-list-item mdui-ripple" href="${link}">
<div class="mdui-list-item-content">
<div class="mdui-list-item-title mdui-list-item-one-line">${title}</div>
<div class="mdui-list-item-text mdui-list-item-two-line">${snippet}</div>
</div>
</a>`)
)))
return false;
}
</script>所以 Theme-M 可以直接使用 google-json 并修改 url 以支持 Worker 搜索引擎;其他适配 google-json 的 Hexo 主题也可以如此套用,真是太好了。但是真的有 Hexo 主题适配这种玩意吗?
那么本文在这里结束,欢迎体验 Hexo Theme-M。虽然还在更新,但 repo 已经很久不动了
作者:藍
链接:https://kwaa.dev/p/hexo-search/
本文采用 CC BY-NC-SA 4.0 进行许可。