Hexo+Next主题集成Algolia搜索

前言

本来这个网站是没有做搜索功能的,因为各种解决方案都不是很完美,而且现在文章也不多,就干脆没有做。偶然间看到了Next主题作者iissnan的个人网站的搜索功能不仅速度很快而且也比较符合我的审美,就研究了一下。写下此文章作为记录。

Algolia是一家致力于为用户提供毫秒级的数据库搜索服务的法国初创公司。其初衷是致力于让客户可以获得“100ms”等级的实时搜索服务。

本文要实现的效果如下:

更新

如果你是使用的Next主题,其5.1以上版本已经集成该功能(Github Branch目前最新版本为5.1)。

激活方法

在站点配置文件_config.yml中写入

1
2
3
4
5
6
algolia:
applicationID: 'applicationID'
apiKey: 'apiKey'
adminApiKey: 'adminApiKey'
indexName: 'indexName'
chunkSize: 5000

在主题配置文件_config.yml中写入

1
2
algolia_search:
enable: true

Enjoy :)

上传数据到Algolia数据库

注册帐号并获得需要的信息

进入官网地址注册帐号

右上角NEW INDEX如图所示

来到API-KEYS页面,这里有我们待会儿需要的信息(还有上面的INDEX名)。

安装hexo-algoliasearch插件并填写配置信息

在Hexo的根目录下执行

1
npm install hexo-algoliasearch --save

在根目录的站点配置文件_config.yml中加入如下配置,注意改成前面第一步注册获得的数据

1
2
3
4
5
6
7
8
9
10
algolia:
appId: 'appId'
apiKey: 'apiKey'
adminApiKey: 'adminApiKey'
indexName: 'indexName'
chunkSize: 5000
fields:
- title
- slug
- content:strip

接着执行,确保得到提交成功提示

1
hexo algolia

如下图

然后在Algolia后台的Indices看是否有我们网站的文章信息,如果有就说明我们的数据提交成功了。

Tips:如果无法运行成功,可以试着先执行如下命令:

1
hexo clean

主题集成Algolia

新建algolia.swig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="site-search">
<div class="algolia-popup popup">
<div class="algolia-search">
<div class="algolia-search-input-icon">
<i class="fa fa-search"></i>
</div>
<div class="algolia-search-input" id="algolia-search-input"></div>
</div>
<div class="algolia-results">
<div id="algolia-stats"></div>
<div id="algolia-hits"></div>
<div id="algolia-pagination" class="algolia-pagination"></div>
</div>
<span class="popup-btn-close">
<i class="fa fa-times-circle"></i>
</span>
</div>
</div>
<script src="//cdn.bootcss.com/instantsearch.js/1.7.1/instantsearch.min.js"></script>

引入algolia.swig

search.swig引入algolia.swig

1
2
{% elseif theme.algolia_search %}
{% include 'search/algolia.swig' %}

由于hexoTemplate renderswig部分语法冲突,所以涉及到swig的部分代码用图片代替。

注意将上面这段代码插入endif之前(如果你需要保留其他搜索接口)。

添加触发节点

在要触发搜索的HTML节点加入一个CLASS名为popup-trigger

1
2
3
4
5
6
{% raw %}
{% elseif theme.algolia_search %}
<a href="#" class="popup-trigger">
{{ __('menu.search') }}
</a>
{% endraw %}

Tips:请不要将以下代码复制,因为hexo的模板引擎会将中的内容当做模板渲染,下面的代码是为了告诉它不要将content里的内容当做模板渲染,详情见此

1
2
3
4
5
{% raw %}
//content
{% endraw %}

在这里我们将以上代码加入到header.swigmenu里面(同样插入endif之前)。

注意header.swig里面有这样一段代码:

1
{% set hasSearch = theme.swiftype_key || theme.tinysou_Key || config.search %}

改为

1
{% set hasSearch = theme.swiftype_key || theme.tinysou_Key || config.search || theme.algolia_search %}

