0%

Hexo评论功能的实现:Gitalk

需求: 1、用Gitalk实现评论功能 2、去除之前的Valine实现的评论功能

可在主题配置文件搜索comment system,查看支持的评论系统;个人使用Gitalk进行配置;

Gitalk评论功能

1、注册OAuth application

  1. 在 github 中进行注册,进入 https://github.com/settings/profile
  2. 点击左侧 Developer settings
  3. Register a new application

1
2
3
4
Application name:	#应用名称
Homepage URL: #网站URL(填自己的博客主页地址)
Application description #描述
Authorization callback URL: #网站URL(填自己的博客主页地址)
  1. 注册完成之后,会得到:Client IDClient Secret[1]

2、新建存放博客评论仓库

可以在 github 中建一个项目,专门用来存储你的博客评论

3、配置 Next 主题文件

编辑主题配置文件:themes\next\ _config.yml,找到有关 gitalk的相关配置进行填写:

1
2
3
4
5
6
7
8
9
10
gitalk:
enable: true 开启gitalk评论,不需要配置
owner: github用户名
admin: github用户名
repo: 博客的仓库名称(注意不是地址)
ClientID: 上面生成的Client ID
ClientSecret: 上面生成的Client Secret
labels: 'gitalk' github issue 对应的issue标签(新建一个)
distractionFreeMode: true 无干扰模式,不需要更改

这是我的配置:

进入到 themes\next\layout\post.swig(我的博客是基于 Next,如果有差异,替换路径中的 next 即可),添加 gitalk 模板文件的导入[2]

