从Hexo到Hugo
背景
促使我从WordPress搬迁到Hexo,再决定从Hexo搬迁到Hugo的动机是:想尽可能简化写博客的流程,减少除文章撰写以外一切无关事务的精力消耗。
这一追求源于我对自身的观察。一天的精力里往往绝大部分都投入在工作之上,在闲暇中再挤出时间投入写作对意志力是个考验。以往使用WordPress时,一旦VPS的访问速度不佳,那登录后台、打开编辑器、调整样式过程中等待耗费的时间,就足以将不多的意志力消磨干净。
后续切到Hexo+Github Pages后,不得不说,这让博客的发布顺畅不少,不再需要登录后台,几乎不需要考虑排版,迁移站点时也不再有数据库的顾虑。唯一的麻烦是,每次修改文件后,需要调用Hexo CLI重新生成站点并推送到Github Pages仓库。这也意味着本地总得准备一份Node.js环境。思考一番后,前面通过使用Github Actions部署Hexo把生成和部署也自动化了,只需要写文章并推送即可。
这么愉快地用了些时日,但在和Obsidian的配合使用中,又发现了新的矛盾:
- Hexo如果需要用相对路径引用图片,图片应置于文章同名的文件夹下。而在Obsidian中,我所采取的方式是图片集中放置于media目录下
- 笔记现在总是用Obsidian创建,但需要为Hexo复制一份需要发布的文章及资源,而重复总是不利于维护的
- 如果将Hexo仓库置于Obsidian Vault中,再将需要发布的文章直接放到Hexo仓库的posts目录下,虽然不用复制文章,但Obsidian Vault中会带入Hexo/Node.js相关的文件。这些文件和笔记无关,在整理笔记时无异于噪音
这些问题都可以通过修改笔记来迁就Hexo,但这就是前文所说的,文章撰写以外的事务。我现在相信:应当让博客工具迁就写作习惯,而不是调整写作习惯来适应工具。
方法
为解决以上矛盾,想到的方法是:
- 在Vault中新建一个文件夹(这里记作Publish),在其中初始化Git仓库,存放供发布的文章
- 新建一个静态站点文件夹,同样初始话Git仓库,Publish仓库作为Git子模块加入其中
- 静态站点生成器应支持相对路径,且对文件夹的名称无要求
这样Obsidian Vault中只需要存放笔记,至于静态站点生成相关的内容则不再其中。为了方便博客发布的流程,还应该做到:
- 推送静态站点仓库时,应自动生成和部署站点,免去本地生成站点的麻烦
- 在Publish仓库中推送时,应自动更新静态站点仓库,让子模块引用Publish仓库的最新提交,免去需要在两个仓库中提交和推送的麻烦
做到这些后,应当可以:
- 不需要为了发布而改变记录笔记的方式
- 只维护一份需发布的资源
- 只需推送文章到远端,站点即能自动更新,本地不需要配置环境或手动生成站点
本想看看有没有办法让Hexo支持任意文件夹名的相对路径,但惊讶地发现并没有轻松的方式实现(我原以为这是非常常见的需求)。Gatsby/Jekyll/Hugo等一众工具也不原生支持此需求,这似乎与文件名到URL的映射有关。不过 GitHub - zoni/obsidian-export: Rust library and CLI to export an Obsidian vault to regular Markdown 提供了一个解决方式,遂决定切换到Hugo。
实施
支持相对路径
- 在Obsidian Vault中新建一个文件夹(这里记为Publish),在其中初始化Git仓库,存放供发布的文章。在Github上创建远端仓库并推送
- 用
hugo new site
命令创建站点文件夹(这里记为Site),同样初始化Git仓库。在Github上创建xxx.github.io
名称的仓库并推送 - 与Hexo一样,Hugo同样支持主题,这里选用了PaperMod | Hugo Themes
- 在主题的
layouts/_default/_markup/render-image.html
中(若无该文件则新建),加入以下代码片段以支持相对路径引用图片:
{{- $url := urls.Parse .Destination -}}
{{- $scheme := $url.Scheme -}}
<a href="
{{- if eq $scheme "" -}}
{{- if strings.HasSuffix $url.Path ".md" -}}
{{- relref .Page .Destination | safeURL -}}
{{- else -}}
{{- .Destination | safeURL -}}
{{- end -}}
{{- else -}}
{{- .Destination | safeURL -}}
{{- end -}}"
{{- with .Title }} title="{{ . | safeHTML }}"{{- end -}}>
{{- .Text | safeHTML -}}
</a>
{{- /* whitespace stripped here to avoid trailing newline in rendered result caused by file EOL */ -}}
- 在主题的
layouts/_default/_markup/render-link.html
中,加入以下片段以支持相对路径引用笔记:
{{- $url := urls.Parse .Destination -}}
{{- $scheme := $url.Scheme -}}
<a href="
{{- if eq $scheme "" -}}
{{- if strings.HasSuffix $url.Path ".md" -}}
{{- relref .Page .Destination | safeURL -}}
{{- else -}}
{{- .Destination | safeURL -}}
{{- end -}}
{{- else -}}
{{- .Destination | safeURL -}}
{{- end -}}"
{{- with .Title }} title="{{ . | safeHTML }}"{{- end -}}>
{{- .Text | safeHTML -}}
</a>
{{- /* whitespace stripped here to avoid trailing newline in rendered result caused by file EOL */ -}}
- 运行
hugo server -D
,在本地搭建站点服务器,发现站点图片显示正常
CI/CD配置
现在考虑站点生成和部署的CI流程。
Deploy key与Token
为了让CI机器能访问Github上的仓库,需要创建Personal access tokens或仓库的Depoly keys。Github的Personal access tokesn现在又分Fine-grained tokens和Classic tokens,前者可以指定token到仓库细粒度的权限;至于Deploy keys,原本就是控制单个仓库的读写权限的,在使用Github Actions部署Hexo有所提及。
Site仓库需要访问Publish仓库。由于之前创建过一对SSH秘钥作为Deploy keys,这里不妨复用。在Publish仓库界面,点击Settings -> Deploy keys -> Add deploy key,填入任意title,value则是公钥的内容。Site不需要也不应该写入Publish,故不勾选Allow Write Access。
为了方便,希望在Publish仓库中有推送时,Site仓库能自动关联Publish的最新提交。这需要Publish仓库能访问Site,通知有新的提交到来。
点击头像 -> Settings -> Developer Settings -> Personal access tokens -> Fine-grained tokens -> Generate new token,创建一个访问Site的Token,注意Contents权限为Read and write.
复制Token的值,在Publish仓库界面,点击Settings -> Secrets and variables -> Actions -> New repository secret,Name设置为PAT(下文的Github Actions配置会用Name索引到此secret),Value则填入Token的值。
在Site仓库中,点击Settings -> Secrets -> Actions -> New repository secret,Name设置为BLOG_REPO_DEPLOY_PRIVATE_KEY
,Value则填入私钥的内容。
创建配置
在Publish仓库中,新建.github/workflows/trigger_hugo.yml
:
name: trigger github pages build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Dispatch event
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.PAT }}
repository: tanjoe/tanjoe.github.io
event-type: posts-push
每当向Publish仓库master分支推送时,repository-dispatch 就会向Site仓库发送posts-push
类型的事件。
在Site仓库中,新建.github/workflows/update_posts.yml
:
name: Update posts in submodule
on:
repository_dispatch:
types: [posts-push]
# Allows you to run this workflow manually from the Actions tab or through HTTP API
workflow_dispatch:
jobs:
sync:
name: Sync submodule
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ssh-key: ${{ secrets.BLOG_REPO_DEPLOY_PRIVATE_KEY }}
submodules: true
lfs: true
# Update references
- name: Git Sumbodule Update
run: |
git pull --recurse-submodules
git submodule update --remote --recursive
- name: Commit update
run: |
git config --global user.name 'Git bot'
git config --global user.email 'bot@noreply.github.com'
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git commit -am "Auto updated submodule references" && git push || echo "No changes to commit"
每当收到posts-push
的事件,或Action被手动触发时,此Action会拉取子模组,在主仓库自动创建新提交并推送。
在Site仓库中,再创建.github/workflows/hugo_deploy.yml
:
name: pages-auto-build-deploy
on:
push:
branches:
- master
workflow_run:
workflows: ["Update posts in submodule"]
types:
- completed
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Sync repo
uses: actions/checkout@v2
with:
submodules: true
ssh-key: ${{ secrets.BLOG_REPO_DEPLOY_PRIVATE_KEY }}
lfs: true
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest'
extended: true
- name: Build Hugo
run: hugo --minify
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
commit_message: ${{ github.event.head_commit.message }}
这样每当master分支有新的推送,或名为Update posts in submodule
的workflow执行完成时(也就是update_posts.yml
对应的workflow),执行build-and-deploy。先通过ssh-key拉取子模块,也就是Publish仓库(以及模板项目仓库),使用 Hugo setup 生成站点,使用 GitHub Pages action 将目录下的./public
推送到仓库的gh-pages分支。
update_posts.yml
的Action执行时本身就会往master分支推送,为什么还要单独指定workflow_run
的部分呢?
原因在Triggering a workflow - GitHub Docs有所解释:
When you use the repository’s
GITHUB_TOKEN
to perform tasks, events triggered by theGITHUB_TOKEN
, with the exception ofworkflow_dispatch
andrepository_dispatch
, will not create a new workflow run. This prevents you from accidentally creating recursive workflow runs. For example, if a workflow run pushes code using the repository’sGITHUB_TOKEN
, a new workflow will not run even when the repository contains a workflow configured to run whenpush
events occur.
即用GITHUB_TOKEN触发的任务默认不会执行,以免无意间创建递归的任务。故这里要用workflow_run
显示指定此Action可以被另一个Action触发。
最后,Github Pages的默认部署分支是master,而master分支现在存放的是Hugo项目的文件,静态站点的内容在gh-pages分支。故要在Site仓库页面的Settings -> Pages -> Build and deployment中,指定gh-pages分支作为部署分支。
大功告成!现在在Publish仓库中添加文章并推送,站点就会自动生成并部署了。
参考
- 使用Github Actions部署Hexo
- 利用GitHub Action实现Hugo博客在GitHub Pages自动部署 - 飞狐的部落格
- GitHub - peter-evans/repository-dispatch: A GitHub action to create a repository dispatch event
- Triggering by other repository · community · Discussion #26323 · GitHub
- git - Using GitHub Actions to automatically update the repo’s submodules - Stack Overflow
- github actions - Triggering a new workflow from another workflow? - Stack Overflow