0%

Hexo - 对接 Gitalk 评论系统

gitalk 是什么

gitalk 是一套免费的评论系统,经过简单的配置就能够正常使用。我选择 gitalk 的原因是其他评论系统大多需要自己再创建一个账户或者需要占用自己服务器的空间。这好吗?这不好。所以我选择了 gitalk,只需要一个 github 账户就能够正常使用,你值得拥有。

我的 hexo 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
hexo: 6.0.0
hexo-cli: 4.3.0
os: win32 10.0.19043
node: 16.8.0
v8: 9.2.230.21-node.20
uv: 1.42.0
zlib: 1.2.11
brotli: 1.0.9
ares: 1.17.2
modules: 93
nghttp2: 1.42.0
napi: 8
llhttp: 6.0.2
openssl: 1.1.1k+quic
cldr: 39.0
icu: 69.1
tz: 2021a
unicode: 13.0
ngtcp2: 0.1.0-DEV
nghttp3: 0.1.0-DEV
next: 7.8.0

创建一个 Github OAuth Apps

点击这里,填写需要填写的信息,然后点击 Register application 即可。创建 App 之后,会得到 Client ID 和 Client Secret 这两个参数,保存它们。

1
2
3
4
Application name # 应用名称,尽量填写英文名称。
Homepage URL # 主页 URL,填写自己的网站的地址,记得末尾加“/”。
Application description # 应用说明,可以不填写。
Authorization callback URL # 授权回调 URL,填写和 Homepage URL 一样的信息即可。

编辑的 themes 的 _config.yml 文件

一般 themes 的 _config.yml 的文件里会有 gitalk 相关的配置,将它们填写完整即可。我使用的是 next 7.8.0,配置文件里有 gitalk 的相关配置。总共需要更改三个地方的配置,第一个是 conmments 区块的 active 选项,第二个是 gitalk 的 css 和 js 链接,第三个是 gitalk 的配置。这里配置好基本就可以正常使用 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: changyan | disqus | disqusjs | gitalk | livere | valine
active: gitalk # 这里更改成 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:
#disqus:
# text: Load Disqus
# order: -1
#gitalk:
# order: -2
1
2
3
4
5
6
7
8
9
10
11
12
13
gitalk:
enable: true
github_id: # 填写自己的用户名,不是邮箱。
repo: # 填写自己存放评论的项目的名称。
client_id: # 之前储存的值。
client_secret: # 之前储存的值。
admin_user: # 填写自己的用户名,不是邮箱。
distraction_free_mode: true # Facebook-like distraction free mode
# Gitalk的显示语言取决于用户的浏览器或系统环境,
# 如果你希望每个访问你网站的人都能看到统一的语言,你可以设置一个强制语言值,
# 可用的值:en | es-ES | fr | ru | zh-CN | zh-TW
language: zh-CN
proxy: # 自己去 cloudflare 注册个账户,然后配置一个代理,后面会讲如何操作。
1
2
3
4
5
# Gitalk
# gitalk_js: //cdn.jsdelivr.net/npm/[email protected]/dist/gitalk.min.js
# gitalk_css: //cdn.jsdelivr.net/npm/[email protected]/dist/gitalk.min.css
gitalk_js: //cdn.jsdelivr.net/npm/[email protected]/dist/gitalk.min.js
gitalk_css: //cdn.jsdelivr.net/npm/[email protected]/dist/gitalk.css

如何配置一个界面代理用来访问 gitalk 登录界面

上接
proxy: # 代理配置,后面会讲如何操作。
我使用的代理是 Cloudflare 的创建的代理,但是一般能在线执行 js 的服务商一般都可以使用我使用的模板。下面说一下如何在 cloudflare 上创建一个给 gitalk 使用的代理。

  1. 创建一个 Cloudflare 账户;
  2. 开启账户的 Workers 功能;
  3. 创建一个项目用于存储代理的 js 代码,并填入下面的代码或者自行从 Github 获取最新的 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
/*
CORS Anywhere as a Cloudflare Worker!
(c) 2019 by Zibri (www.zibri.org)
email: zibri AT zibri DOT org
https://github.com/Zibri/cloudflare-cors-anywhere
*/

/*
whitelist = [ "^http.?://www.zibri.org$", "zibri.org$", "test\\..*" ]; // regexp for whitelisted urls
*/

blacklist = [ ]; // regexp for blacklisted urls
whitelist = [ ".*" ]; // regexp for whitelisted origins

function isListed(uri,listing) {
var ret=false;
if (typeof uri == "string") {
listing.forEach((m)=>{
if (uri.match(m)!=null) ret=true;
});
} else { // decide what to do when Origin is null
ret=true; // true accepts null origins false rejects them.
}
return ret;
}

