经过前面一些文章的介绍,我们已经知道了这两个 org 元素位于 <body>
的开头位置。我们可以在 preamble 中插入一些导航链接,就像 Sacha Chua 的博客一样:

注意到上面的 Home, Resources, Footer, Contact 和 Search 了吗,这一栏在她的博客的所有文章中都会出现,这就起到了导航作用。它的源代码如下(2023-02-04):
<div class="sticky contain-to-grid top-menu">
<nav class="top-bar">
<ul class="links">
<li><a href="/">Home</a></li>
<li><a href="/blog/resources">Resources</a></li>
<li><a href="#footer">Footer</a></li>
<li><a href="/blog/contact/">Contact</a></li>
<li><a href="/blog/search/">Search</a></li>
</ul>
</nav>
</div>
如果我们只用 home-and-up 的话,也可以像 taingram 这样,做一个“悬浮”的 home up 链接:

<div id="org-div-home-and-up">
<a href="https://taingram.org/blog">Blog</a>
<a href="https://taingram.org/">Home</a>
</div>
原先我打算做成 Shaca 那样的,现在发现对于单篇文章来说似乎不太必要,给读者 HOME 和 UP 页面的链接已经足够了。Sacha 博客的规模很恐怖,不过感觉她已经快写了二十年的博客了,倒也正常。我甚至都懒得给文章打上 tag。
虽说 org-html 的 home-and-link 中选项给我们限定死了只能用两个 %s
,但我们完全没必要按照它的来, org-html-home/up-format
可以设置为任何我们想要的内容,而且由于 elisp format
函数的特性,其中的 %s
我们不写也没关系:
(format "Hello world" 1 2) => Hello world
不过话又说回来,添加这些东西最直接的方式应该是在 org-html-head
变量中添加自己想要的 HTML 内容。这样得到的内容是自由程度最高的。
org 在导出 html 时会在许多块上添加一些 id
,这些 id 是使用 org-html--reference
生成的。虽然从显示上说这些 id
并没什么坏处,但是每次导出 HTML 这些 id 都会变一变,这就使得 git diff
命令会显示出一些不必要的红红绿绿。
最简单的想法就是 advice 一下 org-html--reference
,让它在每次调用时生成序号递增的 id
,这样只要 org 源文件中出现需要 id 的地方不发生变化的话,html 导出结果中的 id 也不会改变。另一种思路是对调用 org-html--reference
的函数使用 advice,让它使用我们想要的 id 或者干脆丢掉 id。下面是 org-html--reference
的代码:
(defun org-html--reference (datum info &optional named-only)
"Return an appropriate reference for DATUM.
DATUM is an element or a `target' type object. INFO is the
current export state, as a plist.
When NAMED-ONLY is non-nil and DATUM has no NAME keyword, return
nil. This doesn't apply to headlines, inline tasks, radio
targets and targets."
(let* ((type (org-element-type datum))
(user-label
(org-element-property
(pcase type
((or `headline `inlinetask) :CUSTOM_ID)
((or `radio-target `target) :value)
(_ :name))
datum)))
(cond
((and user-label
(or (plist-get info :html-prefer-user-labels)
;; Used CUSTOM_ID property unconditionally.
(memq type '(headline inlinetask))))
user-label)
((and named-only
(not (memq type '(headline inlinetask radio-target target)))
(not user-label))
nil)
(t
(org-export-get-reference datum info)))))
对于形如 <<abc>>
的内部链接,如果我们将 org-html-prefer-user-labels
设为 t
(默认为 nil),那么该链接的内容会成为供跳转的 id
。我将该变量设为 t
后 <<abc>>
和 <<<abc>>>
都能正确导出至 <a id="abc">abc</a>
,但是指向 <<<abc>>>
的链接给导出成了一个随机 id,有点奇怪。
经过一番调查,我发现 org-html-link
在寻找 radio target id 时使用的是 org-export-get-reference
而不是 org-html--reference
,而这就会导致一个问题:不论我们是否开启 org-html-prefer-user-labels
,radio target 的 名字 都不会被 org-html--reference
中的 org-export-get-reference
缓存。
org-html-link
在获取 radio link 指向的 target 时会使用 org-export-get-reference
生成的 id。而用于生成 target 的 org-html-target
在内部调用的是 org-html--reference
,在选项开启时就是 target 名字。这就使得 target 和 link 的 id 不一致,从而导致链接无效。这也许是个 bug,但是关注这个的人太少了所以没被发现(笑)。
要修复这个问题的话,我们可以将 org-html-link
中的 ((string= type "radio"))
分支处的 org-export-get-reference
改为 org-html--reference
。不过我更建议添加 override advice 而不是修改源代码。
下文中我们会一一分析 org-html--reference
出现的地方需要怎样修改,我发现有些地方完全是把 org-html--reference
拆开了来用(笑)。在本文的最后我会给出对 org-html--reference
和 org-export-get-reference
的修改代码。下面的代码都打开了 org-html-prefer-user-labels
选项。
在导出 headline 时,ox-html 会为每个 headline 生成随机的 id,就像这样:

整个 headline 的结构是这样的: <div><h2></h2><div>...</div></div>
,第一个 div
由 org-html-headline
生成,它负责包围 headline 及其内容, <h2>
中的内容是 headline,标题级别也不限于 h2
。第二个 div
由 org-html-section
生成。感觉这两个 div
中的 id
似乎都不怎么有必要。
我们到底需不需要这第二个 div
呢?我在网上找到了一个博客的做法:org-mode在publish时去掉多余的div和id。这位博主的做法比较激进,直接通过装饰器覆盖了 org-html-headline
函数,去掉了两个 div
块,只保留了标题和内容,下图是一整个原 headline 内容的 HTML 代码截图:

这位博主的文章中没有 toc,所以标题的 id 有无也不太重要,但是 toc 我还是要的,所以 id 也必须得有。指定标题 id 的一种方式是使用 CUSTOM_ID
,这样标题的 id 值就会使用 CUSTOM_ID
指定的字符串,但这样一来我们想要每个标题都不使用随机字符串的话就得给所有标题加上 CUSTOM_ID
,还挺麻烦的。
我们可以修改 org-html-headline
来生成确定的 id,也可以修改 org-html--reference
来专门为标题生成 id。结合上一节中我提到的 org-html--reference
,这里我选择修改 org-html--reference
,并且修改 org-html-headline
和 org-html-section
,移除掉其中不需要的 id
。由于这几个函数都比较长,这里我只给出修改思路,我会在文末给出所有修改的完整代码。
对 org-html-headline
:
- 将 outline-container 格式化字符串中的 id 删除,同时删掉 format 中的对应参数项
对 org-html-section
:
- 将格式化字符串中的 id 删除,同时删除对应参数项
对 org-html--reference
的修改:
对 headline 做特殊处理,使用对应于 headline 序号的 id,简单截一段:
(when (eq type 'headline) (unless user-label (let ((numbers (org-export-get-headline-number datum info))) (setq user-label (concat "org-h-" (mapconcat #'number-to-string numbers "-"))))))
下面是修改后的效果:

现在,得到的 section
块中的各 id 值就和当前位置有关了,而不是一个根据内容随机生成的字符串。这里我选择修改 org-html--reference
而不是把 id 生成写死在 org-html-headline
中的原因是 org-html--format-toc-headline
会在内部调用 org-html--reference
来获取各 headline 的 id,如果我们只修改 org-html-headline
的话是无法让两者同步的,下图是修改 org-html--reference
后生成的 toc:

之前的文章我已经展示过我对图片导出吹毛求疵的 advice 代码(笑):
(defun ad-org-html--wrap-image (st)
(replace-regexp-in-string "\n\n" "\n" st))
(advice-add 'org-html--wrap-image :filter-return 'ad-org-html--wrap-image)
(advice-remove 'org-html--wrap-image 'ad-org-html--wrap-image)
本来我以为这应该已经结束了,但是我当时居然没有注意到图片会带 id
:

我们可以通过 #+NAME
或 #+LABEL
来设置这个 id,但是当我们不设置时 ox-html 还是会为我们生成一个随机的 id,这里需要我们对 org-html--reference
做一些修改,我们加上以下代码即可:
(when (org-html-standalone-image-p datum info)
(unless user-label (setq user-label "")))
同样,我们可以修改 org-html--wrap-image
来去掉一些不愉快的空格:
(defun yynt|org-html--wrap-image (contents info &optional caption label)
"Wrap CONTENTS string within an appropriate environment for images.
INFO is a plist used as a communication channel. When optional
arguments CAPTION and LABEL are given, use them for caption and
\"id\" attribute."
(let ((html5-fancy (org-html--html5-fancy-p info)))
(format (if html5-fancy "\n<figure%s>\n%s%s</figure>" ;;去掉了最后一个 \n
"\n<div%s class=\"figure\">\n%s%s\n</div>")
;; ID.
(if (org-string-nw-p label) (format " id=\"%s\"" label) "")
;; Contents.
(if html5-fancy contents (format "<p>%s</p>" contents))
;; Caption.
(if (not (org-string-nw-p caption)) ""
(format (if html5-fancy "<figcaption>%s</figcaption>\n" ;; 去掉和加上 \n
"\n<p>%s</p>")
caption)))))
(advice-add 'org-html--wrap-image :override 'yynt|org-html--wrap-image)
;;(advice-remove 'org-html--wrap-image 'yynt|org-html--wrap-image)

关于图片的一个比较好玩的功能是,如果 org 中的链接是以 [[high_resolution][low_resolution]]
的形式给出的,那么这张图片显示 low_resolution
的图片,如果你点击它,你会跳转到 high_resolution
的位置,比如 [[https://www.pixiv.net/artworks/90486424][file:./0.png]]
。行间导出时它会以 <a href=xxx><img src=xxxx></a>
的形式导出,单独导出时会在外面加上 <figure>
,以下是示范效果:
这个功能运转正常,我也没什么好修改的。
当我们使用非标准 #+BEGIN
时,ox-html 会将它们导出为 div
块。如果我们开启了 html5 且 BEGIN 后面跟的名字还是 html5 的元素的话,ox-html 会将它们导出为相应的块元素,比如:

同样,我也不想要这里的 id
,我们可以继续修改 org-html--reference
:
(when (eq type 'special-block)
(unless user-label
(setq user-label nil)
(setq named-only t)))
加上 advice 后,导出结果如下:
<aside>
hello
</aside>
到了这里,我们把之前提到的 radio target 和 radio link 的问题解决一下。我们要么修改 org-html-link
,要么修改 org-export-get-reference
来解决这个问题。这里我选择修改 org-html-link
,将对 radio link 使用的 org-exporg-get-reference
改为 org-html--reference
即可。代码很长,我会放在文章最后的 gist 链接中。
之前的文章中我也对 source block 的导出做了一点简单的分析,现在让我们参考参考别人的使用经验,提高一下 html 中的源代码块体验。
我之所以要把所有的代码都放到最后的 gist 上是因为这些代码太长了,那么我们可不可以通过 <details><summary>...</summary>...</details>
把代码块折叠起来呢?结合 ox-html 提供的 html5 标签,我们可以这样做:
#+BEGIN_details
#+BEGIN_summary
‣hello
#+END_summary
#+BEGIN_SRC c
#include <stdio.h>
int main(int argc, char *argv[])
{
return 0;
}
#+END_SRC
#+END_details
这样确实实现了功能,但是这样一来代码高亮就看不到了,Sacha 在这篇文章中展示了另外一种方法,直接修改 org-html 的导出行为:
(setq org-babel-exp-code-template "#+begin_src %lang%switches%flags :summary %summary\n%body\n#+end_src")
(defun my-org-html-src-block (src-block _contents info)
(let* ((result (org-html-src-block src-block _contents info))
(block-info
(org-with-point-at (org-element-property :begin src-block)
(org-babel-get-src-block-info)))
(summary (assoc-default :summary (elt block-info 2))))
(if (member summary '("%summary" ""))
result
(format "<details><summary>%s</summary>%s</details>"
summary
result))))
(with-eval-after-load 'ox-html
(map-put!
(org-export-backend-transcoders (org-export-get-backend 'html))
'src-block 'my-org-html-src-block))
同样是在 Sacha Chua 的博客中我找到了一段可以实现复制功能的代码。内容如下:
/* Start of copy code */
// based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
const copyLabel = 'Copy code';
async function copyCode(block, button) {
let code = block.querySelector('pre.src');
let text = code.innerText;
await navigator.clipboard.writeText(text);
button.innerText = 'Copied';
setTimeout(() => {
button.innerText = copyLabel;
}, 500);
}
function addCopyCodeButtons() {
if (!navigator.clipboard) return;
let blocks = document.querySelectorAll('.org-src-container');
blocks.forEach((block) => {
let button = document.createElement('button');
button.innerText = copyLabel;
button.classList.add('copy-code');
let details = block.closest('details');
let summary = details && details.querySelector('summary');
if (summary) {
summary.appendChild(button);
} else {
block.appendChild(button);
}
button.addEventListener('click', async() => {
await copyCode(block, button);
});
block.setAttribute('tabindex', 0);
});
}
document.addEventListener("DOMContentLoaded", function(event) {
addCopyCodeButtons();
});
/* End of copy code */
参考文章中的 CSS,我们还可以设置按钮的样式,详细内容可以阅读原文来进一步了解。
所有的代码我都放到了 gist 上,这里就不占用空间了。
关于 org 的 html 导出到这里应该就告一段落了,之后的改进重点应该是在 CSS 和 JS 上了。如果我还会写和 org 的 html 导出有关的文章,那应该是我在写 org 的 HTML 导出后端的时候(笑)。
关于一些不错的博客,这里再推荐两个:
Sacha Chua 的 emacs 配置值得一看,她的 org 导出设定具有借鉴意义。