自动化脚本节省的时间可以用来编写自动化脚本。
publish.yml
完成了上次博客迁移时的 TODO,接入 CI 自动部署。
Codeberg 仓库用的是 Forgejo Actions,语法与 GitHub Actions 相似,还能直接从外部链接拉取 actions,蹭 GitHub 的市场还蛮方便的。只是 runner 要用 Codeberg 自己的 codeberg-small,有时需要等待空闲。
我这次用 Wrangler 直接部署到 Cloudflare Pages,不再需要连接 Git 仓库了,同时也省去一个专门存 HTML 文件的分支。
(由于 CF 只支持连接 GitHub 和 GitLab,我之前甚至是先把仓库镜像到 GitLab,绕了好大一圈。)
name: Publish on: workflow_dispatch: push: branches: - main jobs: publish: runs-on: codeberg-small steps: - uses: https://github.com/actions/checkout@v6 - uses: https://github.com/purcell/setup-emacs@master with: version: 30.2 - name: Install pagefind run: | curl -sL "https://github.com/Pagefind/pagefind/releases/download/v1.5.2/pagefind_extended-v1.5.2-x86_64-unknown-linux-musl.tar.gz" \ | tar xz -C /usr/local/bin mv /usr/local/bin/pagefind_extended /usr/local/bin/pagefind - name: Build run: | emacs --batch \ --load setup.el \ --load publish.el \ --eval "(require 'ox-publish)" \ --eval "(require 'ox-html)" \ --eval "(org-publish-project \"blog-all\" t)" - name: Deploy uses: https://github.com/cloudflare/wrangler-action@v4 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} wranglerVersion: "4.98.0" command: pages deploy public/ --project-name=blog
Pagefind 的运行已经写进 publish.el 的 :completion-function,org-publish-project 结束后会自动触发,CI 这里只需要安装好二进制就够了。
publish.el
之前的代码放在 Emacs 用户配置里,挺乱的,现在把它抽成 publish.el 放在博客目录下,本地 Emacs 和 CI 都加载同一份文件。
以前配置的绝对路径在 CI 下肯定不能用,改用 publish.el 自身的路径推导根目录:load-file-name 在文件被 load 时有值,buffer-file-name 在交互式求值时有值。
(defvar blog/root (file-name-directory (or load-file-name buffer-file-name))) (defvar blog/output-dir (expand-file-name "public/" blog/root))
参考了 systemcrafters-site 等仓库,发现函数命名惯例是用作者缩写作前缀(如 dw/ 对应 David Wilson)。我统一改成了 blog/:
11:(defun blog/slugify (str) 18:(defun blog/post-metadata () 52:(defun blog/generate-index (index-file title-str keyword-fn &optional reverse) 84:(defun blog/generate-tag-index (&optional _project) 91:(defun blog/generate-category-index (&optional _project) 98:(defun blog/generate-archive-index (&optional _project) 106:(defun blog/generate-homepage (&optional _project) 182: (defun blog/html-article-meta (info) 214: (defun blog/html-inner-template (contents info) 230: (defun blog/html-headline (headline contents info) 248: (defun blog/html-special-block (special-block contents info) 267:(defun blog/publish-to-html (plist filename pub-dir) 270:(defun blog/run-pagefind (_project) 343: (defun blog/html-wrap-table (string backend _) 350: (defun blog/export-remove-zero-width-space (text backend _info)
也许您注意到了,上篇关于卡丘的文章里 log 和 manifest 文件内容有语法高亮,这是专门为了博客写的自定义 Major mode。
Emacs 定义 Major mode 非常简单,font-lock 规则写几条正则就够了:
(defvar calabiyau-log-fontlock '(("\\([0-9]\\{2\\}-[0-9]\\{2\\} [0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}\\.[0-9]+\\): \\(Debug\\|Info\\|Warning\\)/(\\([0-9]+\\)):" (1 font-lock-keyword-face) (2 (cond ((string= (match-string 2) "Debug") 'font-lock-comment-face) ((string= (match-string 2) "Info") 'font-lock-function-name-face) ((string= (match-string 2) "Warning") 'font-lock-warning-face))) (3 font-lock-constant-face)))) (define-derived-mode calabiyau-log-mode fundamental-mode "calabiyau-log" (setq font-lock-defaults '(calabiyau-log-fontlock))) (defvar calabiyau-manifest-fontlock '(("--[a-z_]+--" (0 font-lock-keyword-face)) ("\\([^|>\n]+\\)->\\([^|>\n]+\\)|\\([^|]+\\)|\\([^|]+\\)|\\([^|\n]+\\)" (1 font-lock-function-name-face) ; old version (2 font-lock-function-name-face) ; new version (3 font-lock-string-face) ; file path (4 font-lock-constant-face) ; md5 (5 font-lock-warning-face)) ; file size ("\\([^|\n]+\\)|\\([^|]+\\)|\\([^|\n]+\\)" (1 font-lock-string-face) ; file path (2 font-lock-constant-face) ; md5 (3 font-lock-warning-face)))) ; file size (define-derived-mode calabiyau-manifest-mode fundamental-mode "calabiyau-manifest" (setq font-lock-defaults '(calabiyau-manifest-fontlock))) (with-eval-after-load 'org (add-to-list 'org-src-lang-modes '("calabiyau-log" . calabiyau-log) '("calabiyau-manifest" . calabiyau-manifest)))
setup.el
setup.el 只在 CI 中使用,负责安装软件包。它不能并入 publish.el,因为 publish.el 会在本地 Emacs 启动时加载,相关代码不能每次都跑。
;; -*- lexical-binding: t; -*- (require 'package) (setq package-archives '(("melpa" . "https://melpa.org/packages/") ("gnu" . "https://elpa.gnu.org/packages/"))) (package-initialize) (package-refresh-contents) (dolist (pkg '(catppuccin-theme htmlize haskell-mode rust-mode racket-mode powershell yaml-mode json-mode hare-mode fish-mode)) (unless (package-installed-p pkg) (package-install pkg))) (unless (package-installed-p 'org-publish-rss) (package-vc-install '(org-publish-rss :url "https://git.sr.ht/~taingram/org-publish-rss"))) (unless (package-installed-p 'ini-mode) (package-vc-install '(ini-mode :url "https://github.com/Lindydancer/ini-mode"))) (setq catppuccin-flavor 'macchiato) (load-theme 'catppuccin :no-confirm)
接下来
- 添加更多 Slash pages
- 404 页面(为什么到现在还没有)
- 给笔记文档安排一个位置,也许是新站点
- 更多代码块样式(行号、高亮行)
- 对话形式的文本块(用虚拟角色对话讲解内容可以带来不一样的体验,比如 OSTEP 的 Dialogue 章节)
- 减少咖啡用量,增加睡眠时间
其实睡眠等同于地球 Online 挂机,挂机 6 小时惩罚 18 小时,你必须要玩这款游戏,永远永远。