addEventListener("fetch", async event=>{
event.respondWith((async function() {
isOPTIONS = (event.request.method == "OPTIONS");
var origin_url = new URL(event.request.url);

function fix(myHeaders) {
// myHeaders.set("Access-Control-Allow-Origin", "*");
myHeaders.set("Access-Control-Allow-Origin", event.request.headers.get("Origin"));
if (isOPTIONS) {
myHeaders.set("Access-Control-Allow-Methods", event.request.headers.get("access-control-request-method"));
acrh = event.request.headers.get("access-control-request-headers");
//myHeaders.set("Access-Control-Allow-Credentials", "true");

if (acrh) {
myHeaders.set("Access-Control-Allow-Headers", acrh);
}

myHeaders.delete("X-Content-Type-Options");
}
return myHeaders;
}
var fetch_url = decodeURIComponent(decodeURIComponent(origin_url.search.substr(1)));

var orig = event.request.headers.get("Origin");

var remIp = event.request.headers.get("CF-Connecting-IP");

if ((!isListed(fetch_url, blacklist)) && (isListed(orig, whitelist))) {

xheaders = event.request.headers.get("x-cors-headers");

if (xheaders != null) {
try {
xheaders = JSON.parse(xheaders);
} catch (e) {}
}

if (origin_url.search.startsWith("?")) {
recv_headers = {};
for (var pair of event.request.headers.entries()) {
if ((pair[0].match("^origin") == null) &&
(pair[0].match("eferer") == null) &&
(pair[0].match("^cf-") == null) &&
(pair[0].match("^x-forw") == null) &&
(pair[0].match("^x-cors-headers") == null)
) recv_headers[pair[0]] = pair[1];
}

if (xheaders != null) {
Object.entries(xheaders).forEach((c)=>recv_headers[c[0]] = c[1]);
}

newreq = new Request(event.request,{
"headers": recv_headers
});

var response = await fetch(fetch_url,newreq);
var myHeaders = new Headers(response.headers);
cors_headers = [];
allh = {};
for (var pair of response.headers.entries()) {
cors_headers.push(pair[0]);
allh[pair[0]] = pair[1];
}
cors_headers.push("cors-received-headers");
myHeaders = fix(myHeaders);

myHeaders.set("Access-Control-Expose-Headers", cors_headers.join(","));

myHeaders.set("cors-received-headers", JSON.stringify(allh));

if (isOPTIONS) {
var body = null;
} else {
var body = await response.arrayBuffer();
}

var init = {
headers: myHeaders,
status: (isOPTIONS ? 200 : response.status),
statusText: (isOPTIONS ? "OK" : response.statusText)
};
return new Response(body,init);

} else {
var myHeaders = new Headers();
myHeaders = fix(myHeaders);

if (typeof event.request.cf != "undefined") {
if (typeof event.request.cf.country != "undefined") {
country = event.request.cf.country;
} else
country = false;

if (typeof event.request.cf.colo != "undefined") {
colo = event.request.cf.colo;
} else
colo = false;
} else {
country = false;
colo = false;
}

return new Response(
"CLOUDFLARE-CORS-ANYWHERE\n\n" +
"Source:\nhttps://github.com/Zibri/cloudflare-cors-anywhere\n\n" +
"Usage:\n" + origin_url.origin + "/?uri\n\n" +
"Donate:\nhttps://paypal.me/Zibri/5\n\n" +
"Limits: 100,000 requests/day\n" +
" 1,000 requests/10 minutes\n\n" +
(orig != null ? "Origin: " + orig + "\n" : "") +
"Ip: " + remIp + "\n" +
(country ? "Country: " + country + "\n" : "") +
(colo ? "Datacenter: " + colo + "\n" : "") + "\n" +
((xheaders != null) ? "\nx-cors-headers: " + JSON.stringify(xheaders) : ""),
{status: 200, headers: myHeaders}
);
}
} else {

return new Response(
"Create your own cors proxy</br>\n" +
"<a href='https://github.com/Zibri/cloudflare-cors-anywhere'>https://github.com/Zibri/cloudflare-cors-anywhere</a></br>\n" +
"\nDonate</br>\n" +
"<a href='https://paypal.me/Zibri/5'>https://paypal.me/Zibri/5</a>\n",
{
status: 403,
statusText: 'Forbidden',
headers: {
"Content-Type": "text/html"
}
});
}
}
)());
});

gitalk 自动初始化

自动初始化 gitalk 使用的是 Github 的 Personal access tokens,此方式限制每小时五千次请求,Github 的 OAuth 限制每小时请求 60 次,限制请求数量太低,不够批量初始化评论。

申请一个 Personal access token

  1. 创建一个 Github 账户;
  2. 转到这个界面创建一个 Github 的 Personal access tokens;
  3. 填写项目名称,选择 repo 权限;
    需要选择的范围
  4. 创建项目成功后,Github 会给你一个 token,保存这个 token。这个 token 只会显示一次,刷新界面就会消失,如果消失了可以选择重新生成 token,即点击 Regenerate token 按钮;

安装自动初始化需要使用到的插件并且生成 sitemap

在 hexo 的根目录(可以自行 cd 或者直接在根目录 Shift + 右键选择 PowerShell。)执行下面的代码。

1
npm i request xml-parser blueimp-md5 moment hexo-generator-sitemap  -S

然后执行下面的指令让 hexo 部署生成 sitemap。

1
hexo g

部署自动初始化程序

在网站根目录新建一个文件,可以自行命名,建议明明一个有实际意义的名字,文件后缀名需要是 js,例如“gitalk-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: '', // GitHub repository 所有者,可以是个人或者组织。对应Gitalk配置中的owner
repo: "", // 储存评论issue的github仓库名,仅需要仓库名字即可。对应 Gitalk配置中的repo
token: '', // 前面申请的 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 文件,添加一个命令来方便初始化的执行

切换到 hexo 的根目录,编辑 package.json 文件。在 scripts 组添加一行代码,如下所示。

1
2
3
"scripts": {
"start": "hexo clean && node gitalk-auto-init.js && hexo d -g ",
}

设置之后,每次执行如下的代码就会自动初始化 gitalk 评论。

1
npm run start

至此,本文关于 gitalk 的配置说明全部完成。

欢迎关注我的其它发布渠道