视频加载失败

Hexo Shoka 主题本地搜索

1887 字
9 分钟
Hexo Shoka 主题本地搜索

由于 Shoka 作者提供的搜索使用 Algolia 实现,但是 Algolia 免费版是有每月请求次数限制的,因此想要使用本地搜索来替换。功夫不负有心人,我在 Shoka 主题的搜索功能配置找到了解决办法,在此感谢 linn 提供的方法。

问题 & 解决#

在使用过程中也遇到了一些问题,如 localSearch() 方法报不支持的语法错误,所以在此记录一下各文件的修改情况。

安装插件#

本地搜索通过 hexo-generator-searchdb 插件实现,需要先安装插件。

Terminal window
npm install hexo-generator-searchdb

修改 page.js#

shoka/source/js/_app/page.jslocalSearch() 修改后的文件如下,将整个 localSearch 复制到主题的 shoka/source/js/_app/page.js 中即可,不要覆盖原文件。

const localSearch = function(pjax) {
// 参考 hexo next 主题的配置方法
// 参考 https://qiuyiwu.github.io/2019/01/25/Hexo-LocalSearch/ 博文
if(CONFIG.localSearch === null)
return
if(!siteSearch) {
siteSearch = BODY.createChild('div', {
id: 'search',
innerHTML: '<div class="inner"><div class="header"><span class="icon"><i class="ic i-search"></i></span><div class="search-input-container"><input class="search-input"autocompvare="off"placeholder="'+LOCAL.search.placeholder+'"spellcheck="false"type="text"id="local-search-input"></div><span class="close-btn"><i class="ic i-times-circle"></i></span></div><div class="results"id="search-results"><div class="inner"><div id="search-stats"></div><div id="search-hits"></div><div id="search-pagination"></div></div></div></div></div>'
});
}
var isFetched = false;
var datas;
var isXml = true;
var current_page = 0;
var pageSize = parseInt(CONFIG.localSearch.pageSize, 10);
if(isNaN(pageSize)) pageSize = 10;
var total_pages = 0;
var max_page_on_show = 7; // 一次最多显示 7 个页码
var start_page = 0;
var end_page = 0;
var resultItems = [];
// search DB path
var searchPath = CONFIG.localSearch.path;
if (searchPath.length == 0) {
searchPath = 'search.xml';
} else if (searchPath.endsWith('json')) {
isXml = false;
}
const input = $('.search-input'); // document.querySelector('.search-input');
const resultContent = document.getElementById('search-hits');
const paginationContent = document.getElementById('search-pagination');
const getIndexByWord = function(word, text, caseSensitive) {
if (CONFIG.localSearch.unescape) {
var div = document.createElement('div');
div.innerText = word;
word = div.innerHTML;
}
var wordLen = word.length;
if (wordLen === 0) {
return [];
}
var startPosition = 0;
var position = [];
var index = [];
if (!caseSensitive) {
text = text.toLowerCase();
word = word.toLowerCase();
}
while ((position = text.indexOf(word, startPosition)) > -1) {
index.push({position:position, word:word});
startPosition = position + wordLen;
}
return index;
};
// Merge hits into slices
const mergeIntoSlice = function(start, end, index, searchText) {
var item = index[index.length - 1];
var position = item.position;
var word = item.word;
var hits = [];
var searchTextCountInSlice = 0;
while (position + word.length <= end && index.length !== 0) {
if (word === searchText) {
searchTextCountInSlice++;
}
hits.push({
position:position,
length: word.length
});
var wordEnd = position + word.length;
// Move to next position of hit
index.pop();
while (index.length !== 0) {
item = index[index.length - 1];
position = item.position;
word = item.word;
if (wordEnd > position) {
index.pop();
} else {
break;
}
}
}
return {
hits:hits,
start:start,
end:end,
searchTextCount: searchTextCountInSlice
};
}
// Highlight title and content
const highlightKeyword = function(text, slice) {
var result = '';
var prevEnd = slice.start;
slice.hits.forEach(function(hit) {
result += text.substring(prevEnd, hit.position);
var end = hit.position + hit.length;
result += '<mark>'+ text.substring(hit.position, end)+'</mark>';
prevEnd = end;
});
result += text.substring(prevEnd, slice.end);
return result;
};
const pagination = function() {
const addPrevPage = function(current_page) {
var classContent = '';
var numberContent = '';
if (current_page === 0) {
classContent = '#search-pagination pagination-item disabled-item';
numberContent = '<span class="#search-pagination page-number"><i class="ic i-angle-left"></i></span>';
} else {
classContent = '#search-pagination pagination-item';
numberContent = '<a class="#search-pagination page-number" aria-label="Prev" href="#"><i class="ic i-angle-left"></i></a>';
}
var prevPage = '<li class="'+ classContent +'" id="prev-page">'+ numberContent+'</li>';
return prevPage;
};
const addNextPage = function(current_page) {
var classContent = '';
var numberContent = '';
if ((current_page + 1) === total_pages) {
classContent = '#search-pagination pagination-item disabled-item';
numberContent = '<span class="#search-pagination page-number"><i class="ic i-angle-right"></i></span>';
} else {
classContent = '#search-pagination pagination-item';
numberContent = '<a class="#search-pagination page-number"aria-label="Next"href="#"><i class="ic i-angle-right"></i></a>';
}
var nextPage = '<li class="' + classContent +'"id="next-page">'+ numberContent +'</li>';
return nextPage;
};
const addPage = function(index, current_page) {
var classContent = '';
var numberContent = '<a class="#search-pagination page-number"aria-label="'+ (index + 1) +'"href="#">'+(index+1)+'</a>';
if (index === current_page) {
classContent = '#search-pagination pagination-item current';
} else {
classContent = '#search-pagination pagination-item';
}
var page = '<li class="'+classContent+'" id="page-'+(index + 1)+'">'+numberContent+'</li>';
return page;
}
const addPaginationEvents = function(start_page, end_page) {
if (total_pages <= 0) {
return;
}
const onPrevPageClick = function(event) {
if (current_page > 0) {
current_page -= 1;
}
if (current_page < start_page) {
start_page = current_page;
end_page = Math.min(end_page, start_page + max_page_on_show);
}
pagination();
};
const onNextPageClick = function(event) {
if ((current_page + 1) < total_pages) {
current_page += 1;
}
if (current_page > end_page) {
end_page = current_page;
start_page = Math.max(0, end_page - max_page_on_show);
}
pagination();
};
const onPageClick = function(event) {
var page_number = parseInt(event.target.ariaLabel);
current_page = page_number - 1; // note minus 1 here
pagination();
};
var prevPage = document.getElementById('prev-page');
if(prevPage != null)prevPage.addEventListener('click', onPrevPageClick);
var nextPage = document.getElementById('next-page');
if(nextPage != null) nextPage.addEventListener('click', onNextPageClick);
for (var i = start_page; i < end_page; i += 1) {
var page = document.getElementById('page-'+(i + 1));
if(page != null)page.addEventListener('click', onPageClick);
}
};
paginationContent.innerHTML = ''; // clear
var begin_index = Math.min(current_page * pageSize, resultItems.length);
var end_index = Math.min(begin_index + pageSize, resultItems.length);
resultContent.innerHTML = resultItems.slice(begin_index, end_index).map(function(result) {return result.item}).join('');
start_page = Math.max(0, total_pages - max_page_on_show);
end_page = start_page + Math.min(total_pages, max_page_on_show);
var pageContent = '<div class="#search-pagination">';
pageContent += '<div class="#search-pagination pagination">';
pageContent += '<ul>';
if (total_pages > 0) {
// add prev page arrow, when no prev page not selectable
pageContent += addPrevPage(current_page);
for (var i = start_page; i < end_page; i += 1) {
pageContent += addPage(i, current_page);
}
// add next page arrow, when no next page not selectable
pageContent += addNextPage(current_page);
}
pageContent += '</ul>';
pageContent += '</div>';
pageContent += '</div>';
paginationContent.innerHTML = pageContent;
addPaginationEvents(start_page, end_page);
resultContent.scrollTop = 0; // scroll to top
window.pjax && window.pjax.refresh(resultContent);
};
const inputEventFunction = function() {
if (!isFetched) {
console.log("Data not fetched.");
return;
}
var searchText = input.value.trim().toLowerCase();
var keywords = searchText.split(/[-\s]+/);
if (keywords.length > 1) {
keywords.push(searchText);
}
resultItems = [];
if (searchText.length > 0) {
// Perform local searching
datas.forEach(function(index) {
var categories = index.categories, title=index.title, content=index.content, url=index.url;
var titleInLowerCase = title.toLowerCase();
var contentInLowerCase = content.toLowerCase();
var indexOfTitle = [];
var indexOfContent = [];
var searchTextCount = 0;
keywords.forEach( function(keyword) {
indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false));
indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false));
});
// Show search results
if (indexOfTitle.length > 0 || indexOfContent.length > 0) {
var hitCount = indexOfTitle.length + indexOfContent.length;
// Sort index by position of keyword
[indexOfTitle, indexOfContent].forEach(function(index) {
index.sort(function(itemLeft, itemRight) {
if (itemRight.position !== itemLeft.position) {
return itemRight.position - itemLeft.position;
}
return itemLeft.word.length - item.word.length;
});
});
var slicesOfTitle = [];
if (indexOfTitle.length !== 0) {
var tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText);
searchTextCount += tmp.searchTextCountInSlice;
slicesOfTitle.push(tmp);
}
var slicesOfContent = [];
while (indexOfContent.length !== 0) {
var item = indexOfContent[indexOfContent.length - 1];
var position = item.position;
var word = item.word;
// Cut out 100 characters
var start = position - 20;
var end = position + 30;
if (start < 0) {
start = 0;
}
if (end < position + word.length) {
end = position + word.length;
}
if (end > content.length) {
end = content.length;
}
var tmp = mergeIntoSlice(start, end, indexOfContent, searchText);
searchTextCount += tmp.searchTextCountInSlice;
slicesOfContent.push(tmp);
}
// Sort slices in content by search text's count and hits' count
slicesOfContent.sort( function(sliceLeft, sliceRight) {
if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) {
return sliceRight.searchTextCount - sliceLeft.searchTextCount;
} else if (sliceLeft.hits.length !== sliceRight.hits.length) {
return sliceRight.hits.length - sliceLeft.hits.length;
}
return sliceLeft.start - sliceRight.start;
});
// Select top N slices in content
var upperBound = parseInt(CONFIG.localSearch.pageSize, 10);
if (upperBound >= 0) {
slicesOfContent = slicesOfContent.slice(0, upperBound);
}
var resultItem = '';
resultItem += '<div class="#search-hits item">';
// resultItem += '<div class="#search-hits">';
// resultItem += '<ol class="item">'
resultItem += '<li>'
// resultItem += '<li>';
var cats = categories !== undefined ? '<span>' + categories.join('<i class="ic i-angle-right"></i>') + '</span>' : '<span>No categories</span>';
resultItem += '<a href="'+url+'">' + cats;
if (slicesOfTitle.length !== 0) {
// resultItem += '<li><a href="'+url}">'+highlightKeyword(title, slicesOfTitle[0])}</a>';
resultItem += '<b>'+highlightKeyword(title, slicesOfTitle[0])+'</b><br>';
} else {
// resultItem += '<li><a href="'+url}">'+title}</a>';
resultItem += '<b>'+title+'</b><br>';
}
slicesOfContent.forEach(function(slice) {
return resultItem += '<li class="#search-hits subitem">'+highlightKeyword(content, slice)+' ...</li>';
});
// resultItem += '</li>';
resultItem += '</a>';
resultItem += '</li>';
// resultItem += '</ol>';
resultItem += '</div>';
resultItems.push({
item: resultItem,
id : resultItems.length,
hitCount:hitCount,
searchTextCount:searchTextCount
});
}
});
}
if (keywords.length === 1 && keywords[0] === '') {
resultContent.innerHTML = '<div id="no-result"><i></i></div>';
} else if (resultItems.length === 0) {
resultContent.innerHTML = '<div id="no-result"><i></i></div>';
} else {
resultItems.sort(function(resultLeft, resultRight) {
if (resultLeft.searchTextCount !== resultRight.searchTextCount) {
return resultRight.searchTextCount - resultLeft.searchTextCount;
} else if (resultLeft.hitCount !== resultRight.hitCount) {
return resultRight.hitCount - resultLeft.hitCount;
}
return resultRight.id - resultLeft.id;
});
}
// Do pagination
total_pages = Math.ceil(resultItems.length / pageSize);
pagination();
}
const fetchData = function() {
fetch(CONFIG.root + searchPath)
.then(function(response) {return response.text()} )
.then( function(res) {
// Get the contents from search data
isFetched = true;
datas = isXml ? [new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map( function(element) {
return {
title : element.querySelector('title').textContent,
content: element.querySelector('content').textContent,
url : element.querySelector('url').textContent
};
}) : JSON.parse(res);
// Only match articles with not empty titles
datas = datas.filter(function(data) {return data.title} ).map( function(data) {
data.title = data.title.trim();
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '';
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
return data;
});
// Remove loading animation
document.getElementById('search-hits').innerHTML = '<i></i>';
inputEventFunction();
});
};
if (CONFIG.localSearch.preload) {
console.log("fetch data.");
fetchData();
}
if (CONFIG.localSearch.trigger === 'auto') {
input.addEventListener('input', inputEventFunction);
} else {
document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
input.addEventListener('keypress',function(event) {
if (event.key === 'Enter') {
inputEventFunction();
}
});
}
// Handle and trigger popup window
document.querySelectorAll('.popup-trigger').forEach( function(element) {
element.addEventListener('click', function() {
document.body.style.overflow = 'hidden';
document.querySelector('.search-pop-overlay').classList.add('search-active');
input.focus();
if (!isFetched) fetchData();
});
});
// Handle and trigger popup window
$.each('.search', function(element) {
element.addEventListener('click', function() {
document.body.style.overflow = 'hidden';
transition(siteSearch, 'shrinkIn', function() {
$('.search-input').focus();
}) // transition.shrinkIn
});
});
// Monitor main search box
const onPopupClose = function() {
document.body.style.overflow = '';
transition(siteSearch, 0); // "transition.shrinkOut"
};
siteSearch.addEventListener('click', function(event) {
if (event.target === siteSearch) {
onPopupClose();
}
});
$('.close-btn').addEventListener('click', onPopupClose);
window.addEventListener('pjax:success', onPopupClose);
window.addEventListener('keyup', function(event) {
if (event.key === 'Escape') {
onPopupClose();
}
});
};

