使用 org-publish 发布博客
博客经历过几次更新,最开始的时候是用 markdown 写博客,然后用 Hexo 发布。
然后学习了 Vue,又尝试用 VuePress 发布,它们都是托管在 Github Pages 上。
后来接触了 Emacs,就从 markdown 迁移到了 org-mode,然后使用 Hugo 1发布。
因为 GitHub 直连变慢了,所以将内容托管到了 Vercel 和 Netlify。
最近将博客又更新了一下,使用 org-publish 进行博客的发布,主要是让博客加载相对快一些,维护起来更简单一些。
相关代码见 init-org-publish.el。
动机
在很早之前看过几篇文章:
- mother fucking website
- better mother fucking website
- the best mother fucking website
- perfect mother fucking website
设计复杂的网站,在网络较差的情况下有可能要加载很久才能显示。
如果样式比较复杂, 在一些小屏幕下的显示可能不好。
所以作者提出网站要保持简洁,这次也主要是基于作者提出的建议修改了网站。
相比之前的变化
- 极大的简化了博客内容,没有主题,没有模板,只有博客内容的 org 文件、css 文件、字体、图片,以及最终转换出来的 HTML 文件
- 博客的编写、构建只需要 Emacs 就能完成(以及 Browsersync 用于本地查看构建的 HTML)
- 转换后就是标准的 HTML,而且是使用 Emacs 内置的包转换 org 文件,理论上 org 的特性应该都能转换
- 按照 better mother fucking website 和 the 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 的日期格式 (3
),这样 sitemap 就能正常按照时间排序了。缓存问题
执行 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.
因此 ,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.
`
和 '
的不同是,它会执行 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_ID
。4
相关代码
(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
行内图片的插入
如果你想在文字中间插入一张图片,同时你希望控制图片的大小,可以这样:
#+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
Refs
脚注:
Blogs and Wikis with Org 上有很多适用于 org-mode 的博客方案,最开始是看到 Eason0210 的博客 觉得挺简洁,就依葫芦画瓢更新了一下。
在 结合 denote 和 org-publish 写博客 中,已经改成了 denote 的 date 格式。
在 用 rx 写正则表达式,太舒服了! 中,还提到了其他方法,例如覆盖 org-export-get-reference
的实现。