使用 org-publish 发布博客

博客经历过几次更新,最开始的时候是用 markdown 写博客,然后用 Hexo 发布。

然后学习了 Vue,又尝试用 VuePress 发布,它们都是托管在 Github Pages 上。

后来接触了 Emacs,就从 markdown 迁移到了 org-mode,然后使用 Hugo 1发布。

因为 GitHub 直连变慢了,所以将内容托管到了 VercelNetlify

最近将博客又更新了一下,使用 org-publish 进行博客的发布,主要是让博客加载相对快一些,维护起来更简单一些。

相关代码见 init-org-publish.el

动机

在很早之前看过几篇文章:

设计复杂的网站,在网络较差的情况下有可能要加载很久才能显示。

如果样式比较复杂, 在一些小屏幕下的显示可能不好。

所以作者提出网站要保持简洁,这次也主要是基于作者提出的建议修改了网站。

相比之前的变化

  • 极大的简化了博客内容,没有主题,没有模板,只有博客内容的 org 文件、css 文件、字体、图片,以及最终转换出来的 HTML 文件
  • 博客的编写、构建只需要 Emacs 就能完成(以及 Browsersync 用于本地查看构建的 HTML)
  • 转换后就是标准的 HTML,而且是使用 Emacs 内置的包转换 org 文件,理论上 org 的特性应该都能转换
  • 按照 better mother fucking websitethe best mother fucking website 的建议,设置了字体颜色,字体大小,链接颜色,行间距,行宽等,比默认的浏览器样式好阅读一些
  • 使用 Atkinson Hyperlegible 字体,让一些容易混淆的字符更容易区分

总而言之,就是清爽了很多,兼容性也应该不错。

org-publish 的使用

org-publish 的使用有点像 gulp,你可以定义很多个任务,每个任务指定处理什么文件,怎么处理,处理之后放到什么地方。

你可以单独地执行某个任务,也可以将任务串联起来执行。

过程中碰到的几个问题:

构建后代码块的高亮

高亮的颜色是基于 Emacs 当前的主题的,而我的博客整体是亮色的,因此需要 Emacs 中也使用亮色的主题才能得到合适的高亮颜色。

为此写了 spike-leung/apply-theme-when-publish 用于在执行 org-publish 的时候切换主题。

RSS 的设置

ox-rss 2可以生成 RSS 文件,但它是针对一个 org 文件的,对于多个 org 文件,它会分别生成 RSS。

但是我需要的是把多个 org 文件生成为一个 RSS 文件。

不过别人已经踩过坑,按照 Org mode blogging: RSS feed 中的方法实现了。

具体来说就是遍历所有的 org 文件,将他们的内容插入到一个 rss.org 文件中,再基于 rss.org 使用 ox-rss 去生成 RSS 文件。

Org mode blogging: RSS feed 的实现会把整个 org 文件的内容插入到 rss.org 中。

但是如果内容很多时,Emacs 会崩溃,我目前是改成只插入标题,后面再研究一下如何优化。

如果执行 RSS 构建后,发现新写的文章不在 RSS 中,也可能是缓存问题,可以尝试:

  • 删除 org-publish-timestamp-directory 中 RSS 的 cache
  • 删除博客相关的 buffer 试试(或者删除全部 buffer)
  • 检查一下是不是 :exclude 正则表达式写得有问题

index 文件的设置

设置 :makeindex t 可以生成 index 文件,需要在 org 文件中设置 #+INDEX keyword。

但是生成的文件名固定是 theindex.org, 并且会按照文章标题首个字进行索引,看起来很怪,最后用 sitemap 替代了。

现在看到的 Home 其实就是一个 sitemap。

#+date 的设置

原来 Hugo 生成的日期 (2023-05-31T13:38:39+08:00) 在转换成 sitemap 的时候似乎不能识别,

于是改成 org 的日期格式 (<2023-05-31 Wed>),这样 sitemap 就能正常按照时间排序了。3

缓存问题

执行 org-publish 之后,会在 org-publish-timestamp-directory 指定的目录下生成缓存,有时调整了页眉页脚,可能需要清除缓存才能看到效果。

不过可以执行 C-u M-x org-publish 忽略缓存进行构建。