修改 script.js#

shoka/scripts/generaters/script.js 中主要是读取配置,添加如下代码

........... // 省略若干代码
if(config.algolia) {
siteConfig.search = {
appID : config.algolia.appId,
apiKey : config.algolia.apiKey,
indexName: config.algolia.indexName,
hits : theme.search.hits
}
}
// 以下为需要添加的代码
if(config.search) {
siteConfig.localSearch = {
enable: config.search.enable,
path: config.search.path,
field: config.search.field,
format: config.search.format,
limit: config.search.limit,
content: config.search.content,
unescape: config.search.unescape,
preload: config.search.preload,
trigger: config.search.trigger,
pageSize: config.search.pageSize
}
}

修改 pjax.js#

shoka/source/js/_app/pjax.js 中是启动搜索功能的部分,这里在两个配置都有的情况下默认使用本地搜索而不是 Algolia

if (CONFIG.localSearch != null) {
localSearch(pjax)
}else if(CONFIG.search != null) {
algoliaSearch(pjax)
}

添加配置#

最后在 hexo 配置(最外层的 _config.yml )中添加 search 配置,就大功告成了!

search:
enable: true
path: search.json # search.xml
field: post
format: html
limit: 10000
content: true
unescape: true
preload: true
trigger: "auto"
pageSize: 10

