Hexo相关问题和优化

本文记录一些 Hexo 的难点问题及其解决方案。

环境及版本声明

本文基于以下环境及版本:

1
2
3
4
hexo: 3.8.0
hexo-cli: 1.1.0
NexT: 7.1.0
OS: Ubuntu 18.04 LTS x86_64

禁止爬虫跟踪外链

搜索引擎的蜘蛛来爬取文章内容时,如果你的文章中有外部链接,它就会到外链的站点去爬取,有可能再也不会回来了。为了告诉搜索引擎不要跟踪这些外链,需要在这些链接标签中添加属性 rel="nofollow"rel="external nofollow"

rel="nofollow" 是通用格式,即是告诉搜索引擎不要跟踪此链接,rel="external nofollow" 是更具体的写法,进一步告诉搜索引擎这是一个外部的链接,不要跟踪它。

我们可以 hexo-autonofollow 插件来给外链添加 nofollow 属性,这样有利于 SEO 。

安装 hexo-autonofollow

在站点根目录下执行下列命令:

1
npm install hexo-autonofollow --save

编辑 站点配置文件

站点配置文件 _config.yml 末尾添加如下内容:

1
2
3
4
5
6
7
# Adds nofollow attribute to all external links in your hexo blog posts automatically.
## https://github.com/liuzc/hexo-autonofollow
nofollow:
enable: true
exclude: # 例外的链接,可将友链放在此处
- yourname.github.io # 排除你的站点
# - 友链地址

配置后,在生成的静态文件中,例外的链接就不会被加上 nofollow 属性。

设置永久链接

链接层级过深、链接中包含中文、因为 title 变动导致链接也经常发生变动,这些都不利于 SEO 。推荐 permalink 的解决方案是使用插件 hexo-abbrlink 生成 permalink 。

站点根目录下执行:

1
npm install hexo-abbrlink --save

编辑 站点配置文件

修改 站点配置文件 _config.yml 的 permalink 如下:

1
permalink: posts/:abbrlink/

其中 :abbrlink 代表连接地址。可以在 站点配置文件 下添加 abbrlink 的配置(其中 alg 和 rep 分别为生成的算法和表示方式),如下:

1
2
3
4
5
# abbrlink config
## https://github.com/rozbo/hexo-abbrlink
abbrlink:
alg: crc32 # support crc16(default) and crc32
rep: hex # support dec(default) and hex

不同配置生成的链接效果如下:

算法 进制 结果
crc16 hex https://post.zz173.com/posts/3ab2.html
crc16 dec https://post.zz173.com/posts/12345.html
crc32 hex https://post.zz173.com/posts/9a8b6c4d.html
crc32 dec https://post.zz173.com/posts/1690090958.html

执行 hexo clean && hexo g 重新生成静态文件后,源文件 front-matter 中会包含 abbrlink: xxx

这样不论我们的文件名、文章的 title 、文章的内容有没有发生改变,abbrlink 都不会改变,同时对搜索引擎更加友好。

CRC 全称 Cyclic Redundancy Check,又叫循环冗余校验。CRC32 跟 MD5一样都是哈希算法的一种。hexo-abbrlink 的源码来看,实际上它并没有利用到时间,只是利用了文章 title 来生成。

使用这种方法生成 permalink 时,在每次提交修改前,最好先执行 hexo clean && hexo g,确保提交前你所有的文章的 front-matter 中都包含 abbrlink ,避免因 title 的改变导致生成 abbrlink 不一致(如果已存在 abbrlink,就不会重新生成,不论 title 是否发生变化)。

分类存放文章源文件

Hexo 默认所有的文章都会放在 source/_posts 文件夹下,当文章越来越多时,将很难管理文章源文件同时也不利于快速查找到想要的文章。

我个人的做法就是分层分类存放,同时利用上面提到的 abbrlink 来设置 permalink,以确保不管文章源文件如何变换分类、分类层次有多深,生成的静态页面的链接始终对 SEO 友好。

例如,在 _posts 文件夹,创建了 AlgorithmLinux 两个文件夹作为一级分类, Linux 分类下创建了 command 文件夹作为二级分类:

source-fold

source-unfold

其中,文章的同名同级的文件夹是它的资源文件夹,存放文章对应的资源,如图片等。

本地运行测试,查看文章 QuickSort 如下:

posts-classify

不管源文件如何存放,链接始终是 /posts/xxxxxxx/ 的形式,另外需要注意 文章 front-matter 中的 categories 与这里的分类存放没有任何关系 ,如上图中 QuickSort 文章分类于 Algorithm -> Sort,而其 md 文件是直接放在了 Algorithm 目录下。

如果你想让 URL 直接对应源文件的分类也是可以的,但不推荐这么做,编辑 站点配置文件,修改 permalink 配置如下:

1
permalink: posts/:title/

重新生成,测试结果如下,注意 URL 与源文件存放位置的对应关系:

post-quicksort

post-netstat

英文引号变成中文引号

引号显示异常,英文单引号 ' 双引号 ",都显示成中文单引号’ 双引号”。

方法一

因为 hexo-renderer-marked 渲染 Markdown 有很多问题,所以推荐使用方法二。

Hexo 默认使用 hexo-renderer-marked 作为 Markdown 的解析引擎,它的 smartypants 配置项默认为 true,只需将其禁用就可以解决引号显示异常的问题。

编辑 站点配置文件,在文件末尾添加如下配置:

1
2
3
4
# Prohibit English quotation marks from becoming Chinese quotation marks
## https://github.com/hexojs/hexo/issues/1981
marked:
smartypants: false

保存修改,重新生成站点,就可以正常显示了。

方法二

Hexo 默认自带的 Markdown 渲染引擎 hexo-renderer-marked 有很多不足,例如在渲染数学公式时会有很多问题,同时因为它默认开启 smartypants,导致引号显示异常,所以最好的解决方案是更换 Hexo 默认的 Markdown 渲染引擎。

这里推荐使用 hexo-renderer-pandoc,这个插件依赖于 Pandoc,具体的操作可以参考下面 "数学公式渲染"。

更换渲染引擎后,无需修改任何配置,引号的显示就是正常的。

Hexo NexT 数学公式渲染

参考:

为什么要替换 Hexo 的默认 Markdown 引擎?

Hexo 默认使用 marked.js 去解析我们写的 markdown,比如一些符号,_ 代表斜体,会被处理为 <em> 标签,比如 x_i 在开始被渲染的时候,处理为 x<em>i</em>,这个时候 mathjax 就无法渲染成下标了。很多符号都有这个问题,比如粗体 *,也是无法在 mathjax 渲染出来的,好在有替代的乘法等,包括 \\ 同理。

目前,NexT 提供两种数学公式渲染引擎,分别为 MathJaxKatex,默认为 MathJax。

如果你选择使用 MathJax 进行数学公式渲染,你需要使用 hexo-renderer-pandoc 或者 hexo-renderer-kramed 这两个渲染器的其中一个。

这里推荐使用 MathJax + hexo-renderer-pandoc ,并以此为例:

本地测试

  1. 因为 hexo-renderer-pandoc 依赖 Pandoc,所以首先需要安装 Pandoc,具体安装步骤参考 how to install pandoc

  2. 站点根目录下执行下列命令,卸载原有的渲染器 hexo-renderer-marked,然后安装 hexo-renderer-pandoc

1
npm uninstall hexo-renderer-marked --save
1
npm install hexo-renderer-pandoc --save
  1. 编辑 主题配置文件,修改配置如下:
1
2
3
4
5
6
7
8
9
10
# Math Equations Render Support
math:
enable: true

# Default (true) will load mathjax / katex script on demand.
# That is it only render those page which has `mathjax: true` in Front Matter.
# If you set it to false, it will load mathjax / katex srcipt EVERY PAGE.
per_page: false

engine: mathjax
  1. 重新生成并测试:
1
hexo clean && hexo g && hexo s -o

Travis CI 配置

因为 hexo-renderer-pandoc 依赖 Pandoc,所以 Travis CI 中也要安装 Pandoc,而且最好安装较新的版本,比较旧的版本如 1.19.X 可能解析出错。