新建search.js

新建search.js,注意需要将第一步注册得到的信息写入头部的CONFIC对象。

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
115
116
117
118
119
120
121
$(document).ready(function() {
var CONFIG = {
root: '/',
algolia: {
applicationID: 'applicationID',
apiKey: 'apiKey',
indexName: 'indexName',
hits: { "per_page": 10 },
labels: { "input_placeholder": "Searching...", "hits_empty": "未发现与 「${query}」相关的内容", "hits_stats": "${hits} 条相关条目,使用了 ${time} 毫秒" }
}
};
var algoliaSettings = CONFIG.algolia;
var isAlgoliaSettingsValid = algoliaSettings.applicationID &&
algoliaSettings.apiKey &&
algoliaSettings.indexName;
if (!isAlgoliaSettingsValid) {
window.console.error('Algolia Settings are invalid.');
return;
}
var search = instantsearch({
appId: algoliaSettings.applicationID,
apiKey: algoliaSettings.apiKey,
indexName: algoliaSettings.indexName,
searchFunction: function(helper) {
var searchInput = $('#algolia-search-input').find('input');
if (searchInput.val()) {
helper.search();
}
}
});
// Registering Widgets
[
instantsearch.widgets.searchBox({
container: '#algolia-search-input',
placeholder: algoliaSettings.labels.input_placeholder
}),
instantsearch.widgets.hits({
container: '#algolia-hits',
hitsPerPage: algoliaSettings.hits.per_page || 10,
templates: {
item: function(data) {
return (
'<a href="' + CONFIG.root + data.slug + '" class="algolia-hit-item-link">' +
data._highlightResult.title.value +
'</a>'
);
},
empty: function(data) {
return (
'<div id="algolia-hits-empty">' +
algoliaSettings.labels.hits_empty.replace(/\$\{query}/, data.query) +
'</div>'
);
}
},
cssClasses: {
item: 'algolia-hit-item'
}
}),
instantsearch.widgets.stats({
container: '#algolia-stats',
templates: {
body: function(data) {
var stats = algoliaSettings.labels.hits_stats
.replace(/\$\{hits}/, data.nbHits)
.replace(/\$\{time}/, data.processingTimeMS);
return (
stats +
'<span class="algolia-powered">' +
' <img src="' + CONFIG.root + 'images/algolia_logo.svg" alt="Algolia" />' +
'</span>' +
'<hr />'
);
}
}
}),
instantsearch.widgets.pagination({
container: '#algolia-pagination',
scrollTo: false,
showFirstLast: false,
labels: {
first: '<i class="fa fa-angle-double-left"></i>',
last: '<i class="fa fa-angle-double-right"></i>',
previous: '<i class="fa fa-angle-left"></i>',
next: '<i class="fa fa-angle-right"></i>'
},
cssClasses: {
root: 'pagination',
item: 'pagination-item',
link: 'page-number',
active: 'current',
disabled: 'disabled-item'
}
})
].forEach(search.addWidget, search);
search.start();
$('.popup-trigger').on('click', function(e) {
e.stopPropagation();
$('body').append('<div class="popoverlay">').css('overflow', 'hidden');
$('.popoverlay').fadeIn(300);
$('.popup').fadeIn(300);
$('#algolia-search-input').find('input').focus();
});
$('.popup-btn-close').click(function() {
$('.popoverlay').fadeOut(300);
$('.popup').fadeOut(300);
$('.popoverlay').remove();
$('body').css('overflow', '');
});
});

