Hexo评论功能的实现:Gitalk

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

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

image

Gitalk评论功能

1、注册OAuth application

  1. 在 github 中进行注册,进入 https://github.com/settings/profile
  2. 点击左侧 Developer settings
  3. Register a new application
image
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的相关配置进行填写:

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

这是我的配置:


image

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

<!-- {### 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),文件内容如下:

<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 安装项目依赖
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 添加配置[4]

#Sitemap
sitemap:
  path: sitemap.xml
  template: ./sitemap_template.xml
  rel: false
  tag: true
  category: false

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

<?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
npm run build

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

4.5 添加自动初始化程序

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

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

以上代码需改动的地方:


image

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

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

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


image

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

4.6 执行命令
npm run talk

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


image
4.7 命令合并

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

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

去除valine评论系统:

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

image

某个页面要不要评论

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

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

报错及解决

image

修改package.json少了个逗号;

image

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

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

image
image

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

参考文章


  1. Hexo Next主题 添加文章评论功能

  2. 如何在 hexo 博客中,集成 gitalk 评论插件

  3. hexo gitalk 评论自动初始化

  4. hexo next

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

  6. package.json 详解

  7. 【Hexo】nexT主题使用攻略基础——添加评论功能

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容