1
2
3
4
<!-- {### Line 357,如果行数有差异,只需要在 POST END 文章结束后添加即可 ###} -->  
{% if theme.git_talk.enabled and not is_index %}
<div>{% include 'git-talk.swig' %}</div>
{% endif %}

然后添加 git-talk.swig 文件(themes\next\layout\git-talk.swig),文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css" />  
<script src="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js"></script>

<div id="gitalk-container"></div>

<script type="text/javascript">
var gitalk = new Gitalk({
clientID: "{{theme.git_talk.clientID}}",
clientSecret: "{{theme.git_talk.clientSecret}}",
repo: "eminoda.github.io", // 博客仓库地址
owner: "eminoda", // github 用户名
admin: ["eminoda"], // github 用户名
perPage: 20,
id: location.pathname.slice(0, 50), // 查找 issus 的条件,后面将对 id 有针对逻辑
title: "{{page.title}}",
body: "🚀 " + location.href + "\n\n欢迎通过 issues 留言 ,互相交流学习😊", // 初始化后,issues 的内容
});
gitalk.render("gitalk-container");
</script>

以上操作完成后,打开文章即可看到以下页面,需要登录github账号初始化;每篇文章都需要进行登录初始化才可以使用;

未登录
初始化后

4、全部文章批量初始化Issues

对于一个刚起步的博客站点没有任何问题,新增一篇文章,初始化下issue,顺手的事情。

但对于一个历史站点,里面可能有百篇文章,如果希望看到别人阅读的回复,则需要人工每篇进行初始化,不太现实,则需要程序来批量初始化。[2]

4.1 开启 OAuth 认证

需要在 Developer Setting 开启 Personal access tokens[3]

4.2 安装项目依赖
1
npm i request xml-parser blueimp-md5 moment hexo-generator-sitemap -S

需要的包:request、xml-parser、 blueimp-md5、 moment、 hexo-generator-sitemap

4.3 修改 hexo-generator-sitemap 配置

项目根目录配置文件 _config.yml 添加配置[5]

1
2
3
4
5
6
7
#Sitemap
sitemap:
path: sitemap.xml
template: ./sitemap_template.xml
rel: false
tag: true
category: false

项目根目录新建文件 sitemap_template.xml ,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>  
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for post in posts %}
<url>
<loc>{{ post.permalink | uriencode }}</loc>
{% if post.updated %}
<lastmod>{{ post.updated.toISOString() }}</lastmod>
{% elif post.date %}
<lastmod>{{ post.date.toISOString() }}</lastmod>
{% endif %}
<date>{{ post.date }}</date>
<title>{{ post.title + ' | ' + config.title }}</title>
{# nunjucks 模版语法 https://github.com/mozilla/nunjucks #}
<desc>{{ post.description | default(post.excerpt) | default(post.content) | default(config.description) | striptags | truncate(200, true, '') }}</desc>
</url>
{% endfor %}
</urlset>
4.4 执行 hexo generate 命令,生成 sitemap
1
npm run build

此命令执行成功之后, public 目录下应该有生成 sitemap.xml 文件,如果没有此文件,请检查包是否安装成功。

4.5 添加自动初始化程序

项目根目录新建文件 talk-auto-init.js ,内容如下[6]

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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
const fs = require('fs');
const path = require('path');
const url = require('url');

const request = require('request');
const xmlParser = require('xml-parser');
const md5 = require('md5');

// 配置信息
const config = {
username: 'toimc', // GitHub repository 所有者,可以是个人或者组织。对应Gitalk配置中的owner
repo: "toimc.github.io", // 储存评论issue的github仓库名,仅需要仓库名字即可。对应 Gitalk配置中的repo
token: 'xxxxxx', // 前面申请的 personal access token
sitemap: path.join(__dirname, './public/sitemap.xml'), // 自己站点的 sitemap 文件地址
cache: true, // 是否启用缓存,启用缓存会将已经初始化的数据写入配置的 gitalkCacheFile 文件,下一次直接通过缓存文件判断
gitalkCacheFile: path.join(__dirname, './gitalk-init-cache.json'), // 用于保存 gitalk 已经初始化的 id 列表
gitalkErrorFile: path.join(__dirname, './gitalk-init-error.json'), // 用于保存 gitalk 初始化报错的数据
};

const api = 'https://api.github.com/repos/' + config.username + '/' + config.repo + '/issues';

/**
* 读取 sitemap 文件
* 远程 sitemap 文件获取可参考 https://www.npmjs.com/package/sitemapper
*/
const sitemapXmlReader = (file) => {
try {
const data = fs.readFileSync(file, 'utf8');
const sitemap = xmlParser(data);
let ret = [];
sitemap.root.children.forEach(function (url) {
const loc = url.children.find(function (item) {
return item.name === 'loc';
});
if (!loc) {
return false;
}
const title = url.children.find(function (item) {
return item.name === 'title';
});
const desc = url.children.find(function (item) {
return item.name === 'desc';
});
const date = url.children.find(function (item) {
return item.name === 'date';
});
ret.push({
url: loc.content,
title: title.content,
desc: desc.content,
date: date.content,
});
});
return ret;
} catch (e) {
return [];
}
};

// 获取 gitalk 使用的 id
const getGitalkId = ({
url: u,
date
}) => {
const link = url.parse(u);
// 链接不存在,不需要初始化
if (!link || !link.pathname) {
return false;
}
if (!date) {
return false;
}
return md5(link.pathname);
};

/**
* 通过以请求判断是否已经初始化
* @param {string} gitalk 初始化的id
* @return {[boolean, boolean]} 第一个值表示是否出错,第二个值 false 表示没初始化, true 表示已经初始化
*/
const getIsInitByRequest = (id) => {
const options = {
headers: {
'Authorization': 'token ' + config.token,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
'Accept': 'application/json'
},
url: api + '?labels=' + id + ',Gitalk',
method: 'GET'
};
return new Promise((resolve) => {
request(options, function (err, response, body) {
if (err) {
return resolve([err, false]);
}
if (response.statusCode != 200) {
return resolve([response, false]);
}
const res = JSON.parse(body);
if (res.length > 0) {
return resolve([false, true]);
}
return resolve([false, false]);
});
});
};

/**
* 通过缓存判断是否已经初始化
* @param {string} gitalk 初始化的id
* @return {boolean} false 表示没初始化, true 表示已经初始化
*/
const getIsInitByCache = (() => {
// 判断缓存文件是否存在
let gitalkCache = false;
try {
gitalkCache = require(config.gitalkCacheFile);
} catch (e) {}
return function (id) {
if (!gitalkCache) {
return false;
}
if (gitalkCache.find(({
id: itemId
}) => (itemId === id))) {
return true;
}
return false;
};
})();

// 根据缓存,判断链接是否已经初始化
// 第一个值表示是否出错,第二个值 false 表示没初始化, true 表示已经初始化
const idIsInit = async (id) => {
if (!config.cache) {
return await getIsInitByRequest(id);
}
// 如果通过缓存查询到的数据是未初始化,则再通过请求判断是否已经初始化,防止多次初始化
if (getIsInitByCache(id) === false) {
return await getIsInitByRequest(id);
}
return [false, true];
};

// 初始化
const gitalkInit = ({
url,
id,
title,
desc
}) => {
//创建issue
const reqBody = {
'title': title,
'labels': [id, 'Gitalk'],
'body': url + '\r\n\r\n' + desc
};

const options = {
headers: {
'Authorization': 'token ' + config.token,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
'Accept': 'application/json',
'Content-Type': 'application/json;charset=UTF-8'
},
url: api,
body: JSON.stringify(reqBody),
method: 'POST'
};
return new Promise((resolve) => {
request(options, function (err, response, body) {
if (err) {
return resolve([err, false]);
}
if (response.statusCode != 201) {
return resolve([response, false]);
}
return resolve([false, true]);
});
});
};


/**
* 写入内容
* @param {string} fileName 文件名
* @param {string} content 内容
*/
const write = async (fileName, content, flag = 'w+') => {
return new Promise((resolve) => {
fs.open(fileName, flag, function (err, fd) {
if (err) {
resolve([err, false]);
return;
}
fs.writeFile(fd, content, function (err) {
if (err) {
resolve([err, false]);
return;
}
fs.close(fd, (err) => {
if (err) {
resolve([err, false]);
return;
}
});
resolve([false, true]);
});
});
});
};

const init = async () => {
const urls = sitemapXmlReader(config.sitemap);
// 报错的数据
const errorData = [];
// 已经初始化的数据
const initializedData = [];
// 成功初始化数据
const successData = [];
for (const item of urls) {
const {
url,
date,
title,
desc
} = item;
const id = getGitalkId({
url,
date
});
if (!id) {
console.log(`id: 生成失败 [ ${id} ] `);
errorData.push({
...item,
info: 'id 生成失败',
});
continue;
}
const [err, res] = await idIsInit(id);
if (err) {
console.log(`Error: 查询评论异常 [ ${title} ] , 信息:`, err || '无');
errorData.push({
...item,
info: '查询评论异常',
});
continue;
}
if (res === true) {
// console.log(`--- Gitalk 已经初始化 --- [ ${title} ] `);
initializedData.push({
id,
url,
title,
});
continue;
}
console.log(`Gitalk 初始化开始... [ ${title} ] `);
const [e, r] = await gitalkInit({
id,
url,
title,
desc
});
if (e || !r) {
console.log(`Error: Gitalk 初始化异常 [ ${title} ] , 信息:`, e || '无');
errorData.push({
...item,
info: '初始化异常',
});
continue;
}
successData.push({
id,
url,
title,
});
console.log(`Gitalk 初始化成功! [ ${title} ] - ${id}`);
continue;
}

console.log(''); // 空输出,用于换行
console.log('--------- 运行结果 ---------');
console.log(''); // 空输出,用于换行

if (errorData.length !== 0) {
console.log(`报错数据: ${errorData.length} 条。参考文件 ${config.gitalkErrorFile}。`);
await write(config.gitalkErrorFile, JSON.stringify(errorData, null, 2));
}

console.log(`本次成功: ${successData.length} 条。`);

// 写入缓存
if (config.cache) {
console.log(`写入缓存: ${(initializedData.length + successData.length)} 条,已初始化 ${initializedData.length} 条,本次成功: ${successData.length} 条。参考文件 ${config.gitalkCacheFile}。`);
await write(config.gitalkCacheFile, JSON.stringify(initializedData.concat(successData), null, 2));
} else {
console.log(`已初始化: ${initializedData.length} 条。`);
}
};

init();

以上代码需改动的地方:

修改博客根目录下的package.json,新增命令:

1
2
3
"scripts": {  
"talk": "node talk-auto-init.js"
},

注意观察文件格式,若放在最后一个,前面需要一个逗号,个人配置如下:

项目的 package.json 是配置和描述如何与程序交互和运行的中心。[4]

4.6 执行命令
1
npm run talk

若出现以下情况,则成功啦:

4.7 命令合并

修改 package.json 中的 build 命令,将自动初始化添加到 build 之后,这样每次执行 build 命令就会自动执行初始化命令。

1
2
3
"scripts": {  
"build": "hexo generate && node talk-auto-init.js"
},

去除valine评论系统:

编辑themes\next\ _config.yml文件:将enable选项改为false即可

某个页面要不要评论

可以单独关闭某个页面的评论,在页面的 Front-matter 中添加 comments 字段,设为 false。比如标签页不想要评论,则在标签页面中做如下设置[7]

1
2
3
4
title: xxxxxxxxx
date: 2022-03-06 17:05:24
type: "tags"
comments: false

报错及解决

修改package.json少了个逗号;

原因:talk-auto-init.js有误,之前借鉴的是这篇文章:hexo gitalk 评论自动初始化里的talk-auto-init.js,造成错误,适用于我的是这篇文章的talk-auto-init.jshexo主题next中gitalk配置与评论初始化本文贴出的也是这篇talk-auto-init.js

解决方案:修改 talk-auto-init.js

修改之后还是有一些成功了,有一些还是报错,直接删除用第一篇的文章的talk-auto-init.js,用第二篇文章的talk-auto-init.js,再改个人配置即可;

参考文章

+