hexo主题next中gitalk配置与评论初始化

本文的环境是hexo 5.x版本,next 8.x主题,介绍在next主题中如何配置gitalk与评论初始化,缓存issues初始化结果,加速初始化过程,解决问题:“未找到相关的 Issues 进行评论,请联系xxx初始化创建”和“Request failed with status code 403”。

基本的流程:

  • 申请Github账号 —— OAuth应用
  • 配置gitalk
  • 申请Github - Token
  • 配置自动化脚本
  • 配置nginx(解决403问题)
  • 测试

next配置gitalk

在next项目_config.yml中,已经有明确的配置项:

第一个地方,需要注意配置gitalk与其他评论工具的顺序以及默认的评论工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Multiple Comment System Support
comments:
# Available values: tabs | buttons
style: tabs
# Choose a comment system to be displayed by default.
# Available values: disqus | disqusjs | changyan | livere | gitalk | utterances
active: gitalk
# Setting `true` means remembering the comment system selected by the visitor.
storage: true
# Lazyload all comment systems.
lazyload: false
# Modify texts or order for any navs, here are some examples.
nav:
gitalk:
order: -2
disqusjs:
text: Load Disqus
order: -1

第二个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Gitalk
# For more information: https://gitalk.github.io
gitalk:
enable: true
github_id: toimc # GitHub repo owner
repo: toimc.github.io # Repository name to store issues
client_id: c2d14f07d902bf339af2 # GitHub Application Client ID
client_secret: a41a05429a0ca340152b13fccx48245674437ba7 # GitHub Application Client Secret
admin_user: toimc # GitHub repo owner and collaborators, only these guys can initialize gitHub issues
distraction_free_mode: true # Facebook-like distraction free mode
# When the official proxy is not available, you can change it to your own proxy address
proxy: /login/oauth/access_token # This is official proxy adress
# proxy: https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token # This is official proxy adress
# Gitalk's display language depends on user's browser or system environment
# If you want everyone visiting your site to see a uniform language, you can set a force language value
# Available values: en | es-ES | fr | ru | zh-CN | zh-TW
language: zh-CN

上面的配置项中有几个需要说明的:

  • client_idclient_secret是Github 的 OAuth 认证(下面会介绍)
  • github_idadmin_user这里建议填一样,填成自己的github账号名(不是邮箱)
  • proxy默认即是上面的地址,它其实会回调到这里https://github.com/login/oauth/access_token来,大家出403的问题,主要是由于跨域。

Github的OAuth 认证

前提是自己得有一个github账号,才能注册OAuth application,OAuth应用注册地址

gitalk自动初始化

原理:通过sitemap中的信息,请求github开放api达到自动产生issues的目的

基本的要求:github API需要请求token

申请github Token

我们需要使用 Personal access tokens 方式,这种方式限制每小时 5000 次,结合缓存功能,基本满足需求。

从 Github 的 Personal access tokens 页面,点击 Generate new token

安装npm依赖项

1
2
3
4
5
"hexo-generator-sitemap": "^2.1.0",

"md5": "^2.3.0",
"request": "^2.88.2",
"xml-parser": "^1.2.1"

安装命令:

1
2
3
npm i -D md5 moment request xml-parser

npm i -S hexo-generator-sitemap

配置sitemap

在根目录中创建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>

修改根_config.yml

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

生成sitemap.xml文件

1
2
hexo clean
hexo generate

根目录添加talk-auto-init.js

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();

修改scripts中的脚本,添加"talk": "node talk-auto-init.js"

1
2
3
4
5
6
"scripts": {
"start": "hexo clean && hexo g && hexo s",
"publish": "hexo g && hexo s",
"d": "npm run prod && hexo deploy && npm run talk",
"talk": "node talk-auto-init.js"
},

测试

1
npm run talk

已经缓存过后的结果:

1
2
3
4
5
6
7
8
9
10
➜ npm run talk

> hexo-site@2.0.0 talk /Users/macos/Projects/hexo-blog
> node talk-auto-init.js


--------- 运行结果 ---------

本次成功: 0 条。
写入缓存: 73 条,已初始化 73 条,本次成功: 0 条。参考文件 ~/gitalk-init-cache.json。

403错误

nginx解决方案(推荐)

在nginx的博客配置中加入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 443 ssl http2;
// ...

location = /login/oauth/access_token {
add_header Access-Control-Allow-Origin 'https://www.toimc.com'; // 这里改成你自己的域名,并删除注释
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass https://github.com;
}
}

自建一个workers

地址:CloudFlare Workers

参考项目:Zibri/cloudflare-cors-anywhere

比如我们部署的地址:右键复制 Link 大家也可以使用

参考文章:使用 CloudFlare Workers 实现 CORS Anywhere

使用其他人搭建的代理

比如这个issues介绍到的:

1
https://shielded-brushlands-08810.herokuapp.com/https://github.com/login/oauth/access_token