新建search.css

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
ul.search-result-list {
padding-left: 0px;
margin: 0px 5px 0px 8px;
}
p.search-result {
border-bottom: 1px dashed #ccc;
padding: 5px 0;
}
a.search-result-title {
font-weight: bold;
}
a.search-result {
border-bottom: transparent;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-keyword {
border-bottom: 1px dashed #4088b8;
font-weight: bold;
}
#local-search-result {
height: 90%;
overflow: auto;
}
.popup {
display: none;
position: fixed;
top: 10%;
left: 50%;
width: 700px;
height: 80%;
margin-left: -350px;
padding: 3px 0 0 10px;
background: #fff;
color: #333;
z-index: 9999;
border-radius: 0px;
}
@media (max-width: 767px) {
.popup {
padding: 3px;
top: 0;
left: 0;
margin: 0;
width: 100%;
height: 100%;
border-radius: 0px;
}
}
.popoverlay {
display: none;
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
z-index: 2080;
background-color: rgba(0,0,0,0.3);
}
#local-search-input {
margin-bottom: 10px;
width: 50%;
}
.popup-btn-close {
position: absolute;
top: 6px;
right: 14px;
color: #4ebd79;
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
}
#no-result {
position: absolute;
left: 44%;
top: 42%;
color: #ccc;
}
.busuanzi-count:before {
content: " ";
float: left;
width: 260px;
min-height: 25px;
}
@media (min-width: 768px) and (max-width: 991px) {
.busuanzi-count {
width: auto;
}
.busuanzi-count:before {
display: none;
}
}
@media (max-width: 767px) {
.busuanzi-count {
width: auto;
}
.busuanzi-count:before {
display: none;
}
}
.site-uv,
.site-pv,
.page-pv {
display: inline-block;
}
.site-uv .busuanzi-value,
.site-pv .busuanzi-value,
.page-pv .busuanzi-value {
margin: 0 5px;
}
.site-uv {
margin-right: 10px;
}
.site-uv::after {
content: "|";
padding-left: 10px;
}
.algolia-popup {
overflow: hidden;
padding: 0;
}
.algolia-popup .popup-btn-close {
padding-left: 15px;
border-left: 1px solid #eee;
top: 10px;
}
.algolia-popup .popup-btn-close .fa {
color: #999;
font-size: 18px;
}
.algolia-popup .popup-btn-close:hover .fa {
color: #222;
}
.algolia-search {
padding: 10px 15px 5px;
max-height: 50px;
background: #f5f5f5;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.algolia-search-input-icon {
display: inline-block;
width: 20px;
}
.algolia-search-input-icon .fa {
font-size: 18px;
}
.algolia-search-input {
display: inline-block;
width: calc(90% - 20px);
}
.algolia-search-input input {
padding: 5px 0;
width: 100%;
outline: none;
border: none;
background: transparent;
}
.algolia-powered {
float: right;
}
.algolia-powered img {
display: inline-block;
height: 18px;
vertical-align: middle;
}
.algolia-results {
position: relative;
overflow: auto;
padding: 10px 30px;
height: calc(100% - 50px);
}
.algolia-results hr {
margin: 10px 0;
}
.algolia-results .highlight {
font-style: normal;
margin: 0;
padding: 0 2px;
font-size: inherit;
color: #f00;
}
.algolia-hits {
margin-top: 20px;
}
.algolia-hit-item {
margin: 15px 0;
}
.algolia-hit-item-link {
display: block;
border-bottom: 1px dashed #ccc;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
transition-delay: 0s;
}
.algolia-pagination .pagination {
margin-top: 40px;
border-top: none;
padding: 0;
text-align: center;
}
.algolia-pagination .pagination-item {
display: inline-block;
}
.algolia-pagination .page-number {
border-top: none;
}
.algolia-pagination .page-number:hover {
border-bottom: 1px solid #222;
}
.algolia-pagination .disabled-item {
visibility: hidden;
}
.fa-search::before{
color: #999999;
}

将上述search.jssearch.css文件引入footer.swig文件

1
2
<link href="{{ url_for(theme.css) }}/search.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="{{ url_for(theme.js) }}/search.js"></script>

将下面这张图片拷贝到source目录的images目录下

至此,我们的工作就完成了。