在 .travis.yml 中添加类似配置,参考 installing-packages-without-an-apt-repository

1
2
3
before_install:
- wget https://github.com/jgm/pandoc/releases/download/2.7.2/pandoc-2.7.2-1-amd64.deb
- sudo dpkg -i pandoc-2.7.2-1-amd64.deb

完整配置可参考 wylu.github.io/.travis.yml

文章更新时间异常

方法一:使用 updated 属性

推荐使用方法二,避免手动操作。

当使用 Travis CI 自动部署时,发现部署成功后,所有文章的更新时间都变成了此次提交修改的时间,但有些文章在上一次提交后是没有发生过任何修改的。另外,本地测试显示的更新时间是正常的。

这是因为 git 在推送更新时,并不记录保存文件的访问时间、修改时间等元信息,所以每次使用 git 把项目 clone 下来时,文件的时间都是克隆时的时间。又因为如果没有在 front-matter 中指定 updated,Hexo 会默认使用文件的最后修改时间作为文章的更新时间,所以会出现所有文章的更新时间都发生变化的情况。

总的来说,使用 git clone 下来的文件的时间都不是原来文件的时间,而 Travis CI 每次都需要 clone 源码才能进行后面的生成和部署操作,所以目前如果想正确显示更新时间,可以在 front-matter 中指定,或者改用本地手动推送部署。

下面是一个 python 脚本,用来向 front-matter 中插入最后修改时间。

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import time

file_encoding = 'utf-8'


def last_modify(path):
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(os.stat(path).st_mtime))


def write2file(file_path, updated=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())):
with open(file_path, 'r', encoding=file_encoding) as f:
lines = f.readlines()
updated = 'updated: ' + updated + '\n'

flag = False
i = 1
while True:
if lines[i].startswith('updated: '):
flag = True
break
if lines[i] == '---\n':
break
i += 1

if flag:
print("===> Existed. " + file_path)
return

lines = lines[:i] + [updated] + lines[i:]
with open(file_path, 'w', encoding=file_encoding) as f:
f.write(''.join(lines))
print("===> Success. " + file_path)


def get_paths(path):
paths = []
if os.path.isfile(path) and path.endswith(".md"):
return [{"file_path": path, "updated": last_modify(path)}]
for root, dirs, files in os.walk(os.path.join(path)):
for file in files:
if file.endswith(".md"):
file_path = os.path.join(root, file)
paths.append({"file_path": file_path, "updated": last_modify(file_path)})
return paths


def add_update_time(path):
if not os.path.exists(os.path.join(path)):
return
paths = get_paths(path)
for i, item in enumerate(paths):
print(i, end='')
write2file(item['file_path'], item['updated'])
print("Done.")


def main():
global file_encoding
argc = len(sys.argv)
if argc < 2 or argc > 3:
print("Error!\nUsage: python post_util <post directory> [file encoding], for example:")
print('\tpython post_util.py /home/wylu/hexo/source/_post')
print('\tpython post_util.py "/home/wylu/hexo/source/_post" utf-8')
return
if argc == 3:
file_encoding = sys.argv[2]
add_update_time(path=sys.argv[1])


if __name__ == '__main__':
main()

指定 _post 文件夹路径,可批量操作,如:

1
python post_util.py /home/yourname/hexo/source/_post

如果 front-matter 中已存在 updated,则不会覆盖。如果想重新生成,可以删掉 updated 所在的行然后重新执行。

方法二:使用 git 推送时间

参考 NexT deployment .travis.yml

在 Travis CI 的配置文件 .travis.yml 中添加如下配置:

1
2
3
before_install:
# Restore last modified time
- "git ls-files -z | while read -d '' path; do touch -d \"$(git log -1 --format=\"@%ct\" \"$path\")\" \"$path\"; done"

实际上,clone 下来的文件的时间还是克隆时的时间,然后通过上面的命令,它将 clone 下来的文件的时间改成了该文件最近一次变动的推送时间(也即文件最后一次修改的 push 时间)。