博客从 Hexo 迁移到 Hugo
引子
好久没更新博客了,最近正好想把博客迁移到 CloudFlare Pages 上,索性升级下。
借着迁移的机会,聊聊我折腾博客的经历。我是在 2015 年开始折腾博客,当时距离 Via 已经发布了几个月,我刚好高考结束,时间充裕,天天在弯路群闲聊,认识了不少好友,依稀记得好像是在 sgfox 的帮助下,了解了 DNS 解析和搭建网站这回事。记得第一次搭建博客时,精心挑选了域名 1year.cc,一口气续费了十年(至今还在我的阿里云账户呢),当时觉得 wordpress 太重,用了 Typecho 搭建在虚拟主机上,不过只在搭建最初几天趁着新鲜劲写过几篇文章。2018 年我开始折腾树莓派,对当时的我来说这可真是个新奇玩意,学着搞了不少东西,想着记录一下,决定认真写博客,就又从 Typecho 迁移到了 Hexo,域名也换成了现在这个,断断续续又写了些。一直在折腾,却没留下多少文字,博客就这么一直睡在网络上,像是从未搭建过。如今只剩下这些用以凑数的零碎文章,本想迁移一并删掉,后来还是决定保留做个记录吧。
搭建 Hugo 博客
搭建过程很简单,跟着官方文档一步步来就好。我用的是 macOS,以下如不做特殊说明都是在 macOS 下操作。
首先,在本机安装 hugo:
brew install hugo
接着,创建站点并配置主题:
# 新建站点到当前目录下的 blog 文件夹
hugo new site blog
# 打开 blog 文件夹
cd blog
# 从远端下载 ananke 主题
git clone --depth=1 https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
# 将主题添加到配置中
echo "theme = 'ananke'" >> hugo.toml
# 启动 hugo 服务
hugo server
最后,就可以在浏览器打开 http://localhost:1313
查看了。想部署到服务器的话,使用 hugo
命令,会在 blog 文件夹的 public 目录下输出所有资源,把这个文件夹的所有内容推到服务器就好。更多的用法这里就不说了,官方文档写得很详细。
配置博客
配置主题
上面使用的主题只是为了让我们快速看到效果,要不然下载了第三方主题,发现启动报错可太有挫败感了 orz。首先去 Hugo Themes 挑选一个喜欢的主题,我使用的是 no-style-please,我觉得这主题太酷了,第一眼就爱上了。像上一步那样,下载主题到 themes 文件夹,修改 hugo.toml
配置文件中的主题选项。
配置完主题后,如果你只是写博客,那这部分下面的内容就不用看了,以下基本是从 Hexo 迁移过来导致的变更。
迁移 Hexo 文章
因为我是从 Hexo 迁移过来的,所以为了兼容原有的链接,需要做一些配置。首先,将以下配置添加到 hugo.toml
中,以修改文章链接为 /article/:slug
:
[permalinks]
posts = "/article/:slug"
接着就可以迁移 markdown 文件了,主要是把 metadata 改成 hugo 格式的,像下面例子这样:
Hexo 文档的 metada:
---
title: 树莓派安装 CentOS
date: 2019-08-28 15:40
tags:
- raspi
categories:
- raspi
permalink: install-centos-on-raspi
---
Hugo 文档的 metadata:
---
title: 树莓派安装 CentOS
date: 2019-08-28T15:40:00+08:00
tags: ["raspi"]
categories: ["raspi"]
draft: false
slug: install-centos-on-raspi
---
我是借助 ChatGPT 写了个 python 脚本处理掉了:
import os
import re
import datetime
def convert_timestamp(timestamp):
dt = datetime.datetime.strptime(timestamp, "%Y-%m-%d %H:%M")
return dt.strftime("%Y-%m-%dT%H:%M:%S+08:00")
def convert_file(input_file, output_dir):
with open(input_file, 'r') as f:
content = f.read()
# 提取元数据
metadata_pattern = re.compile(r'---(.*?)---', re.DOTALL)
metadata_match = metadata_pattern.search(content)
metadata_str = metadata_match.group(1)
# 解析元数据
metadata = {}
key = None
for line in metadata_str.strip().split('\n'):
if line.strip() == '':
continue
if ':' in line:
key, value = re.split(r':\s*', line, maxsplit=1)
key = key.strip()
value = value.strip()
metadata[key] = value
elif key:
metadata[key] += line.strip()
# 修改日期格式
metadata['date'] = convert_timestamp(metadata['date'])
if 'updated' in metadata:
metadata['lastmod'] = convert_timestamp(metadata['updated'])
# 将 tags 和 categories 转换为列表形式
tags_str = ', '.join(f'"{tag}"' for tag in metadata.get('tags', '').split('- ') if tag).strip()
if tags_str:
metadata['tags'] = '[' + tags_str + ']'
else:
metadata.pop('tags')
categories_str = ', '.join(f'"{category}"' for category in metadata.get('categories', '').split('- ') if category)
if categories_str:
metadata['categories'] = '[' + categories_str + ']'
else:
metadata.pop('categories')
permalink = metadata.get('permalink', '')
if permalink:
metadata['slug'] = permalink
# 添加 draft 属性
metadata['draft'] = 'false'
# 构建新的文件内容
keys = ['title', 'date', 'lastmod', 'tags', 'categories', 'draft', 'slug']
new_content = '---\n'
for k in keys:
if k in metadata:
new_content += k + ': ' + metadata[k] + '\n'
new_content += '---\n' + content[metadata_match.end():]
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
# 写入新文件
output_file = os.path.join(output_dir, os.path.basename(input_file))
with open(output_file, 'w') as f:
f.write(new_content)
if __name__ == "__main__":
input_dir = "."
output_dir = "posts_done"
for filename in os.listdir(input_dir):
if filename.endswith(".md"):
input_file = os.path.join(input_dir, filename)
convert_file(input_file, output_dir)
print(f"Converted {filename} and saved to {output_dir}")
脚本大意就是,把当前目录下的 markdown 文件,处理 metadata 数据后,保存到当前目录下的 posts_done 文件夹中。之后就可以将 posts_done 文件夹下的所有文档,复制到 blog 目录下的 content/posts
目录,把文档对应的图片资源,一并复制到 blog 目录下的 static
目录。
添加关于页
文章处理好后,再迁移关于页。把 Hexo 博客里的关于页对应的 markdown 文件,手动处理好 metadata 后,存到 blog 目录下的 content/about.md
即可。最后使用 hugo server
启动服务,打开 http://localhost:1313/about/
查看效果。
添加归档页
归档页(Archives)这个麻烦点,因为 Hugo 默认是不带这个页面的,需要我们手动操作。首先复制使用主题的文件夹里的 layouts/posts/single.html
到 blog 目录的 layouts/archive/single.html
,比如我这里就是:
mkdir layout/archive
cp themes/nostyleplease/layouts/posts/single.html layouts/archive/single.html
接着,修改 layouts/archive/single.html
,将 {{ .Content }}
修改为对应的帖子列表,我这里是按年分类的帖子列表,代码如下:
{{ range where .Site.Pages "Section" "posts" }}
{{ range (.Pages.GroupByDate "2006") }}
<h3>{{ .Key }}</h3>
<ul>
{{ range .Pages }}
<li>
<span>{{- (.Date | time.Format "01-02") }}</span>
<a href="{{ .Permalink | relURL }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>
{{ end }}
{{ end }}
之后,创建归档页面 markdown 文档 content/archives.md
,内容如下:
---
title: "Archives"
type: archive
date: 2024-03-21T23:09:18+08:00
---
接着,使用 hugo server
启动服务,打开 http://localhost:1313/archives/
查看效果。
写文章
好在我之前的 Hexo 配置并不复杂,经过上述的配置过程,在不改动原 Hexo URL 链接的情况下,已经基本将原来的 Hexo 博客完整迁移了。接下来就要考虑如何写博客的问题了。
简单编写
如果你只想简单写写博客,没有编辑器的需求,那直接使用如下命令创建文章:
hugo new posts/post.md
将 post.md 改成你想要的文件名,只要是 md 后缀都可以的。文章文件会创建在 blog 目录下的 content/posts
下,直接用你喜欢的编辑器修改这个文件就好。
使用 Emacs 编写
Emacs 有 ox-hugo 包,可以很方便地将 org 文档存储到 blog 目录下的合适位置,还会同时处理文档的图片,非常好用。我这篇文章就是使用 org mode 编写,并且通过 ox-hugo 导出的。
ox-hugo 有两种编写文章的方式,一种是把所有文章放在一个 org 文件里,用子树的方式组织;另一种是一个 org 文件对应一篇文章。虽然官方推荐使用子树的方式组织文章,但我个人更喜欢一篇文章一个 org 文件,这样日后检索可以直接搜索文件名,修改也快很多。以下都是默认使用一个 org 文件对应一篇文章的做法实现。
首先,在 Emacs 的配置中的合适位置添加 ox-hugo 的包:
(use-package ox-hugo
:ensure t
:defer t
:after ox)
之后在 org 文档开始位置添加头信息,比如我这篇文档的 metadata 就是:
#+title: 博客从 Hexo 迁移到 Hugo
#+date: <2024-03-21 Thu 23:59>
#+hugo_base_dir: ~/code/hugo/blog
#+hugo_section: posts
#+hugo_auto_set_lastmod: t
#+hugo_draft: false
#+hugo_tags: hugo
#+hugo_categories: others
#+hugo_front_matter_format: yaml
#+hugo_locale: zh
#+hugo_slug: migrate-from-hexo-to-hugo
#+options: toc:t author:nil
简单做些说明:
- title: 文章标题,也是导出文档的文件名;
- date: 创建日期;
- hugo_base_dir: blog 目录;
- hugo_section: 保存到哪个 section;
- hugo_auto_set_lastmod: 设置为 t 即表示自动添加最后修改时间;
- hugo_draft: true 代表文章是草稿,不会发布出来,发布时记得改为 false;
- hugo_tags: 文章标签,空格分隔;
- hugo_categories: 文章目录,空格分隔;
- hugo_front_matter_format: metadata 的格式,用 yaml 就好;
- hugo_locale: 为了避免奇怪的空格问题,设置为 zh;
- hugo_slug: 文章的链接;
- options: toc:t 代表输出目录,author:nil 代表不要添加作者信息。
写完文章后,使用 M-x org-hugo-export-to-md
,或是 C-c C-e H h
快捷键,就可以自动将文章转换为 markdown 文件,放在 blog 目录对应的文章,并自动处理好文章引用的图片和文件。
每次都手动编写 metadata 是很累的,我使用 Yasnippet 自动补全,模版如下:
# -*- mode: snippet -*-
# name: hugo-file-header
# key: hoh
# --
#+title: ${1:`(file-name-sans-extension (buffer-name))`}
#+date: `(format-time-string "<%Y-%m-%d %a %H:%M>")`
#+hugo_base_dir: ~/code/hugo/blog
#+hugo_section: posts
#+hugo_auto_set_lastmod: t
#+hugo_draft: true
#+hugo_tags: $2
#+hugo_categories: $3
#+hugo_front_matter_format: yaml
#+hugo_locale: zh
#+hugo_slug: $4
#+options: toc:t author:nil
$0
这样想写博客,新建 org 文件后,输入 hoh 按 tab 键就会自动使用该模版,十分方便。
部署博客到 Cloudflare Pages
以前我一直是将博客托管到自己的服务器,但这样做很麻烦,要操心证书(虽然 acme.sh 很好用但总归要先部署不是?),操心服务器续费。明明是来写博客的,却把自己变成了运维,所以这次我决定把博客部署到 Cloudflare Pages。
首先使用 hugo
命令,网站会生成所有部署资源到 blog 目录下的 public 目录,我们之后需要将这个目录的所有内容上传到 Pages。接着,在 CloudFlare Pages 创建一个项目,项目名称自己想好,这是之后的二级域名(project_name.pages.dev)。
直接上传
压缩 public 文件夹得到一个 zip 包,上传到网页后台。
我没有采用这种方法,虽然可以写个脚本,自动生成网页并压缩 zip 包,自己只需要手动上传,但还是觉得可以直接用 CLI 推送上去更快。
CLI 上传
命令行上传需要先安装 Wrangler,后续上传都是通过 Wrangler 命令:
# 打开博客目录
cd blog
# 安装 wrangler
npm install wrangler --save-dev
安装完成后可以通过 npx wrangler --version
检查版本。(如果想通过 CLI 创建项目,可以使用 npx wrangler pages project create
命令。)使用如下命令将 public 文件夹部署到 Pages:
npx wrangler pages deploy public/
跟着命令行提示一步步进行即可,成功后会给一个临时域名预览。
这里我写了个 bash 脚本 deploy.sh
,后续可以直接执行,不需要每次都打这么多命令,脚本内容如下:
#!/bin/bash
local=$(cd "$(dirname "$0")";pwd)
cd $local
while getopts ":d" opt; do
case ${opt} in
d )
hugo && npx wrangler pages deploy public
;;
\? )
echo "Invalid option: $OPTARG" 1>&2
exit 1
;;
: )
echo "Option -$OPTARG requires an argument." 1>&2
exit 1
;;
esac
done
shift $((OPTIND -1))
这样以后只要执行 bash deploy.sh -d
就会自动生成文件并部署到服务器了。
尾巴
虽然文章很长,折腾起来还是很快的,基本上花了一晚上就搞定了(但是写这篇文章花了差不多快一天)。
时间匆匆,回看博客里以前的文章,发现自己还是进步了很多,无论技术还是文笔。在我为写这篇文章查找资料时,检索到的资料大多是 2020~2022 年发布的,甚至还有不少 2019 年的资料,少有 2023 年写成的文章。AI 时代,知识的获取飞快,或许这篇文章你也是通过 AI 总结读完的,或许根本没有读,只扫过开头便草草关闭网页。但不管怎样,文字总是在那,沉睡在互联网,偶尔通过深埋在海底的光缆,在世间游荡。