博客系统迁移:Hexo 到 Hugo

Jiacai Liu

发布: 2020-12-05   上次更新: 2023-04-01   分类: 效率工具   标签: Emacs Hugo blog

文章目录

上一篇文章介绍了 Emacs 的理念以及其强大的扩展功能,基本上能在 Emacs 里面做到事,我都在 Emacs 里面做。之前的博客一直都是用的 markdown 来书写,虽然 Emacs 也有 markdown 插件,但是总感觉体验不如 org-mode。这周末就趁着手热,把博客系统进行了升级,完美支持 org-mode,这篇文章就是用 org-mode 完成的,下面就来讲一下迁移过程。

Hexo vs Hugo

最近两年一直在用 hexo 来写博客,记得静态托管类网站刚兴起时,hexo 是比较流行的,整个生态也比较完整。这次迁移博客系统时,决定不再用 hexo,原因主要有下面几个:

  1. 安装烦。作为一个 Node.js 写的系统,安装时需要下载大量的 node_modules 。这一点不是很核心,毕竟安装只是一次性工作
  2. 升级难。记得当初在 hexo 2 升级到 3 时,遇到过不少坑,对 Node 的版本也有要求,如果部分依赖用了 C 代码(比如 node-sass),安装起来也比较费劲;现在 hexo 大版本已经到了 5.2.0,不想再去折腾
  3. 原生不支持 org-mode,使用了下 hexo-renderer-org 插件,也没成功,而且其作者现在也没精力来维护了

基于以上几点,hexo 于我而言,不再是最佳的选择,当然并不是说它不优秀,Hugo 也很有可能有类似问题。我之所以选择 Hugo 主要有两个原因:

  1. 基于 Golang 的 Hugo 实现,安装方便,只需一个二进制文件;而且最近两年我个人也一直在用 Golang 开发,比较熟悉
  2. 原生支持 org-mode

Hugo 使用方式比较简单,这里不再赘述,新手可参考官方 Quick Start

迁移过程

旧博客备份

虽然旧博客的源文件不会删除,但是其主题、布局等还是具有怀念意义的,因此采用之前自己写的工具对其备份,然后利用 PdfMerger 将所有文章打成一个文件。