:time-stamp-file nil

设置 :time-stamp-file nil 可以避免每次执行 org-publish 的时候都往 HTML 插入最新的时间戳,导致每次变更的文件很多。

:html-head:html-preamble:html-postamble 的复用

最开始定义 org-publish-project-alist , 我是这么写的,导致一直无法使用变量抽象一些公用的 string:

(defconst spike-leung/html-head "
  <link rel=\"stylesheet\" href=\"../styles/style.css\" type=\"text/css\"/>
  <link rel=\"icon\" href=\"/favicon.ico\" type=\"image/x-icon\">
  "
  "`:html-head' for `org-publish'.")

(setq org-publish-project-alist
      '(("orgfiles"
         :base-directory "~/git/taxodium/post"
         :base-extension "org"
         :html-head ,spike-leung/html-head
         ;; ... 还有其他很多设置
         )))

我在这里用的是 ' 去定义, ' 在 elisp 中的作用是:

The special form quote returns its single argument, as written, without evaluating it.

This provides a way to include constant symbols and lists, which are not self-evaluating objects, in a program.

Quoting

因此 ,spike-leung/html-head 会被当作一个常量,而不会被执行, 而 ,spike-leung/html-head 这个字符串 :html-head 并不认识。

要让 ,spike-leung/html-head 被执行,需要将 ' 换成 ` :

    (defconst spike-leung/html-head "
      <link rel=\"stylesheet\" href=\"../styles/style.css\" type=\"text/css\"/>
      <link rel=\"icon\" href=\"/favicon.ico\" type=\"image/x-icon\">
      "
      "`:html-head' for `org-publish'.")

    (setq org-publish-project-alist
-          '(("orgfiles"
+          `(("orgfiles"
             :base-directory "~/git/taxodium/post"
             :base-extension "org"
             :html-head ,spike-leung/html-head
             ;; ... 还有其他很多设置
             )))

Backquote constructs allow you to quote a list, but selectively evaluate elements of that list.

In the simplest case, it is identical to the special form quote (described in the previous section; see Quoting).

The special marker ‘,’ inside of the argument to backquote indicates a value that isn’t constant.

The Emacs Lisp evaluator evaluates the argument of ‘,’, and puts the value in the list structure.

Backquote

`' 的不同是,它会执行 list 里面的内容, 这样 ,spike-leung/html-head 就会被执行并返回对应的 string,满足 :html-head 的需要。

id 问题

org publish 的时候会给 heading、figure、details、pre 等元素生成 id,如果没有主动设置 id,则会使用 org-export 的默认 id 生成策略,生成一个形如 org123456 的 id。

更大的问题是,如果清除缓存后再执行 org publish,这些 id 都会发生变化,如果有人引用了某个 heading 的 id,就会导致他跳转的时候,无法跳转到他引用的 heading。

为此,我希望能够固定 heading 的 id,看了文档后,可以通过给 heading 添加 CUSTOM_ID ,导出时存在 CUSTOM_ID ,就会使用 CUSTOM_ID

具体的做法是每次保存 org 文件的时候,我都为 heading 生成 CUSTOM_ID4

相关代码
(defun spike-leung/org-add-custom-id-to-headings-in-blog-files ()
  "Add a CUSTOM_ID property to all headings in the current buffer, if it does not already exist."
  (interactive)
  (org-map-entries
   (lambda ()
     (unless (org-entry-get nil "CUSTOM_ID")
       (let ((custom-id (org-id-new)))
         (org-set-property "CUSTOM_ID" custom-id))))))

(add-hook 'org-mode-hook
          (lambda ()
            (when (and buffer-file-name
                       (string-match "taxodium" buffer-file-name)) ;; only apply to blog files
              (add-hook 'before-save-hook 'spike-leung/org-add-custom-id-to-headings-in-blog-files nil 'local))))

而对于 figure、details 这些元素,我其实不关心它们的 id,目前我不知道如何避免这些元素生成 id,我的做法是在导出成 HTML 之后,将它们的 id 属性移除。

相关代码
(defun spike-leung/remove-unnessary-id-from-html (text backend info)
  "Remove unnecessarily id attibute.
These elements's ID will be remove: figure,details,pre ..."
  (when (org-export-derived-backend-p backend 'html)
    (replace-regexp-in-string (rx (seq "<"
                                       (group (or "figure" "details" "pre"))
                                       (group (zero-or-more (not ">")))
                                       (group (seq whitespace "id=" (syntax string-quote) "org" (zero-or-more hex) (syntax string-quote)))
                                       (group (zero-or-more (not ">")))
                                       ">"))
                              (lambda (match)
                                (format "<%s%s%s%s>"
                                        (match-string 1 match) ;; tag
                                        (match-string 2 match) ;; keep other attrs
                                        "" ;; remove id
                                        (match-string 4 match) ;; keep other attrs
                                        ))
                              text)))
(with-eval-after-load 'ox
  (add-to-list 'org-export-filter-final-output-functions
               'spike-leung/remove-unnessary-id-from-html))

一些缺点

  • 在执行 org-publish 时,如果内容多,可能会很慢,此时会阻塞 Emacs,不能做其他事情
  • 由于依赖 Emacs,Netlify 等平台上没有开箱即用的构建命令,集成 CI/CD 相对麻烦
  • 还不知道如何 Hot Reload,所以预览编辑后的效果不是很方便,总是需要先执行一次构建

Tricks

Snippet

为了方便,可以通过 YASnippet 或其他方式,设置 org 文件共用内容。

例如这是我用于写周刊的 snippet:

weekly snippet
# -*- mode: snippet -*-
# name: weekly
# key: weekly
# group: blog
# --

#+title: Zine#$1
#+INDEX: weekly!#$1
#+date: `(format-time-string "<%Y-%m-%d %a>" (current-time))`
#+lastmod: `(format-time-string "<%Y-%m-%d %a %H:%M>" (current-time))`
#+author: Spike Leung
#+email: l-yanlei@hotmail.com
#+description: ""
#+tags: weekly


* News | Article
$0
* Tutorial

* Code

* Cool Bit

* Tool | Library

* Music

内容的收起展开

需要开启 org-html-html5-fancy (13.9.3 HTML doctypes ¶),从而可以使用 <details>

(setq org-html-html5-fancy t)
(setq org-html-doctype "html5")

然后就可以这么用:

#+begin_details
#+HTML: <summary>Demo Code</summary>
#+begin_src JavaScript
console.log('Hello world!')
#+end_src
#+end_details

行内图片的插入

如果你想在文字中间插入一张图片avatar.jpg,同时你希望控制图片的大小,可以这样:

#+ATTR_HTML: :class inline-image
如果你想在文字中间插入一张图片[[file:./images/org-publish-blog/avatar.jpg]],同时你希望控制图片的大小,可以这样:

然后在 CSS 中通过类名设置样式:

img.inline-image {
  width: 1em;
  height: 1em;
  vertical-align: -3.5px;
}

TODO Todo

  • [X] 复用 :html-head:html-preamble
  • [X] 优化 RSS 的内容 -> 博客的一些更新
  • [X] 针对 zine 单独设置 RSS -> 好像没有什么必要 -> 已经实现了,见 zine.xml
  • [X] 增加搜索功能 -> search,使用 pagefind 实现,不过对于中文有的分词分的不是很好,可能搜索不到,凑合用着
  • [ ] 实现 CI/CD
  • [ ] 编写 Elisp,方便创建当前博客文章对应的图片目录,以及插入图片的 snippets

Refs

脚注:

1

Blogs and Wikis with Org 上有很多适用于 org-mode 的博客方案,最开始是看到 Eason0210 的博客 觉得挺简洁,就依葫芦画瓢更新了一下。

2

现在换成了 NodeJS 脚本生成,效率更高一些。见 博客的一些更新

3

结合 denote 和 org-publish 写博客 中,已经改成了 denote 的 date 格式。

4

用 rx 写正则表达式,太舒服了! 中,还提到了其他方法,例如覆盖 org-export-get-reference 的实现。

作 者: Spike Leung

创建于: 2024-09-27 Fri 21:40

修改于: 2025-09-03 Wed 07:40

许可证: CC BY-NC 4.0

支持我: 用你喜欢的方式