看下效果吧#

image-20240522172507594
image-20240522172507594

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Hexo Shoka 主题本地搜索
https://blog.huhaha.vip/posts/e4550ecb/
作者
小王的博客
发布于
2024-05-22
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
Hexo主题安装
Hexo如何在Hexo博客框架中安装和配置多个热门主题,包括Butterfly、yun、Fluid和shoka。以shoka主题为例,详细说明了下载、配置主题的步骤,以及安装必要插件的具体操作。最后,提供了检查主题是否工作正常的步骤,确保读者能够顺利使用这些主题。
2
Hexo基本使用
HexoHexo的基本使用方法,Hexo是一个快速、简洁且高效的博客框架。它使用Markdown(或其他标记语言)解析文章,并在几秒内生成静态网页。文章详细讲解了如何安装和配置Hexo,以及如何使用不同的主题和插件来丰富博客的功能和外观。适合初学者快速上手Hexo,搭建自己的个人博客。
3
Github+picGo+Typora搭建图床
Hexo本文《Github+picGo+Typora搭建图床》详细介绍了如何使用Github、PicGo和Typora搭建个人图床的过程。如何在Vscode中安装Cnblogs插件,以便将写好的博客上传到博客园。整个过程步骤清晰,适合初学者参考和实践。
4
安装主题
Hexo如果能给我一个star那将是对我莫大的鼓励。使用这个主题之前,你应该明白它是一个 Hexo 主题,它的基本逻辑离不开 Hexo,关于如何新建分类,如何新建标签这些问题应该在使用之前就从互联网或官方文档了解详情 。安知鱼主题 是基于 Hexo 的一款 简单、美丽 的主题,由 安知鱼 负责开发与维护。
5
音乐馆页配置
Hexo如果能给我一个star那将是对我莫大的鼓励。使用这个主题之前,你应该明白它是一个 Hexo 主题,它的基本逻辑离不开 Hexo,关于如何新建分类,如何新建标签这些问题应该在使用之前就从互联网或官方文档了解详情 🔎。安知鱼主题 是基于 Hexo 的一款 简单、美丽 的主题,由 安知鱼 负责开发与维护。
随机文章随机推荐

评论区

Profile Image of the Author
小王的博客
一个上了年纪的猿人.
分类
标签
最新动态
站点统计
文章
209
分类
15
标签
161
总字数
478,322
运行时长
0
最后活动
0 天前
站点信息
构建平台
Vercel
博客版本
Firefly v6.16.7
文章许可
CC BY-NC-SA 4.0
文章目录