1
2
blog-backup -w ljc -o /tmp/blog -v
java -jar target/pdfmerger-1.2.2-jar-with-dependencies.jar  /tmp/blog/*pdf ljc-backup.pdf
https://img.alicdn.com/imgextra/i3/581166664/O1CN017uZ3vH1z6A1cbCE4F_!!581166664.png
博客备份截图

文章链接

对于个人博客的迁移而言,保证文章链接不变是最重要的。在 hexo 中,文件名与链接的对应关系如下:

1
permalink: blog/:year/:month/:day/:title/

对应的 markdown 文件是

1
source/_posts/:year-:month-:day-:title.markdown

这样的好处是文件按照时间排列,管理方便。

Hugo 则不同,它默认会用文件中 frontmatter 的 date,而非文件名中的。我已经习惯了 hexo 的组织方式,不想去改变,经过一番探索,最终在 hugo 添加下面的配置完美解决:

1
2
3
4
5
[frontmatter]
  date = [":filename", ":default", ":fileModTime"]

[permalinks]
  post = "/blog/:year/:month/:day/:slug/"

这样 Hugo 就会首先从文件名中解析日期,同时也会解析 slug;失败时再从 frontmatter 中解析,更多用法可参考官方文档 Configure Dates

同时,为了避免 hugo new post/year-month-day-slug.org 时标题中带日期,修改默认模板如下:

1
title: {{ replace  .Name "-" " " | replaceRE "^\\d{4} \\d{2} \\d{2} (.*)" "$1" | title }}

RSS

默认 Hugo 生成的 RSS 链接为 /index.xml ,而我之前用的是 /atom.xml,可以通过如下配置修改:

1
2
3
4
[outputFormats]
[outputFormats.RSS]
mediatype = "application/rss"
baseName = "atom"

另外一点,默认 Hugo 只会把文章的摘要输出到 RSS,如果想输出全文,可以修改 RSS 模板rss.xml 是我目前使用的。

Tags 链接

Hugo 中默认会把链接中的字母变成小写,比如标签 Go ,在之前对应地址 /tags/Go ,换成 Hugo 后则是 /tags/go ,可以通过下面的配置关闭这个转化。

1
disablePathToLower = true

如果想保留这个功能,可以进行下面的操作:

1
2
3
4
5
6
7
8
# 让 git 区分文件名大小写
git config --global core.ignorecase true
# 删掉仓库中已有的大写目录
git rm -rf 'tags/Go'
# 重新生成网站
hugo
# 重新添加
git add .

这时 git status 会显示

1
renamed:    tags/Go/index.html -> tags/go/index.html

然后提交就可以了。

Frontmatter

Frontmatter 定义了每篇文章的属性,比如标题、分类等。这也是在 hexo 迁移到 hugo 时问题最多的地方,根本原因在于 hexo 对 frontmatter 格式较宽松,而 hugo 则比较严格。

下面一个 hugo 中标准的 frontmatter(除 yaml 外,还可以是 toml/json):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
categories:
- Development
- VIM
date: "2012-04-06"
description: spf13-vim is a cross platform distribution of vim plugins and resources
  for Vim.
slug: spf13-vim-3-0-release-and-new-website
tags:
- .vimrc
- plugins
- spf13-vim
- vim
title: spf13-vim 3.0 release and new website

主要有两点需要注意:

  1. categories/tags 这两个属性必须是数组
  2. frontmatter 前后需要用 --- 包起来,与正文区分

而在 hexo 中,

  1. categories/tags 可以是数组,也可以是字符串,表示一个元素的数组
  2. 只需要 frontmatter 末尾强制用 --- 与正文区分,前面的不做要求

由于我文章较多(72篇需要迁移),且格式也都不一样(可能是 hexo 2/3 的区别),因此写了两个脚本来辅助,最终生成符合 hugo 要求的 frontmatter。如果 frontmatter 格式不对,可能会遇到下面的错误:

1
2
3
4
5
6
Start building sites …
ERROR 2020/12/04 20:33:38 render of "home" failed:
execute of template failed: template: index.html:6:9:
executing "content" at <.Render>: error calling Render: "~/quickstart/content/post/2016-04-23-sicp-chapter4-summary.markdown:8:19":
failed to execute template ["summary"] v: "~/quickstart/themes/even/layouts/post/summary.html:8:19": execute of template failed:
template: post/summary.html:8:19: executing "post/summary.html" at <.>: range can't iterate over 研习经典

说明 categories 或 tags 有不是数组的,需要改成数组

1
EOF looking for end YAML front matter delimiter

说明缺少了 frontmatter 结尾的分隔符,如果缺少开头的分隔符,编译文章没有错误,但是最终生成的文章页面会没有标题。

Categories

在 hexo 中分类(category) 和标签(tags) 用法是不一样的,分类可以有层次,比如:

1
2
3
4
5
categories:
- [Sports, Baseball]
- [MLB, American League, Boston Red Sox]
- [MLB, American League, New York Yankees]
- Rivalries

标签则没有;在 hugo 中分类与标签用法一样,都只有一层。由于我之前博客就没有用到多层分类的情况,所以也就不需要额外处理了。

其次,在 hexo 可以通过 category_map/tag_map 来定义 category/tags 的固定链接地址(即 slug),虽然我之前也了这个特性,但是这次并没有去适配,采用 hugo 默认的即可。有修改需求的读者可参考:

Render hook

markdown 中引用图片的标准做法是

1
![Alt text here](/images/image.jpg "Title here")

但是我一般只写 alt,title 基本没写过,之前使用的主题 maupassant 默认会把图片的 alt 显式在图片下面,而 hugo 只认 title,搜索发现可以通过 hugo 提供的 markdown render hook 来实现。方式如下:

  1. 创建 layouts/_default/_markup/render-image.html 文件
  2. 添加内容

    1
    2
    3
    4
    5
    6
    7
    8
    
    {{ if .Text }}
    <figure>
    <img src="{{ .Destination | safeURL }}" alt="{{ .Text }}">
    <figcaption>{{ .Text }}</figcaption>
    </figure>
    {{ else }}
    <img src="{{ .Destination | safeURL }}" alt="{{ .Text }}">
    {{ end }}

对于 org-mode 而言,直接采用下面的方式即可:

1
2
#+CAPTION: some-title
[[<img-src>]]

修改记录

本次迁移的所有修改可以在 Github 中查看,供有相同迁移需求的读者参考。

Emacs 集成

经过上面的步骤,已经可以很好的把 hexo 迁移到 hugo,这一小节主要是介绍 Hugo 与编辑器的集成,方便写文章。

Easy-hugo

2022-09-03 更新:这个插件功能我用的比较少,已经不再使用,主要是使用我自己写的 my/hugo-newpost 来创建文章。

Hugo 官网上列举了一些与常用编辑整合的插件,这里介绍 easy hugo 的使用方式。由于目前我又两个博客(中文和英文),因此需要做些配置让 easy hugo 识别这两个。

1
2
3
4
5
6
7
(use-package easy-hugo
  :custom ((easy-hugo-basedir  "~/gh/jiacai2050.github.io/")
		   (easy-hugo-url  "https://liujiacai.net")
           (easy-hugo-default-ext ".org")
           (easy-hugo-bloglist '(((easy-hugo-basedir . "~/gh/en-blog/")
                                  (easy-hugo-default-ext ".org")
		                          (easy-hugo-url . "https://en.liujiacai.net"))))))

由于目前我全局开启了 evil mode,需要把 easy-hugo-mode 添加到 evil-emacs-state-modes 里面去才能使用 easy-hugo 的快捷键,顺道解决了 easy hugo 的一个 bug

easy-hugo 还提供了预览、发布(默认调用 deploy.sh)等命令,比较简单,这里不再赘述。

创建新文章

虽然可以用 hugo new post/xxx.org 的方式来创建新文件,但是由于文件名中需要有固定格式的日期,每次手动输入很繁琐,因此自己实现了一个辅助函数,实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(defun my/hugo-newpost (slug title tags categories)
  (interactive "sSlug: \nsTitle: \nsTags: \nsCategories: ")
  (let* ((now (current-time))
		 (basename (concat (format-time-string "%Y-%m-%d-" now)
						   slug ".org"))
		 (postdir (expand-file-name "content/post" (locate-dominating-file default-directory "config.toml")))
		 (filename (expand-file-name basename postdir))
         (create-date (my/iso-8601-date-string)))
	(when (file-exists-p filename)
      (error "%s already exists!" filename))
	(find-file filename)
	(insert
	 (format "#+TITLE: %s\n#+DATE: %s\n#+LASTMOD: %s\n#+TAGS[]: %s\n#+CATEGORIES[]: %s\n"
             title create-date create-date tags categories))
	(goto-char (point-max))
	(save-buffer)))

这样就可以通过调用 my/hugo-newpost 来自动生成带日期的文件名。

https://img.alicdn.com/imgextra/i4/581166664/O1CN01919NEK1z6A1ZyLbC8_!!581166664.gif
使用 my/hugo-newpost 创建新文章

文章更新时间

Hugo 用 lastmod 这个页面级别的变量表示文章更新时间。每次修改文件时,手动去更新 frontmatter 中的 lastmod 是一个比较麻烦的事情,可以利用 Emacs 中的时间戳自动更新功能,具体可以参考:

自动部署

GitHub Pages

可以利用 actions-hugo 这个 action 实现网站自动的部署

早期的 Pages 需要把网站源码发布到一个 gh-pages 的分支上,对于纯手写 HTML 的网站来说还算可以,但是对于使用工具生成的网站来说,就有些浪费,因为这个分支根本不需要版本管理,不仅会污染仓库,还会导致仓库体积变大。

自从 2022-08-11 后,GitHub 解决了这个问题,允许使用 Actions 中构建出来的产物(Artifact)直接部署网站,无需新建分支,毫无疑问这种方式更优雅。使用方式两步:

  1. 配置 Pages 生成方式为基于 Action
    Pages 配置
  2. 根据 hugo.yml 编写自己项目的 action,本博客的配置文件可供读者参考:gh-pages.yml

Codeberg Pages

Codeberg 提供与 GitHub Pages 类似的服务,只不过必须用一个分支来保存 HTML 源文件,可以利用 Codeberg CI 自动化构建过程,可以参考:.woodpecker.yml

总结

屠龙刀已经磨好了,下面就需要多去动“刀”写出更多文章了。

参考

评论

欢迎读者通过邮件与我交流,也可以在 MastodonTwitter 上关注我。