博客从 WordPress 迁移至 GitHub 手记
经过足足 3 周的时间终于把博客 https://yanbin.blog 从 AWS Lightsail 主机的 WordPress 迁移到了 Hugo 搭建的 GitHub Pages 上。 从动态页面转换成纯静态页,访问时确实是飞快,至少是从北美访问每个半秒以下开。
本来一个博客网站就没有必要搞成动态的,之前网站由于操作系统,WordPress, 和数据库的升级,一段时间以来常出现网站无法访问,1G 内存都顶不住, 后经 一番 Apache, MySQL, 系统调优才得以解决,不过 WordPress 再如何使用文件缓存性能都比静态页面差许多。
快速回顾一下本博客的历史,2006 年前 QQ 空间,后来 blogcn.com, 再到 blogjava.net,2010 年始声请了 unmi.cc 域名, 租 VPS 自搭 WordPress 服务,后面就不断的换 VPS 提供商,也出现过数据少量丢失的现象,所以有些图片或附件不可考。由于 unmi.cc 无法顺利迁出才有了新的 yanbin.blog 域名, 所以博文中还有不少 unmi.cc 的影子,以至于 unmi.cc 被人注册了,并且还堂而皇之的建立了一个李鬼网站,其中很多标题是盗用我的,内容全是 AI 生成。
考虑使用静态页面博客的优点有
- 省钱, 不用租用 VPS 来搭建 WordPress. 可免费托管静态页面的地方有 Cloudflare Pages, GitHub Page, 和 Netlify
- 借助于 GitHub, 有着天然的版本管理功能, 不需要 WordPress 的 Revision 功能. 还不用担心备份的问题
- 速度极快, 这就是静态页面直接带来的好处, 如果必要的话还能免费使用 Cloudflare, Netlify 的全球 CDN 功能
- Markdown 书写日志比在 WordPress 后台编辑方便, 可随时随地, 完成后只要提交到 GitHub 使会自动发布. 而且嵌入程序代码时更方便, 只要三个撇号
- Markdown 文档既适合人阅读也适合机器解析, 而 HTML 格式的内容就不是给人看的, 所以在从 WordPress 转换为静态页面时不少日志内容全在一行
- 用 Hugo 这样的工具管理静态页面, 定制性能 WordPress 更强, 再也不用管理那些插件了, 有定制需求直接编辑 layout, shortcode 等文件
- GitHub Pages 或 Cloudflare Pages 可绑定自己的域名, 和自动管理 HTTPS 证书, 不用自己 Let's Encrypt
在使用 GitHub Pages 静态面面网站有以下流行的工具可供选择
- Jekyll, Ruby 写的, GitHub Pages 原生支持, 不用配置 Action
- Astro, Node.js, TypeScript 编写的
- Hexo, Node.js, JavaScript 编写的
- VitePress 和 VuePress, Node.js, 依托 Vue.js 生态, 新项目建议用 VitePress
- Hugo, Go 语言编写的, 编译型, 所以比以上解释型工具要快
- Zola, 刚刚得知的一款 Rust 编写的工具, 比 Hugo 还快
具体选用哪个工具根据自己喜欢的语言生态, 提供的主题是否丰富, 以及可定制性, 本地调试的方便性. 基于此我选择了 Go 写的 Hugo, 因为它的快, 有着丰富的主题, 关键是找到我喜爱的 Clarity 主题. 并且在后期使用当中感觉不错.
创建一个 Hugo 站点并用 Clarity 主题
下面相应的命令
1brew install hugo # 在 macOS 下安装 Hugo
2hugo new site my-static-site
3cd my-static-site
4git init
5git submodule add https://github.com/chipzoller/hugo-clarity.git themes/hugo-clarity
6cp -R themes/hugo-clarity/exampleSite/* ./ # 使用主题中的示例站点内容
7hugo server
这样就在 1313 端口上启动了一个站点, 访问 http://localhost:1313 就能看到与 Clarity 主题示例站点内容. Hugo 生成的所有内容都在 public
目录下. 自己的页面在 content/post 下创建.
例如用 hugo 命令
1hugo new post/my-first-post.md # 会创建 /content/post/my-first-post.md
2hugo new post/my-second-post/index.md # 会创建 /content/post/my-second-post/index.md
修改以上两个 *.md 文件中的 draft: true 为 draft: false 就能浏览 http://localhost:1313/post/my-first-post 和
http://localhost:1313/post/my-second-post/.
把 post/my-second-post/index.md 中的 usePageBundles: false 改为 usePageBundles: true 就是 Page bundles.
WordPress 逐篇迁移时的过程记录.
以上静态页面工具多是推荐使用 Markdown 格式的文档, Hugo 也不例外, 于是一开始想像着能否用 WorkPress 的某个插件直接导出所有的日志为静态页面, 找到了类似于 Jekyll Exporter 的插件 wordpress-to-hugo-exporter, 导出后与想像相差甚远, 一劳永逸还是别想了. 但由它导出的图片等文件还是用上了.
干脆自己来, 顺便熟悉一下 Rust, 所以用 Rust 直接访问 WordPress 的数据库导出全部日志包括特征图片, 页面访问到一个 JSON 文件中. 使用的 SQL 为
1select a.id, a.post_status, nullif(a.post_date_gmt, '0000-00-00 00:00:00'),
2 a.post_title, a.post_name, a.post_content, nullif(a.post_modified_gmt, '0000-00-00 00:00:00'),
3 b.meta_value as views, d.guid as feature_image
4 from wp_posts a
5 left join wp_postmeta b on a.id=b.post_id and b.meta_key='views'
6 left join wp_postmeta c on a.id=c.post_id and c.meta_key ='_thumbnail_id'
7 left join wp_posts d on d.id=c.meta_value
8 where a.post_type in ('post') and a.post_status in ('publish', 'draft', 'pending', 'private')
再基于这个 all_posts.json 文件用 Rust 生成一个一个名为 a.post_name(即 url) 的目录, 目录中包含 page.html, 其中有 FrontMatter
和日志的 HTML 内容. 生成 page.html 对许多 HTML 标签作了 <br>, \r\n 等替换处理.
由 Rust 生成像 aws-python-lambda-layer/page.html 文件时用到了模板 Crate Tera, 模板文件的内容为
1---
2title: {{ post.title }}
3url: /{{ post.name }}/
4date: {{ post.post_date_gmt | date_format }}
5featured: false
6draft: true
7toc: false
8# menu: main
9usePageBundles: true{% if post.feature_image %}
10thumbnail: "../images/logos/{{ post.feature_image }}"{% endif %}
11categories:{% for cat in post.categories %}
12 - {{ cat }}{% endfor %}
13tags: {% for tag in post.tags %}
14 - {{ tag }}{% endfor %}
15comment: true
16codeMaxLines: 50
17# additional
18wpPostId: {{ post.id }} {# post id in Wordpress #}
19wpStatus: {{ post.status }}
20views: {{ post.views }}
21lastmod: {{ post.last_modified_gmt | date_format }}
22---
23
24{{ post_content }}
这也将作为以后写作新日志的模板.
其间尝试过用 html2md 等 HTML To Markdown 的 Crates 转换 WordPress 的 HTML 内容为 Markdown 格式, 但始终都无法得到理想的内容,
如表格转换有问题, 对众多的代码片段如 <pre class="lang:java">...</pre> 无法正确转换. 最后还是索性从产生的 1200 多个 page.html
逐个理一遍.
针对 1200 多个 page.html 中的每一篇, 几乎对每一行都要查看, 要做的事情基本是
- 把
draft由true改成false - 把出现的
<pre class="..."标签都替换成{{< highlight java >}}. 在 WordPress 中多数时候没有指定语言, 但用 highlight Hugo 标签时必须确定语言, 如果在 HTML 中有 mark 行也须手工填过去, 如<pre class="lang:default mark:1,4-6 decode:true">...</pre>就要变成{{< highlight java "hl_lines=1 4-6" >}}...{{</ highlight >}} - 对于新的
{{< highlight xxx >}}中的所有 HTML 实体全部还原回去, 如<,>变回<和>;<br><br>会还原为\r\n, 每行后挂单的<br/>要删除掉 - 从 WordPress 生成的 page.html 中有大量的
<br>, 在某些行上后的<br>要进行清除 - 对于
<img>图片链接, 转换为自定义的 Shortcode{{< bundle-image xxx.png 611 >}}, 并且从 WordPress to Hugo 导出的文件中找到相应的图片拷贝到对应page.html所在的目录中 - 把站内的链接由
https://unmi.cc/xxx或https://yanbin.blog/yyy改成相对链接/xxx和/yyy - 编辑
page.html的过程中还发现少量的 WordPress 日志还被挂码了, 看到有这样的内容<div id="xunlei_com_thunder_helper_plugin_d462f475-c18e-46be-bd10-327458d045bd"> </div>, 也作了相应的清理 page.html中有大量的原本为 对应的空格显示为
. 对于 <blockquote>中的 NBSP 保留, 在{{/* highligth xxx */>}}中的全部用 Vims/\%u00a0/ /g进行替换- 还有些日志在几经辗转之后居然产生了乱码, 像
特性还�欢际谴悠渌镅阅嵌韫吹摹O喾碈#也从Ja, 暂时难以恢复, 除非进行 encode/decode 尝试 - 把有些整篇日志 HTML 内容全揉成了一行的内容进行简单的分行, 不然对以上替换操作不易进行, 分成多行后稍稍利于人工阅读
- 把
<!--more-->从标签内部移出, 例如有些日志<!--more-->在<strong>..</strong>之间, 造成列表页面其他日志概要全变粗.
WordPress WYSIWYG 编辑器选择 More 按钮把不少的<!--more-->加到的标签中间
为什么选择用 page.html
Hugo 不仅支持 Markdown 的文件格式, 还支持以下其他几种格式
- index.adoc: AsciiDoc 格式
- index.org: Emacs Org Mode 格式文件
- index.pandoc: Pandoc 格式
- index.rst: rsStructuredText 格式
- index.html
除了 HTML Markup 格式外, 其余的格式都是广义上的 Markdown 格式.
存储在 WordPress wp_posts 表中的日志内容是 HTML 格式, 经过程序自动化转换 HTML 为 Markdown 的尝试均以失败告终. 幸好 Hugo 直接支持
HTML 格式, 所以对原来 WordPress 中的老日志就直接用 HTML 格式, 然后在此基础上进行修改, 以尽可能的减少内容失真.
为何不用 index.html 而用 page.html 文件名, 这要回到 Hugo 支持的 Page bundles
说起.
前面有提到 Page bundles, 就是创建的 /post/my-second-post/index.md 这样的目录层次, 并且在 index.md 中启用 usePageBundles.
这样的话与该 post 相关的资源就直接放在该 /post/my-second-post 中了, 因为基本上每个 post 相关资源是独立的. 修改或删除该 post
只需在该目录中进行, 这给 post 创造了十分好的隔离性. 如有共享内容就放到 static/目录中即可.
Hugo 对于 /post/my-first-post.md 文件编译后会生成 /public/post/my-first-post/index.html, 对于 /post/my-fist-post.html
格式也是一样的.
采用 Bundle 的话, 编译后在 /public/post/my-second-post/ 目录中同时包含 index.html 和其他资源文件. 但对于 /post/my-second-post/
目录下文件命名为 index.html, _index.html, 或 page.html 时产生的内容稍有不同. 对应的访问方式, 前二者为
http://localhost:1313/post/my-second-post/, 后者为 http://localhost:1313/post/my-second-post/page/
如果在 post/my-second-post/page.html 的 FrontMatter 中配置 url: /second-post/ 后, 访问方式就变成了
http://localhost:1313/second-post, 这时候在 /public 目录中 index.html 和资源文件分到两个目录中去了
1public/second-post # 跟随 url
2└── index.html
3
4public/post/my-second-post # 跟随 bundle(目录) 名
5└── wp-hugo-1.png
对于 Bundle 使用了自定义 url 之后图片等资源访问就稍有不同了.
使用 index.html 能让编译后的 index.html 和其他资源文件都在一起, 但实际运行时会产生许多的 public/post/{1,2,3,...17} 这样奇怪的目录. 所以对于从 WordPress 迁移过来的文件还是选择了用 page.html, 而非 index.html` 文件.
关于 IntelliJ IDEA Hugo fix 插件
刚开始对于产生的每一个 page.html 文件真是纯手工打造, 用下面各种 Vim 命令替换
1'<,'>s/</</g
2'<,'>s/>/>/g
3'<,'>s/&/\&/g
4'<,'>s/\%u00a0/ /g
5'<,'>s#<br/><br/>#\r#g
6'<,'>s#<br/>##
7'<,'>s#<br />##
对选择区域用 gv 反复操作. 然后在 IntelliJ 中查找 <pre> 和 </pre> 进行相应的替换. 大约进行到 400 多篇后, 需要些更自动化且可靠的操作.
纯 AI 系统提示词让它对剩下的 800 多篇进行操作是信不过的, 但是可以做一个 IntelliJ 插件啊. 于是用 Claude Code CLI 以 Vibe Coding
的方式做一个 Hugo fix 插件, 在 IntelliJ IDEA 中使用上下文菜单和快捷键操作

这是第一次完全用提示词创建的一个 IntelliJ IDEA 插件, 在这里不是吹嘘什么没写一行代码, 或者零 IntelliJ IDEA 插件开发经验就做成了一个可用的产品,
而是在使用 Claude Code CLI 过程中人自动懒惰的根本不想去看它生成的代码, 所以做完之后仍然是零编程经验, 学习到的知识也是零, 也没有经历过任何的思考,
只是每一步问 process? 的时候回签 yes. 但是经常人的一番调教之后, AI 变得更聪明了, 当前系统只有 JDK 25 的情况下, 它自行分析错误,
然后主动下载 JDK 17 解决了构建问题, 然而使用 AI 的人却变得更愚蠢了.
下面的该插件完成之后, 用 list the menu 显示出来的功能
1> list the menu
2
3⏺ Here's the complete menu structure for the Hugo fix plugin:
4
5 Menu Structure
6
7 ┌─────────────────────┐
8 │ Hugo fix ► │──┬─► fix entities
9 └─────────────────────┘ ├─► replace </pre>
10 ├─► select <pre>
11 ├─► remove last <br/>
12 ├─► rewrite <img>
13 ├─► toggle draft
14 └─► copy url
15
16 All Menu Items with Shortcuts
17
18 | # | Menu Item | Shortcut | Description |
19 |-----|--------------|----------|--------------------------------------------------------------------------------------|
20 | 1 | fix entities | ⌃⌘⇧Z | Decode HTML entities in entire file (< → <, > → >, & → &, <br/><br/> → \n) |
21 | 2 | replace | ⌃⇧⌘R | Replace </pre> tags with \n{{</ highlight >}} in entire file |
22 | 3 | select | ⌃⌘⇧S | Search and select next <pre[^>]*?> tag |
23 | 4 | remove last | ⌃⇧⌘B | Remove last <br/> from current line if it ends with <br/><br/> |
24 | 5 | rewrite | ⌃⇧⌘I | Convert <img src="path/file.jpg" width="600"> to {{< bundle-image file.jpg 600 >}} |
25 | 6 | toggle draft | ⌃⇧⌘D | Toggle first occurrence of draft: true ↔ draft: false |
26 | 7 | copy url | ⌃⇧⌘C | Copy first url: <value> with prefix http://localhost:1313<value> to clipboard |
27
28 Keyboard Shortcut Quick Reference
29
30 - ⌃⌘⇧Z - fix entities
31 - ⌃⇧⌘R - replace
32 - ⌃⌘⇧S - select
33 - ⌃⇧⌘B - remove last
34 - ⌃⇧⌘I - rewrite
35 - ⌃⇧⌘D - toggle draft
36 - ⌃⇧⌘C - copy url
37
38───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
39> test the plugin in RustRover ↵ send
40───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
41 ? for shortcuts
上面也是差不多用过的提示词, 每一个子菜单是经过多次迭代增加的.
后面使用该插件进行修改其余的 page.html 文件效率高了 10X 以上. 这才得已在昨天完成最后的收尾工作, 把域名管理移到 Cloudflare 上,
绑定老域名到新的静态博客站点.
启用 giscus 评论
即使是静态站点, 评论也还是必要的, 方便于各种交流. Clarity 主题内置支持的评论组件有 giscus 和 utterances. giscus 使用的是 GitHub
Repo 的 Discussions 功能来存储关连日志评论的. 启用 giscu 的步骤大致为
- GitHub repo 启用
Discussions功能 - https://github.com/apps/giscus 由安装
giscus到 GitHub Pages 对应的 Repo - https://giscus.app/ 配置得到参数, 并配置到
/config/_default/params.toml中
启用 Page view
在 WordPress 中每篇日志都有页面查看计数, 在导出 WordPress 数据时每篇日志的 Page view 也获取到了. 网站统计功能使用了 GoatCounter. GoatCounter 除了可统计每个页面被访问的次数, 还能由 API 获得相应 URL 被访问的次数.
1curl <goatcounter-url>/counter/study-rust-workspace-package-crate-module.json
2{
3 "count": "8",
4 "count_unique": "8"
5}
基于这个功能, 就能实现每个页面显示访问计数, 加上在 WordPress 中的页面原始计数就是总是访问次数. 显示样式为
, 135 为 WordPress 和新页面访问总和.
一些自定义的功能
基于主题 Clarity 自定义了一些 layouts 下的 partials, shortcodes 页面, 主要有
- 去除了白色, 黑色主题的选择功能, 强制为白色主题. 现阶段各种应用至少还有白色主题可选, 不知是谁设定的很多地方默认主题为黑色
归档页面按年显示日志条目, 通过自定义的 ShortCodearchive.html实现- 右边栏增加
Blog Stats显示几个统计数据, 包括日志总数, 标签总计, 日志分类, 和最后构建时间 - 列表页(如首页, 分类或标签列表页面) 中显示每篇日志
<!--more-->之前的内容, 并且正确按格式分行显示 /assets/sass/_custom.sass中定义代码白色主题显示, 和其他更多的样式定义- 原创日志后加上了
CC BY-NC-SA 4.0许可声明, 不过基本是意思一下, 别人怎么用也拦不住 - 两个重要自定义 ShortCode,
bundle-image和bundle-resource
显示图片虽然在 Markdown 文件中可以用
1
或者用 figure ShortCode, 它完整的参数是
1{{< figure
2 src="/images/examples/zion-national-park.jpg"
3 alt="A photograph of Zion National Park"
4 link="https://www.nps.gov/zion/index.htm"
5 caption="Zion National Park"
6 class="ma0 w-75"
7>}}
参数是可选的, 所以能因地制宜的写上必须的项, 如
1{{< figure src="wp-hugo-1.png" >}}
2{{< figure src="wp-hugo-1.png" width="611px" >}}
为了进一步简化, 定义了 bundle-image, 因为基本上日志都是在引用当前 Bundle 中的资源, 它的完整参数是
1{{< bundle-image src="a.jpg" width="800px" class="aligncenter" >}}
也能基于位置来传入参数, 如
1{{< bundle-image a.jpg >}}
2{{< bundle-image a.jpg 800 >}}
3{{< bundle-image a.jpg 800 inline >}}
bundle-resource 与 bundle-image 的功能类似, 只是它用来引用 Bundle 目录中的其他资源, 如
1<a href="{{< bundle-resource tools.jar >}}">tools.jar</a>
迁移完所有的 WordPress 日志到 Hugo 之后, 这是写下的第一篇日志, 用 Markdown 写日志果然是清爽了许多. 再也不用打开网页来编辑文章了, 也不会写作过程中提示该页无法响应了.
本文链接 https://yanbin.blog/blog-from-wordpress-to-github-notes/, 来自 隔叶黄莺 Yanbin's Blog[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。