百般武艺,此乃 Emacs (一):用 Emacs 写博客

2024-02-21 2024-03-09History2628 字

本文简要介绍了如何使用 Emacs 来生成静态博客网站。主要分为三个模块:

  1. 博客生成
  2. RSS 生成
  3. 文学编程网页导出

使用 ox-publish.el 来生成博客

为什么选择 ox-publish.el

在 Emacs 里写博客的方案有很多,比如说 Org + Hugo 、Markdown/Org + Jekyll 等等。它们各有各的优点,但这不是本文的讨论重点,有兴趣的读者可以自行查阅。我之所以使用 ox-publish.el ,主要有以下几点原因:

兼容性

我基本上所有的写作活动都是在 Org-mode 里完成的,对 Org-mode 有着比较深度的自定义配置,因此希望能直接基于 org 文件来生成博客。Hugo 虽然号称能够解析 org 格式,但是对于自定义的格式就无能为力了,比如说自定义的链接格式。而 ox-publish.el 采用的是 Org-mode 自带的导出功能,只需要你自己写好相关格式的导出方式,便可以完美地支持任何 org 文件。

内置

ox-publish.el 中的 oxorg-export 的缩写,从名字就能看出来它是 Org-mode 导出功能的一个模块。因此可以和 Org-mode 的其他功能功能无缝整合,任何需求都能较为简单地实现。内置的另一个好处是我不用担心这个项目后续的发展,只要 Org-mode 还能用, ox-publish.el 就能用。

可拓展性

Org-mode 的 ox 模块提供了很多 hook ,比如能够分别在导出前后修改原始文本(的复制)和导出文本。这基本上满足我的绝大部分个性化需求,实在不行我也可以通过 advice 来修改原有函数,总之可扩展性非常好强。

博客结构

首先介绍一下博客的基本结构

./
├── articles/
├── css/
├── orgs/
├── scripts/
├── static/
├── config.html
├── index.html
├── rss.xml
└── sitemap.xml
articles
此文件夹包含所有的博客文章
css
此文件夹包含所有的 CSS 文件
orgs
此文件夹包含所有的 Org 源文件
script
此文件夹包含所有的 JS 文件
static
此文件夹包含所有的其他静态资源
config.html
我的 Emacs 文学编程配置
index.html
博客主页
rss.xml
RSS 订阅
sitemap.xml
网站站点地图,用于在 Google Search Console 中建立索引

基本配置

ox-publish.el 的配置几乎只用一个变量 org-publish-project-alist 就能完成,非常简单。相关语法和用法请查阅 org-publish-project-alist 的文档和 Publishing (The Org Manual) ,本文将侧重于分享我的个人配置。

org-publish-project-alist 中个每个元素就是一个发布项目,其由一个项目名和一个 property list 组成。如

1: ("eli's blog"
2:  <<misc>>
3: 
4:  <<sitemap>>
5: 
6:  <<html>>
7:  )
 1: ("eli's blog"
 2:  :base-directory ,eli/blog-base-dir
 3:  :publishing-directory ,(expand-file-name "articles" eli/blog-publish-dir)
 4:  :base-extension "org"
 5:  :recursive nil
 6:  :htmlized-source t
 7:  :publishing-function eli/org-blog-publish-to-html
 8:  :exclude "rss.org"
 9: 
10:  :auto-sitemap t
11:  :preparation-function eli/kill-sitemap-buffer
12:  :completion-function eli/blog-publish-completion
13:  :sitemap-filename ,eli/blog-sitamap
14:  :sitemap-title "Eli's Blog"
15:  :sitemap-sort-files anti-chronologically
16:  :sitemap-function eli/org-publish-sitemap
17:  :sitemap-format-entry eli/sitemap-dated-entry-format
18: 
19:  :html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/style.css\" />
20:   <link rel=\"stylesheet\" type=\"text/css\" href=\"/css/htmlize.css\" />
21:   <script src=\"/scripts/script.js\"></script>
22:   <script src=\"/scripts/toc.js\"></script>"
23:  :html-preamble t
24:  :html-preamble-format (("en" "<nav class=\"nav\">
25:    <a href=\"/index.html\" class=\"button\">Home</a>
26:    <a href=\"/rss.xml\" class=\"button\">RSS</a>
27:    <a href=\"/config.html\" class=\"button\">Literate Emacs Config</a>
28:  </nav>
29:  <hr>"))
30:  :html-postamble t
31:  :html-postamble-format (("en" "<hr class=\"Solid\">
32:  <div class=\"info\">
33:    <span class=\"author\">Author: %a (%e)</span>
34:    <span class=\"date\">Create Date: %d</span>
35:    <span class=\"date\">Last modified: %C</span>
36:    <span>Creator: %c</span>
37:  </div>"))
38:  :with-creator nil
39:  )

我们的主要工作就是配置这个 property list 以满足我们不同的需求,接下来的几个部分将介绍我们所使用的属性:

自定义导出后端

我希望博客的导出和常规 HTML 之间的行为有所区别,所以首先让我们定义一个专门用于博客导出的 org 导出后端 blog ,这是为了方便修改html 后端的默认行为(通过 :translate-alist 等其他方式实现)。

1: (org-export-define-derived-backend 'blog 'html
2:   :translate-alist '((src-block . eli/org-blog-src-block)
3:                      (footnote-reference . eli/org-blog-footnote-reference)
4:                      (template . eli/org-blog-template)))

我们分别修改了代码块、文档注释和导出模板:

代码块

由于博客文章中的代码块的组织方式是以文学编程编程的方式来处理的,而我的习惯是仅使用 name 属性来命名代码块,所以我希望在没有 caption 的时候用 name 属性来作为代码块的标签:

 1: (defun eli/org-blog-src-block (src-block _contents info)
 2:   "Transcode a SRC-BLOCK element from Org to HTML.
 3: CONTENTS holds the contents of the item.  INFO is a plist holding
 4: contextual information."
 5:   (if (org-export-read-attribute :attr_html src-block :textarea)
 6:       (org-html--textarea-block src-block)
 7:     (let* ((lang (org-element-property :language src-block))
 8:            (code (org-html-format-code src-block info))
 9:            (label (let ((lbl (org-html--reference src-block info t)))
10:                     (if lbl (format " id=\"%s\"" lbl) "")))
11:            (klipsify  (and  (plist-get info :html-klipsify-src)
12:                             (member lang '("javascript" "js"
13:                                            "ruby" "scheme" "clojure" "php" "html")))))
14:       (if (not lang) (format "<pre class=\"example\"%s><code>\n%s</code></pre>" label code)
15:         (format "<div class=\"org-src-container\">\n%s%s\n</div>"
16:                 ;; Build caption.
17:                 (let ((caption (or (org-export-get-caption src-block)
18:                                    (org-element-property :name src-block))))
19:                   (if (not caption) ""
20:                     (let ((listing-number
21:                            (format
22:                             "<span class=\"listing-number\">%s </span>"
23:                             "Listing: ")))
24:                       (format "<div class=\"org-src-name\">%s%s</div>"
25:                               listing-number
26:                               (org-trim (org-export-data caption info))))))
27:                 ;; Contents.
28:                 (if klipsify
29:                     (format "<pre><code class=\"src src-%s\"%s%s>%s</code></pre>"
30:                             lang
31:                             label
32:                             (if (string= lang "html")
33:                                 " data-editor-type=\"html\""
34:                               "")
35:                             code)
36:                   (format "<pre class=\"src src-%s\"%s><code>%s</code></pre>"
37:                           lang label code)))))))
注释

我们对注释也需要额外处理。默认的处理方式是放在页面的末尾,这其实是不利于读者阅读的,经常前后跳转可能会打断读者的心流。所以我们更希望采用侧注的方式,方便读者就近查阅。而对于移动设备,我们希望采用弹出注释的方式:

Peek 2024-03-08 17-58.gif
Figure 1: show-annotations

为了实现上述需求,仅使用 HTML 是不够的,还需要 CSS/JS 的帮助,这部分细节可以在仓库 GitHub - Elilif/Elilif.github.io 查看,本文专注于导出部分。下面的代码在原有基础上添加了几个标签,方便后续处理。

 1: (defun eli/org-blog-footnote-reference (footnote-reference _contents info)
 2:   "Transcode a FOOTNOTE-REFERENCE element from Org to HTML.
 3: CONTENTS is nil.  INFO is a plist holding contextual information."
 4:   (concat
 5:    ;; Insert separator between two footnotes in a row.
 6:    (let ((prev (org-export-get-previous-element footnote-reference info)))
 7:      (when (eq (org-element-type prev) 'footnote-reference)
 8:        (plist-get info :html-footnote-separator)))
 9:    (let* ((n (org-export-get-footnote-number footnote-reference info))
10:           (id (format "fnr.%d%s"
11:                       n
12:                       (if (org-export-footnote-first-reference-p
13:                            footnote-reference info)
14:                           ""
15:                         ".100"))))
16:      (format
17:       (concat (plist-get info :html-footnote-format)
18:               "<input id=\"%s\" class=\"footref-toggle\" type=\"checkbox\">")
19:       (format "<label for=\"%s\" class=\"footref\">%s</label>"
20:               id n)
21:       id))))
导出模板

最后我们需要修改下默认的导出模板:

 1: (defun eli/org-blog-template (contents info)
 2:   "Return complete document string after HTML conversion.
 3: CONTENTS is the transcoded contents string.  INFO is a plist
 4: holding export options."
 5:   (setq eli-test info)
 6:   (concat
 7:    (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
 8:      (let* ((xml-declaration (plist-get info :html-xml-declaration))
 9:             (decl (or (and (stringp xml-declaration) xml-declaration)
10:                       (cdr (assoc (plist-get info :html-extension)
11:                                   xml-declaration))
12:                       (cdr (assoc "html" xml-declaration))
13:                       "")))
14:        (when (not (or (not decl) (string= "" decl)))
15:          (format "%s\n"
16:                  (format decl
17:                          (or (and org-html-coding-system
18:                                   ;; FIXME: Use Emacs 22 style here, see `coding-system-get'.
19:                                   (coding-system-get org-html-coding-system 'mime-charset))
20:                              "iso-8859-1"))))))
21:    (org-html-doctype info)
22:    "\n"
23:    (concat "<html"
24:            (cond ((org-html-xhtml-p info)
25:                   (format
26:                    " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
27:                    (plist-get info :language) (plist-get info :language)))
28:                  ((org-html-html5-p info)
29:                   (format " lang=\"%s\"" (plist-get info :language))))
30:            ">\n")
31:    "<head>\n"
32:    (org-html--build-meta-info info)
33:    (org-html--build-head info)
34:    (org-html--build-mathjax-config info)
35:    "</head>\n"
36:    "<body>\n"
37:    (let ((link-up (org-trim (plist-get info :html-link-up)))
38:          (link-home (org-trim (plist-get info :html-link-home))))
39:      (unless (and (string= link-up "") (string= link-home ""))
40:        (format (plist-get info :html-home/up-format)
41:                (or link-up link-home)
42:                (or link-home link-up))))
43:    ;; Preamble.
44:    (org-html--build-pre/postamble 'preamble info)
45:    ;; Document contents.
46:    (let ((div (assq 'content (plist-get info :html-divs))))
47:      (format "<%s id=\"%s\" class=\"%s\">\n"
48:              (nth 1 div)
49:              (nth 2 div)
50:              (plist-get info :html-content-class)))
51:    ;; Document title.
52:    (when (plist-get info :with-title)
53:      (let ((title (and (plist-get info :with-title)
54:                        (plist-get info :title)))
55:            (subtitle (plist-get info :subtitle))
56:            (html5-fancy (org-html--html5-fancy-p info)))
57:        (when title
58:          (format
59:           (if html5-fancy
60:               "<header>\n<h1 class=\"title\">%s</h1>\n%s</header>"
61:             "<h1 class=\"title\">%s%s</h1>\n")
62:           (org-export-data title info)
63:           (if subtitle
64:               (format
65:                (if html5-fancy
66:                    "<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n"
67:                  (concat "\n" (org-html-close-tag "br" nil info) "\n"
68:                          "<span class=\"subtitle\">%s</span>\n"))
69:                (org-export-data subtitle info))
70:             "")))))
71:    ;; add article status
72:    (eli/blog-build-article-status info)
73:    contents
74:    (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
75:    ;; gisus
76:    (eli/blog-build-giscus info)
77:    ;; Postamble.
78:    (org-html--build-pre/postamble 'postamble info)
79:    ;; Possibly use the Klipse library live code blocks.
80:    (when (plist-get info :html-klipsify-src)
81:      (concat "<script>" (plist-get info :html-klipse-selection-script)
82:              "</script><script src=\""
83:              org-html-klipse-js
84:              "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
85:              org-html-klipse-css "\"/>"))
86:    ;; Closing document.
87:    "</body>\n</html>"))

目前主要是两个部分:一是增加标题下的文章信息;二是添加评论模块(使用 giscus )。注意这两个信息我们都不希望添加到主页中,所以在后面的代码中都做了相应的判断。

添加文章信息部分逻辑很简单,就是组合一些字符串,代码如下:

 1: (defvar eli/blog-status-format "<span><i class='bx bx-calendar'></i>
 2: <span>%d</span></span>\n<span><i class='bx bx-edit'></i><span>%C</span></span>")
 3: (defvar eli/blog-history-base-url "https://github.com/Elilif/Elilif.github.io/commits/master/orgs/")
 4: 
 5: (defun eli/blog-build-article-status (info)
 6:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
 7:     (unless (string-equal input-file eli/blog-sitamap)
 8:       (let ((spec (org-html-format-spec info))
 9:             (history-url (concat eli/blog-history-base-url input-file)))
10:         (concat
11:          "<div class=\"post-status\">"
12:          (format-spec eli/blog-status-format spec)
13:          (format "<span><i class='bx bx-history'></i><span><a href=\"%s\">history</a></span></span>"
14:                  history-url)
15:          "</div>")))))

效果:

2024-03-08_18-40.png
Figure 2: article-status

添加评论模块的代码如下:

 1: (defvar eli/blog-giscus-script "<script src=\"https://giscus.app/client.js\"
 2:           data-repo=\"Elilif/Elilif.github.io\"
 3:           data-repo-id=\"MDEwOlJlcG9zaXRvcnkyOTgxNjM5ODg=\"
 4:           data-category=\"Announcements\"
 5:           data-category-id=\"DIC_kwDOEcWfFM4Cdz5V\"
 6:           data-mapping=\"pathname\"
 7:           data-strict=\"0\"
 8:           data-reactions-enabled=\"1\"
 9:           data-emit-metadata=\"0\"
10:           data-input-position=\"top\"
11:           data-theme=\"light\"
12:           data-lang=\"zh-CN\"
13:           crossorigin=\"anonymous\"
14:           async>
15:   </script>")
16: 
17: (defun eli/blog-build-giscus (info)
18:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
19:     (unless (string-equal input-file eli/blog-sitamap)
20:       eli/blog-giscus-script)))

到这里,我们自定义的导出后端就定义完成了。最后我们需要为 ox-publish 定义一个导出后端为 blog 的发布函数:

 1: ;;;###autoload
 2: (defun eli/org-blog-publish-to-html (plist filename pub-dir)
 3:   "Publish an org file to HTML.
 4: 
 5: FILENAME is the filename of the Org file to be published.  PLIST
 6: is the property list for the given project.  PUB-DIR is the
 7: publishing directory.
 8: 
 9: Return output file name."
10:   (org-publish-org-to 'blog filename
11:                       (concat (when (> (length org-html-extension) 0) ".")
12:                               (or (plist-get plist :html-extension)
13:                                   org-html-extension
14:                                   "html"))
15:                       plist pub-dir))

杂项部分

为了方便后续的编辑,我们定义如下几个变量:

1: (setq eli/blog-base-dir "path-to/blog/"
2:       eli/blog-publish-dir "your-blog-site-dir"
3:       eli/blog-sitamap "index.org")

property list 中的杂项部分如下,基本上看名字就知道是什么意思,下面这些未指出的属性读者可以查阅Publishing (The Org Manual) 等文档,这里就不一一说明了。

1: :base-directory ,eli/blog-base-dir
2: :publishing-directory ,(expand-file-name "articles" eli/blog-publish-dir)
3: :base-extension "org"
4: :recursive nil
5: :htmlized-source t
6: :publishing-function eli/org-blog-publish-to-html
7: :exclude "rss.org"

HTML 部分

org 文件导出后的 HTML 主要由 preamblecontentpostamble 三个部分组成,我们的文章内容填充的是 content 部分,其他两个部分由变量 org-html-preambleorg-html-preamble-formatorg-html-postambleorg-html-postamble-format 分别控制。其具体用途可任由读者发挥,这里我们将 preamle 用做导航栏,而 postamble 则用作提供文章信息。

导航栏是几个很简单的标签

1: (("en" "<nav class=\"nav\">
2:   <a href=\"/index.html\" class=\"button\">Home</a>
3:   <a href=\"/rss.xml\" class=\"button\">RSS</a>
4:   <a href=\"/config.html\" class=\"button\">Literate Emacs Config</a>
5: </nav>
6: <hr>"))

postamble 则提供了作者、创建时间和修改时间等信息

1: (("en" "<hr class=\"Solid\">
2: <div class=\"info\">
3:   <span class=\"author\">Author: %a (%e)</span>
4:   <span class=\"date\">Create Date: %d</span>
5:   <span class=\"date\">Last modified: %C</span>
6:   <span>Creator: %c</span>
7: </div>"))

此外,我们还可以为导出的 HTML 提供 CSS 和 JS 文件,以获取更舒适的浏览体验

至此,property list 中的 HTML 部分就大致完成了:

1: :html-head <<head>>
2: :html-preamble t
3: :html-preamble-format <<blog-preamble>>
4: :html-postamble t
5: :html-postamble-format <<blog-postamble>>
6: :with-creator nil

sitemap 部分

sitemap 就是站点地图,其中列举了项目中的所有文章链接,这样通过 sitemap 就可以访问全部的文章内容。因此我们可以把 sitemap 稍作修改,用作我们博客的主页。

首先我们为博客主页固定一个创建时间:

1: (defun eli/org-publish-sitemap (title list)
2:   "Generate the sitemap with title."
3:   (concat "#+TITLE: " title
4:           "\n"
5:           "#+DATE: 2023-10-10"
6:           "\n\n"
7:           (org-list-to-org list)))

其次我们可以为 sitemap 中的每一个 entry 添加时间前缀:

 1: (defun eli/sitemap-dated-entry-format (entry _style project)
 2:   "Sitemap PROJECT ENTRY STYLE format that includes date."
 3:   (let* ((file (org-publish--expand-file-name entry project))
 4:          (parsed-title (org-publish-find-property file :title project))
 5:          (title
 6:           (if parsed-title
 7:               (org-no-properties
 8:                (org-element-interpret-data parsed-title))
 9:             (file-name-nondirectory (file-name-sans-extension file)))))
10:     (org-publish-cache-set-file-property file :title title)
11:     (if (= (length title) 0)
12:         (format "*%s*" entry)
13:       (format "{{{timestamp(%s)}}}   [[file:%s][%s]]"
14:               (car (org-publish-find-property file :date project))
15:               (concat "articles/" entry)
16:               title))))

注意 eli/sitemap-dated-entry-format 里的 {{{timestamp(%s)}}} 是一个导出宏:

1: (add-to-list 'org-export-global-macros
2:              '("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@"))

一般情况下 sitemap 会和同一 :base-directory 目录下的其他文件一起导出到 :publishing-directory ,但从博客结构一节可以看出,我们希望把 sitemap 导出到博客的根目录来充当主页,所以需要利用 :completion-function 属性来在导出完成后把 sitemap 移到根目录

1: (defun eli/blog-publish-completion (project)
2:   (let* ((publishing-directory (plist-get project :publishing-directory))
3:          (sitamap (file-name-with-extension eli/blog-sitamap "html"))
4:          (orig-file (expand-file-name sitamap publishing-directory))
5:          (target-file (expand-file-name
6:                        sitamap
7:                        (file-name-directory publishing-directory))))
8:     (rename-file orig-file target-file t)))

最后还有一点需要注意,在 ox-publish.el 的实现过程中,ox-publish 会优先使用正在访问博客文件的 buffer 来作为导出的来源,这在一般情况下没什么问题,但是如果你在导出前在 buffera 打开了 sitemap 文件,那么 ox-publish 就会在重新生成 sitemap 后继续使用 buffera 中的内容。此时的行为会受 auto-revert 或其他相关设置的影响,比如说此时 auto-revert-timer 的定时还没有到,那么 ox-publish 就会使用旧的 sitemap 内容。这不是我们想要的,所以我的方案是在导出前关闭访问 sitemap 的 buffer ,反正 sitemap 是自动生成的,我们也不需要修改。

1: (defun eli/kill-sitemap-buffer (project)
2:   (let* ((sitemap-filename (plist-get project :sitemap-filename))
3:          (base-dir (plist-get project :base-directory))
4:          (sitemap-filepath (expand-file-name sitemap-filename base-dir)))
5:     (when-let ((sitemap-buffer (find-buffer-visiting sitemap-filepath)))
6:       (kill-buffer sitemap-buffer))))

至此,一个基本的博客主页就完成了,下面是相应的属性:

1: :auto-sitemap t
2: :preparation-function eli/kill-sitemap-buffer
3: :completion-function eli/blog-publish-completion
4: :sitemap-filename ,eli/blog-sitamap
5: :sitemap-title "Eli's Blog"
6: :sitemap-sort-files anti-chronologically
7: :sitemap-function eli/org-publish-sitemap
8: :sitemap-format-entry eli/sitemap-dated-entry-format

本章总结

现在我们已经介绍完了所有需要的属性,接下来让我们让我们把这些属性合到一起,加上名字组成一个完整的 project 。至此,一个简单的博客导出工具就完成了:

  1: (defun eli/kill-sitemap-buffer (project)
  2:   (let* ((sitemap-filename (plist-get project :sitemap-filename))
  3:          (base-dir (plist-get project :base-directory))
  4:          (sitemap-filepath (expand-file-name sitemap-filename base-dir)))
  5:     (when-let ((sitemap-buffer (find-buffer-visiting sitemap-filepath)))
  6:       (kill-buffer sitemap-buffer))))
  7: 
  8: (defun eli/blog-publish-completion (project)
  9:   (let* ((publishing-directory (plist-get project :publishing-directory))
 10:          (sitamap (file-name-with-extension eli/blog-sitamap "html"))
 11:          (orig-file (expand-file-name sitamap publishing-directory))
 12:          (target-file (expand-file-name
 13:                        sitamap
 14:                        (file-name-directory publishing-directory))))
 15:     (rename-file orig-file target-file t)))
 16: 
 17: (add-to-list 'org-export-global-macros
 18:              '("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@"))
 19: 
 20: (defun eli/sitemap-dated-entry-format (entry _style project)
 21:   "Sitemap PROJECT ENTRY STYLE format that includes date."
 22:   (let* ((file (org-publish--expand-file-name entry project))
 23:          (parsed-title (org-publish-find-property file :title project))
 24:          (title
 25:           (if parsed-title
 26:               (org-no-properties
 27:                (org-element-interpret-data parsed-title))
 28:             (file-name-nondirectory (file-name-sans-extension file)))))
 29:     (org-publish-cache-set-file-property file :title title)
 30:     (if (= (length title) 0)
 31:         (format "*%s*" entry)
 32:       (format "{{{timestamp(%s)}}}   [[file:%s][%s]]"
 33:               (car (org-publish-find-property file :date project))
 34:               (concat "articles/" entry)
 35:               title))))
 36: 
 37: (defun eli/org-publish-sitemap (title list)
 38:   "Generate the sitemap with title."
 39:   (concat "#+TITLE: " title
 40:           "\n"
 41:           "#+DATE: 2023-10-10"
 42:           "\n\n"
 43:           (org-list-to-org list)))
 44: 
 45: (setq eli/blog-base-dir "path-to/blog/"
 46:       eli/blog-publish-dir "your-blog-site-dir"
 47:       eli/blog-sitamap "index.org")
 48: 
 49: ;;;###autoload
 50: (defun eli/org-blog-publish-to-html (plist filename pub-dir)
 51:   "Publish an org file to HTML.
 52: 
 53: FILENAME is the filename of the Org file to be published.  PLIST
 54: is the property list for the given project.  PUB-DIR is the
 55: publishing directory.
 56: 
 57: Return output file name."
 58:   (org-publish-org-to 'blog filename
 59:                       (concat (when (> (length org-html-extension) 0) ".")
 60:                               (or (plist-get plist :html-extension)
 61:                                   org-html-extension
 62:                                   "html"))
 63:                       plist pub-dir))
 64: 
 65: (defvar eli/blog-giscus-script "<script src=\"https://giscus.app/client.js\"
 66:           data-repo=\"Elilif/Elilif.github.io\"
 67:           data-repo-id=\"MDEwOlJlcG9zaXRvcnkyOTgxNjM5ODg=\"
 68:           data-category=\"Announcements\"
 69:           data-category-id=\"DIC_kwDOEcWfFM4Cdz5V\"
 70:           data-mapping=\"pathname\"
 71:           data-strict=\"0\"
 72:           data-reactions-enabled=\"1\"
 73:           data-emit-metadata=\"0\"
 74:           data-input-position=\"top\"
 75:           data-theme=\"light\"
 76:           data-lang=\"zh-CN\"
 77:           crossorigin=\"anonymous\"
 78:           async>
 79:   </script>")
 80: 
 81: (defun eli/blog-build-giscus (info)
 82:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
 83:     (unless (string-equal input-file eli/blog-sitamap)
 84:       eli/blog-giscus-script)))
 85: 
 86: (defvar eli/blog-status-format "<span><i class='bx bx-calendar'></i>
 87: <span>%d</span></span>\n<span><i class='bx bx-edit'></i><span>%C</span></span>")
 88: (defvar eli/blog-history-base-url "https://github.com/Elilif/Elilif.github.io/commits/master/orgs/")
 89: 
 90: (defun eli/blog-build-article-status (info)
 91:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
 92:     (unless (string-equal input-file eli/blog-sitamap)
 93:       (let ((spec (org-html-format-spec info))
 94:             (history-url (concat eli/blog-history-base-url input-file)))
 95:         (concat
 96:          "<div class=\"post-status\">"
 97:          (format-spec eli/blog-status-format spec)
 98:          (format "<span><i class='bx bx-history'></i><span><a href=\"%s\">history</a></span></span>"
 99:                  history-url)
100:          "</div>")))))
101: 
102: (defun eli/org-blog-template (contents info)
103:   "Return complete document string after HTML conversion.
104: CONTENTS is the transcoded contents string.  INFO is a plist
105: holding export options."
106:   (setq eli-test info)
107:   (concat
108:    (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
109:      (let* ((xml-declaration (plist-get info :html-xml-declaration))
110:             (decl (or (and (stringp xml-declaration) xml-declaration)
111:                       (cdr (assoc (plist-get info :html-extension)
112:                                   xml-declaration))
113:                       (cdr (assoc "html" xml-declaration))
114:                       "")))
115:        (when (not (or (not decl) (string= "" decl)))
116:          (format "%s\n"
117:                  (format decl
118:                          (or (and org-html-coding-system
119:                                   ;; FIXME: Use Emacs 22 style here, see `coding-system-get'.
120:                                   (coding-system-get org-html-coding-system 'mime-charset))
121:                              "iso-8859-1"))))))
122:    (org-html-doctype info)
123:    "\n"
124:    (concat "<html"
125:            (cond ((org-html-xhtml-p info)
126:                   (format
127:                    " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
128:                    (plist-get info :language) (plist-get info :language)))
129:                  ((org-html-html5-p info)
130:                   (format " lang=\"%s\"" (plist-get info :language))))
131:            ">\n")
132:    "<head>\n"
133:    (org-html--build-meta-info info)
134:    (org-html--build-head info)
135:    (org-html--build-mathjax-config info)
136:    "</head>\n"
137:    "<body>\n"
138:    (let ((link-up (org-trim (plist-get info :html-link-up)))
139:          (link-home (org-trim (plist-get info :html-link-home))))
140:      (unless (and (string= link-up "") (string= link-home ""))
141:        (format (plist-get info :html-home/up-format)
142:                (or link-up link-home)
143:                (or link-home link-up))))
144:    ;; Preamble.
145:    (org-html--build-pre/postamble 'preamble info)
146:    ;; Document contents.
147:    (let ((div (assq 'content (plist-get info :html-divs))))
148:      (format "<%s id=\"%s\" class=\"%s\">\n"
149:              (nth 1 div)
150:              (nth 2 div)
151:              (plist-get info :html-content-class)))
152:    ;; Document title.
153:    (when (plist-get info :with-title)
154:      (let ((title (and (plist-get info :with-title)
155:                        (plist-get info :title)))
156:            (subtitle (plist-get info :subtitle))
157:            (html5-fancy (org-html--html5-fancy-p info)))
158:        (when title
159:          (format
160:           (if html5-fancy
161:               "<header>\n<h1 class=\"title\">%s</h1>\n%s</header>"
162:             "<h1 class=\"title\">%s%s</h1>\n")
163:           (org-export-data title info)
164:           (if subtitle
165:               (format
166:                (if html5-fancy
167:                    "<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n"
168:                  (concat "\n" (org-html-close-tag "br" nil info) "\n"
169:                          "<span class=\"subtitle\">%s</span>\n"))
170:                (org-export-data subtitle info))
171:             "")))))
172:    ;; add article status
173:    (eli/blog-build-article-status info)
174:    contents
175:    (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
176:    ;; gisus
177:    (eli/blog-build-giscus info)
178:    ;; Postamble.
179:    (org-html--build-pre/postamble 'postamble info)
180:    ;; Possibly use the Klipse library live code blocks.
181:    (when (plist-get info :html-klipsify-src)
182:      (concat "<script>" (plist-get info :html-klipse-selection-script)
183:              "</script><script src=\""
184:              org-html-klipse-js
185:              "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
186:              org-html-klipse-css "\"/>"))
187:    ;; Closing document.
188:    "</body>\n</html>"))
189: 
190: (defun eli/org-blog-footnote-reference (footnote-reference _contents info)
191:   "Transcode a FOOTNOTE-REFERENCE element from Org to HTML.
192: CONTENTS is nil.  INFO is a plist holding contextual information."
193:   (concat
194:    ;; Insert separator between two footnotes in a row.
195:    (let ((prev (org-export-get-previous-element footnote-reference info)))
196:      (when (eq (org-element-type prev) 'footnote-reference)
197:        (plist-get info :html-footnote-separator)))
198:    (let* ((n (org-export-get-footnote-number footnote-reference info))
199:           (id (format "fnr.%d%s"
200:                       n
201:                       (if (org-export-footnote-first-reference-p
202:                            footnote-reference info)
203:                           ""
204:                         ".100"))))
205:      (format
206:       (concat (plist-get info :html-footnote-format)
207:               "<input id=\"%s\" class=\"footref-toggle\" type=\"checkbox\">")
208:       (format "<label for=\"%s\" class=\"footref\">%s</label>"
209:               id n)
210:       id))))
211: 
212: (defun eli/org-blog-src-block (src-block _contents info)
213:   "Transcode a SRC-BLOCK element from Org to HTML.
214: CONTENTS holds the contents of the item.  INFO is a plist holding
215: contextual information."
216:   (if (org-export-read-attribute :attr_html src-block :textarea)
217:       (org-html--textarea-block src-block)
218:     (let* ((lang (org-element-property :language src-block))
219:            (code (org-html-format-code src-block info))
220:            (label (let ((lbl (org-html--reference src-block info t)))
221:                     (if lbl (format " id=\"%s\"" lbl) "")))
222:            (klipsify  (and  (plist-get info :html-klipsify-src)
223:                             (member lang '("javascript" "js"
224:                                            "ruby" "scheme" "clojure" "php" "html")))))
225:       (if (not lang) (format "<pre class=\"example\"%s><code>\n%s</code></pre>" label code)
226:         (format "<div class=\"org-src-container\">\n%s%s\n</div>"
227:                 ;; Build caption.
228:                 (let ((caption (or (org-export-get-caption src-block)
229:                                    (org-element-property :name src-block))))
230:                   (if (not caption) ""
231:                     (let ((listing-number
232:                            (format
233:                             "<span class=\"listing-number\">%s </span>"
234:                             "Listing: ")))
235:                       (format "<div class=\"org-src-name\">%s%s</div>"
236:                               listing-number
237:                               (org-trim (org-export-data caption info))))))
238:                 ;; Contents.
239:                 (if klipsify
240:                     (format "<pre><code class=\"src src-%s\"%s%s>%s</code></pre>"
241:                             lang
242:                             label
243:                             (if (string= lang "html")
244:                                 " data-editor-type=\"html\""
245:                               "")
246:                             code)
247:                   (format "<pre class=\"src src-%s\"%s><code>%s</code></pre>"
248:                           lang label code)))))))
249: 
250: (org-export-define-derived-backend 'blog 'html
251:   :translate-alist '((src-block . eli/org-blog-src-block)
252:                      (footnote-reference . eli/org-blog-footnote-reference)
253:                      (template . eli/org-blog-template)))
1: <<blog-helper--functions>>
2: 
3: (setq org-publish-project-alist `(
4:                                   <<my-blog>>
5:                                   ))
  1: (defun eli/kill-sitemap-buffer (project)
  2:   (let* ((sitemap-filename (plist-get project :sitemap-filename))
  3:          (base-dir (plist-get project :base-directory))
  4:          (sitemap-filepath (expand-file-name sitemap-filename base-dir)))
  5:     (when-let ((sitemap-buffer (find-buffer-visiting sitemap-filepath)))
  6:       (kill-buffer sitemap-buffer))))
  7: 
  8: (defun eli/blog-publish-completion (project)
  9:   (let* ((publishing-directory (plist-get project :publishing-directory))
 10:          (sitamap (file-name-with-extension eli/blog-sitamap "html"))
 11:          (orig-file (expand-file-name sitamap publishing-directory))
 12:          (target-file (expand-file-name
 13:                        sitamap
 14:                        (file-name-directory publishing-directory))))
 15:     (rename-file orig-file target-file t)))
 16: 
 17: (add-to-list 'org-export-global-macros
 18:              '("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@"))
 19: 
 20: (defun eli/sitemap-dated-entry-format (entry _style project)
 21:   "Sitemap PROJECT ENTRY STYLE format that includes date."
 22:   (let* ((file (org-publish--expand-file-name entry project))
 23:          (parsed-title (org-publish-find-property file :title project))
 24:          (title
 25:           (if parsed-title
 26:               (org-no-properties
 27:                (org-element-interpret-data parsed-title))
 28:             (file-name-nondirectory (file-name-sans-extension file)))))
 29:     (org-publish-cache-set-file-property file :title title)
 30:     (if (= (length title) 0)
 31:         (format "*%s*" entry)
 32:       (format "{{{timestamp(%s)}}}   [[file:%s][%s]]"
 33:               (car (org-publish-find-property file :date project))
 34:               (concat "articles/" entry)
 35:               title))))
 36: 
 37: (defun eli/org-publish-sitemap (title list)
 38:   "Generate the sitemap with title."
 39:   (concat "#+TITLE: " title
 40:           "\n"
 41:           "#+DATE: 2023-10-10"
 42:           "\n\n"
 43:           (org-list-to-org list)))
 44: 
 45: (setq eli/blog-base-dir "path-to/blog/"
 46:       eli/blog-publish-dir "your-blog-site-dir"
 47:       eli/blog-sitamap "index.org")
 48: 
 49: ;;;###autoload
 50: (defun eli/org-blog-publish-to-html (plist filename pub-dir)
 51:   "Publish an org file to HTML.
 52: 
 53: FILENAME is the filename of the Org file to be published.  PLIST
 54: is the property list for the given project.  PUB-DIR is the
 55: publishing directory.
 56: 
 57: Return output file name."
 58:   (org-publish-org-to 'blog filename
 59:                       (concat (when (> (length org-html-extension) 0) ".")
 60:                               (or (plist-get plist :html-extension)
 61:                                   org-html-extension
 62:                                   "html"))
 63:                       plist pub-dir))
 64: 
 65: (defvar eli/blog-giscus-script "<script src=\"https://giscus.app/client.js\"
 66:           data-repo=\"Elilif/Elilif.github.io\"
 67:           data-repo-id=\"MDEwOlJlcG9zaXRvcnkyOTgxNjM5ODg=\"
 68:           data-category=\"Announcements\"
 69:           data-category-id=\"DIC_kwDOEcWfFM4Cdz5V\"
 70:           data-mapping=\"pathname\"
 71:           data-strict=\"0\"
 72:           data-reactions-enabled=\"1\"
 73:           data-emit-metadata=\"0\"
 74:           data-input-position=\"top\"
 75:           data-theme=\"light\"
 76:           data-lang=\"zh-CN\"
 77:           crossorigin=\"anonymous\"
 78:           async>
 79:   </script>")
 80: 
 81: (defun eli/blog-build-giscus (info)
 82:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
 83:     (unless (string-equal input-file eli/blog-sitamap)
 84:       eli/blog-giscus-script)))
 85: 
 86: (defvar eli/blog-status-format "<span><i class='bx bx-calendar'></i>
 87: <span>%d</span></span>\n<span><i class='bx bx-edit'></i><span>%C</span></span>")
 88: (defvar eli/blog-history-base-url "https://github.com/Elilif/Elilif.github.io/commits/master/orgs/")
 89: 
 90: (defun eli/blog-build-article-status (info)
 91:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
 92:     (unless (string-equal input-file eli/blog-sitamap)
 93:       (let ((spec (org-html-format-spec info))
 94:             (history-url (concat eli/blog-history-base-url input-file)))
 95:         (concat
 96:          "<div class=\"post-status\">"
 97:          (format-spec eli/blog-status-format spec)
 98:          (format "<span><i class='bx bx-history'></i><span><a href=\"%s\">history</a></span></span>"
 99:                  history-url)
100:          "</div>")))))
101: 
102: (defun eli/org-blog-template (contents info)
103:   "Return complete document string after HTML conversion.
104: CONTENTS is the transcoded contents string.  INFO is a plist
105: holding export options."
106:   (setq eli-test info)
107:   (concat
108:    (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
109:      (let* ((xml-declaration (plist-get info :html-xml-declaration))
110:             (decl (or (and (stringp xml-declaration) xml-declaration)
111:                       (cdr (assoc (plist-get info :html-extension)
112:                                   xml-declaration))
113:                       (cdr (assoc "html" xml-declaration))
114:                       "")))
115:        (when (not (or (not decl) (string= "" decl)))
116:          (format "%s\n"
117:                  (format decl
118:                          (or (and org-html-coding-system
119:                                   ;; FIXME: Use Emacs 22 style here, see `coding-system-get'.
120:                                   (coding-system-get org-html-coding-system 'mime-charset))
121:                              "iso-8859-1"))))))
122:    (org-html-doctype info)
123:    "\n"
124:    (concat "<html"
125:            (cond ((org-html-xhtml-p info)
126:                   (format
127:                    " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
128:                    (plist-get info :language) (plist-get info :language)))
129:                  ((org-html-html5-p info)
130:                   (format " lang=\"%s\"" (plist-get info :language))))
131:            ">\n")
132:    "<head>\n"
133:    (org-html--build-meta-info info)
134:    (org-html--build-head info)
135:    (org-html--build-mathjax-config info)
136:    "</head>\n"
137:    "<body>\n"
138:    (let ((link-up (org-trim (plist-get info :html-link-up)))
139:          (link-home (org-trim (plist-get info :html-link-home))))
140:      (unless (and (string= link-up "") (string= link-home ""))
141:        (format (plist-get info :html-home/up-format)
142:                (or link-up link-home)
143:                (or link-home link-up))))
144:    ;; Preamble.
145:    (org-html--build-pre/postamble 'preamble info)
146:    ;; Document contents.
147:    (let ((div (assq 'content (plist-get info :html-divs))))
148:      (format "<%s id=\"%s\" class=\"%s\">\n"
149:              (nth 1 div)
150:              (nth 2 div)
151:              (plist-get info :html-content-class)))
152:    ;; Document title.
153:    (when (plist-get info :with-title)
154:      (let ((title (and (plist-get info :with-title)
155:                        (plist-get info :title)))
156:            (subtitle (plist-get info :subtitle))
157:            (html5-fancy (org-html--html5-fancy-p info)))
158:        (when title
159:          (format
160:           (if html5-fancy
161:               "<header>\n<h1 class=\"title\">%s</h1>\n%s</header>"
162:             "<h1 class=\"title\">%s%s</h1>\n")
163:           (org-export-data title info)
164:           (if subtitle
165:               (format
166:                (if html5-fancy
167:                    "<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n"
168:                  (concat "\n" (org-html-close-tag "br" nil info) "\n"
169:                          "<span class=\"subtitle\">%s</span>\n"))
170:                (org-export-data subtitle info))
171:             "")))))
172:    ;; add article status
173:    (eli/blog-build-article-status info)
174:    contents
175:    (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
176:    ;; gisus
177:    (eli/blog-build-giscus info)
178:    ;; Postamble.
179:    (org-html--build-pre/postamble 'postamble info)
180:    ;; Possibly use the Klipse library live code blocks.
181:    (when (plist-get info :html-klipsify-src)
182:      (concat "<script>" (plist-get info :html-klipse-selection-script)
183:              "</script><script src=\""
184:              org-html-klipse-js
185:              "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
186:              org-html-klipse-css "\"/>"))
187:    ;; Closing document.
188:    "</body>\n</html>"))
189: 
190: (defun eli/org-blog-footnote-reference (footnote-reference _contents info)
191:   "Transcode a FOOTNOTE-REFERENCE element from Org to HTML.
192: CONTENTS is nil.  INFO is a plist holding contextual information."
193:   (concat
194:    ;; Insert separator between two footnotes in a row.
195:    (let ((prev (org-export-get-previous-element footnote-reference info)))
196:      (when (eq (org-element-type prev) 'footnote-reference)
197:        (plist-get info :html-footnote-separator)))
198:    (let* ((n (org-export-get-footnote-number footnote-reference info))
199:           (id (format "fnr.%d%s"
200:                       n
201:                       (if (org-export-footnote-first-reference-p
202:                            footnote-reference info)
203:                           ""
204:                         ".100"))))
205:      (format
206:       (concat (plist-get info :html-footnote-format)
207:               "<input id=\"%s\" class=\"footref-toggle\" type=\"checkbox\">")
208:       (format "<label for=\"%s\" class=\"footref\">%s</label>"
209:               id n)
210:       id))))
211: 
212: (defun eli/org-blog-src-block (src-block _contents info)
213:   "Transcode a SRC-BLOCK element from Org to HTML.
214: CONTENTS holds the contents of the item.  INFO is a plist holding
215: contextual information."
216:   (if (org-export-read-attribute :attr_html src-block :textarea)
217:       (org-html--textarea-block src-block)
218:     (let* ((lang (org-element-property :language src-block))
219:            (code (org-html-format-code src-block info))
220:            (label (let ((lbl (org-html--reference src-block info t)))
221:                     (if lbl (format " id=\"%s\"" lbl) "")))
222:            (klipsify  (and  (plist-get info :html-klipsify-src)
223:                             (member lang '("javascript" "js"
224:                                            "ruby" "scheme" "clojure" "php" "html")))))
225:       (if (not lang) (format "<pre class=\"example\"%s><code>\n%s</code></pre>" label code)
226:         (format "<div class=\"org-src-container\">\n%s%s\n</div>"
227:                 ;; Build caption.
228:                 (let ((caption (or (org-export-get-caption src-block)
229:                                    (org-element-property :name src-block))))
230:                   (if (not caption) ""
231:                     (let ((listing-number
232:                            (format
233:                             "<span class=\"listing-number\">%s </span>"
234:                             "Listing: ")))
235:                       (format "<div class=\"org-src-name\">%s%s</div>"
236:                               listing-number
237:                               (org-trim (org-export-data caption info))))))
238:                 ;; Contents.
239:                 (if klipsify
240:                     (format "<pre><code class=\"src src-%s\"%s%s>%s</code></pre>"
241:                             lang
242:                             label
243:                             (if (string= lang "html")
244:                                 " data-editor-type=\"html\""
245:                               "")
246:                             code)
247:                   (format "<pre class=\"src src-%s\"%s><code>%s</code></pre>"
248:                           lang label code)))))))
249: 
250: (org-export-define-derived-backend 'blog 'html
251:   :translate-alist '((src-block . eli/org-blog-src-block)
252:                      (footnote-reference . eli/org-blog-footnote-reference)
253:                      (template . eli/org-blog-template)))
254: 
255: (setq org-publish-project-alist `(
256:                                   ("eli's blog"
257:                                    :base-directory ,eli/blog-base-dir
258:                                    :publishing-directory ,(expand-file-name "articles" eli/blog-publish-dir)
259:                                    :base-extension "org"
260:                                    :recursive nil
261:                                    :htmlized-source t
262:                                    :publishing-function eli/org-blog-publish-to-html
263:                                    :exclude "rss.org"
264: 
265:                                    :auto-sitemap t
266:                                    :preparation-function eli/kill-sitemap-buffer
267:                                    :completion-function eli/blog-publish-completion
268:                                    :sitemap-filename ,eli/blog-sitamap
269:                                    :sitemap-title "Eli's Blog"
270:                                    :sitemap-sort-files anti-chronologically
271:                                    :sitemap-function eli/org-publish-sitemap
272:                                    :sitemap-format-entry eli/sitemap-dated-entry-format
273: 
274:                                    :html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/style.css\" />
275:                                     <link rel=\"stylesheet\" type=\"text/css\" href=\"/css/htmlize.css\" />
276:                                     <script src=\"/scripts/script.js\"></script>
277:                                     <script src=\"/scripts/toc.js\"></script>"
278:                                    :html-preamble t
279:                                    :html-preamble-format (("en" "<nav class=\"nav\">
280:                                      <a href=\"/index.html\" class=\"button\">Home</a>
281:                                      <a href=\"/rss.xml\" class=\"button\">RSS</a>
282:                                      <a href=\"/config.html\" class=\"button\">Literate Emacs Config</a>
283:                                    </nav>
284:                                    <hr>"))
285:                                    :html-postamble t
286:                                    :html-postamble-format (("en" "<hr class=\"Solid\">
287:                                    <div class=\"info\">
288:                                      <span class=\"author\">Author: %a (%e)</span>
289:                                      <span class=\"date\">Create Date: %d</span>
290:                                      <span class=\"date\">Last modified: %C</span>
291:                                      <span>Creator: %c</span>
292:                                    </div>"))
293:                                    :with-creator nil
294:                                    )
295:                                   ))

RSS 生成

RSS 对于一个博客来说是比较重要的,可惜的是 ox-publish.el 没有原生功能支持。不过好在有 GitHub - BenedictHW/ox-rss 。然而 ox-rss 也有缺点,它只能用于单个文件里的 headlines 。因此我们需要曲线救国,新建一个 publish 项目,使用 sitemap 来收集 RSS entry ,生成一个 rss.org,最后把他导出成我们需要的 rss.xml 。

 1: (defun eli/org-publish-rss-entry (entry _style project)
 2:   "Format ENTRY for the posts RSS feed in PROJECT."
 3:   (let* ((file (org-publish--expand-file-name entry project))
 4:          (preview (eli/blog-get-preview file))
 5:          (parsed-title (org-publish-find-property file :title project))
 6:          (title
 7:           (if parsed-title
 8:               (org-no-properties
 9:                (org-element-interpret-data parsed-title))
10:             (file-name-nondirectory (file-name-sans-extension file))))
11:          (root (org-publish-property :html-link-home project))
12:          (link (concat
13:                 "articles/"
14:                 (file-name-sans-extension entry) ".html"))
15:          (pubdate (car (org-publish-find-property file :date project))))
16:     (org-publish-cache-set-file-property file :title title)
17:     (format "%s
18: :properties:
19: :rss_permalink: %s
20: :pubdate: %s
21: :end:\n%s\n[[%s][Read More]]"
22:             title
23:             link
24:             pubdate
25:             preview
26:             (concat
27:              root
28:              link))))
1: (defun eli/org-publish-rss-sitemap (title list)
2:   "Generate a sitemap of posts that is exported as a RSS feed.
3: TITLE is the title of the RSS feed.  LIST is an internal
4: representation for the files to include.  PROJECT is the current
5: project."
6:   (concat
7:    "#+TITLE: " title
8:    "\n\n"
9:    (org-list-to-subtree list)))
1: (defun eli/org-publish-rss-feed (plist filename dir)
2:   "Publish PLIST to Rss when FILENAME is rss.org.
3: DIR is the location of the output."
4:   (if (equal "rss.org" (file-name-nondirectory filename))
5:       (org-rss-publish-to-rss plist filename dir)))
1: :publishing-function eli/org-publish-rss-feed
2: :auto-sitemap t
3: :sitemap-function eli/org-publish-rss-sitemap
4: :sitemap-title "Eli's Blog"
5: :sitemap-filename "rss.org"
6: :sitemap-sort-files anti-chronologically
7: :sitemap-format-entry eli/org-publish-rss-entry

在导出的时候我们只希望导出 rss.org ,所以需要设置 :include 属性为 ("rss.org") ,同时我们不希望收集 RSS entry 时把 index.org 中的内容也收集进去,所以需要设置 :exclude 属性为 "index.org"

剩余的属性如下:

 1: :preparation-function eli/kill-sitemap-buffer
 2: :publishing-directory ,eli/blog-publish-dir
 3: :base-directory ,eli/blog-base-dir
 4: :rss-extension "xml"
 5: :base-extension "org"
 6: :html-link-home "https://elilif.github.io/"
 7: :html-link-use-abs-url t
 8: :html-link-org-files-as-html t
 9: :include ("rss.org")
10: :exclude "index.org"

现在整个 rss 项目就完成了。

1: ("eli's blog rss"
2:  <<rss-sitemap>>
3:  <<rss-misc>>)
 1: ("eli's blog rss"
 2:  :publishing-function eli/org-publish-rss-feed
 3:  :auto-sitemap t
 4:  :sitemap-function eli/org-publish-rss-sitemap
 5:  :sitemap-title "Eli's Blog"
 6:  :sitemap-filename "rss.org"
 7:  :sitemap-sort-files anti-chronologically
 8:  :sitemap-format-entry eli/org-publish-rss-entry
 9:  :preparation-function eli/kill-sitemap-buffer
10:  :publishing-directory ,eli/blog-publish-dir
11:  :base-directory ,eli/blog-base-dir
12:  :rss-extension "xml"
13:  :base-extension "org"
14:  :html-link-home "https://elilif.github.io/"
15:  :html-link-use-abs-url t
16:  :html-link-org-files-as-html t
17:  :include ("rss.org")
18:  :exclude "index.org")

下面是完整的代码:

1: <<eli/org-publish-rss-feed>>
2: 
3: <<eli/org-publish-rss-sitemap>>
4: 
5: <<eli/org-publish-rss-entry>>
 1: (defun eli/org-publish-rss-feed (plist filename dir)
 2:   "Publish PLIST to Rss when FILENAME is rss.org.
 3: DIR is the location of the output."
 4:   (if (equal "rss.org" (file-name-nondirectory filename))
 5:       (org-rss-publish-to-rss plist filename dir)))
 6: 
 7: (defun eli/org-publish-rss-sitemap (title list)
 8:   "Generate a sitemap of posts that is exported as a RSS feed.
 9: TITLE is the title of the RSS feed.  LIST is an internal
10: representation for the files to include.  PROJECT is the current
11: project."
12:   (concat
13:    "#+TITLE: " title
14:    "\n\n"
15:    (org-list-to-subtree list)))
16: 
17: (defun eli/org-publish-rss-entry (entry _style project)
18:   "Format ENTRY for the posts RSS feed in PROJECT."
19:   (let* ((file (org-publish--expand-file-name entry project))
20:          (preview (eli/blog-get-preview file))
21:          (parsed-title (org-publish-find-property file :title project))
22:          (title
23:           (if parsed-title
24:               (org-no-properties
25:                (org-element-interpret-data parsed-title))
26:             (file-name-nondirectory (file-name-sans-extension file))))
27:          (root (org-publish-property :html-link-home project))
28:          (link (concat
29:                 "articles/"
30:                 (file-name-sans-extension entry) ".html"))
31:          (pubdate (car (org-publish-find-property file :date project))))
32:     (org-publish-cache-set-file-property file :title title)
33:     (format "%s
34: :properties:
35: :rss_permalink: %s
36: :pubdate: %s
37: :end:\n%s\n[[%s][Read More]]"
38:             title
39:             link
40:             pubdate
41:             preview
42:             (concat
43:              root
44:              link))))
1: <<rss-helper--functions>>
2: 
3: <<rss>>
 1: (defun eli/org-publish-rss-feed (plist filename dir)
 2:   "Publish PLIST to Rss when FILENAME is rss.org.
 3: DIR is the location of the output."
 4:   (if (equal "rss.org" (file-name-nondirectory filename))
 5:       (org-rss-publish-to-rss plist filename dir)))
 6: 
 7: (defun eli/org-publish-rss-sitemap (title list)
 8:   "Generate a sitemap of posts that is exported as a RSS feed.
 9: TITLE is the title of the RSS feed.  LIST is an internal
10: representation for the files to include.  PROJECT is the current
11: project."
12:   (concat
13:    "#+TITLE: " title
14:    "\n\n"
15:    (org-list-to-subtree list)))
16: 
17: (defun eli/org-publish-rss-entry (entry _style project)
18:   "Format ENTRY for the posts RSS feed in PROJECT."
19:   (let* ((file (org-publish--expand-file-name entry project))
20:          (preview (eli/blog-get-preview file))
21:          (parsed-title (org-publish-find-property file :title project))
22:          (title
23:           (if parsed-title
24:               (org-no-properties
25:                (org-element-interpret-data parsed-title))
26:             (file-name-nondirectory (file-name-sans-extension file))))
27:          (root (org-publish-property :html-link-home project))
28:          (link (concat
29:                 "articles/"
30:                 (file-name-sans-extension entry) ".html"))
31:          (pubdate (car (org-publish-find-property file :date project))))
32:     (org-publish-cache-set-file-property file :title title)
33:     (format "%s
34: :properties:
35: :rss_permalink: %s
36: :pubdate: %s
37: :end:\n%s\n[[%s][Read More]]"
38:             title
39:             link
40:             pubdate
41:             preview
42:             (concat
43:              root
44:              link))))
45: 
46: ("eli's blog rss"
47:  :publishing-function eli/org-publish-rss-feed
48:  :auto-sitemap t
49:  :sitemap-function eli/org-publish-rss-sitemap
50:  :sitemap-title "Eli's Blog"
51:  :sitemap-filename "rss.org"
52:  :sitemap-sort-files anti-chronologically
53:  :sitemap-format-entry eli/org-publish-rss-entry
54:  :preparation-function eli/kill-sitemap-buffer
55:  :publishing-directory ,eli/blog-publish-dir
56:  :base-directory ,eli/blog-base-dir
57:  :rss-extension "xml"
58:  :base-extension "org"
59:  :html-link-home "https://elilif.github.io/"
60:  :html-link-use-abs-url t
61:  :html-link-org-files-as-html t
62:  :include ("rss.org")
63:  :exclude "index.org")

Org 文学编程网页导出

我希望在导出时能够把代码块中的 noweb 展开,并且在网页中同时提供不展开和展开两种版本。这样就能在保持原汁原味的文学编程的同时又方便读者查看。下面是大致实现思路:

通过 org-export-before-processing-functions 在导出时复制一遍代码块,并在添上 :noweb yes 参数后和原代码块放到一个 special block (#+begin_multilang#+end_multilang) 之间。这样在 HTML 就是一个 class 为 multilangdiv 。然后使用 js 添加一个按钮,实现不同版本间的切换。

 1: (defun eli/org-export-src-babel-duplicate (backend)
 2:   "Duplicate every src babels in the current buffer.
 3: 
 4: add \":noweb yes\" to duplicated src babels."
 5:   (when (eq backend 'blog)
 6:     (save-excursion
 7:       (goto-char (point-min))
 8:       (while (re-search-forward org-babel-src-block-regexp nil t)
 9:         (let ((end (copy-marker (match-end 0)))
10:               (string (match-string 0))
11:               (block (org-element-at-point)))
12:           (goto-char (org-element-property :begin block))
13:           (insert "#+begin_multilang")
14:           (insert "\n")
15:           (goto-char end)
16:           (insert "\n")
17:           (insert string)
18:           (save-excursion
19:             (goto-char (1+ end))
20:             (end-of-line)
21:             (insert " :noweb yes"))
22:           (insert "\n")
23:           (insert "#+end_multilang"))))))
24: 
25: (add-hook 'org-export-before-processing-functions #'eli/org-export-src-babel-duplicate)

Emacs 配置部分的 project 如下:

1: <<eli/org-export-src-babel-duplicate>>
 1: (defun eli/org-export-src-babel-duplicate (backend)
 2:   "Duplicate every src babels in the current buffer.
 3: 
 4: add \":noweb yes\" to duplicated src babels."
 5:   (when (eq backend 'blog)
 6:     (save-excursion
 7:       (goto-char (point-min))
 8:       (while (re-search-forward org-babel-src-block-regexp nil t)
 9:         (let ((end (copy-marker (match-end 0)))
10:               (string (match-string 0))
11:               (block (org-element-at-point)))
12:           (goto-char (org-element-property :begin block))
13:           (insert "#+begin_multilang")
14:           (insert "\n")
15:           (goto-char end)
16:           (insert "\n")
17:           (insert string)
18:           (save-excursion
19:             (goto-char (1+ end))
20:             (end-of-line)
21:             (insert " :noweb yes"))
22:           (insert "\n")
23:           (insert "#+end_multilang"))))))
24: 
25: (add-hook 'org-export-before-processing-functions #'eli/org-export-src-babel-duplicate)
1: ("Emacs config"
2:  :publishing-directory ,eli/blog-publish-dir
3:  :base-directory ,user-emacs-directory
4:  :include ("config.org")
5:  :publishing-function eli/org-blog-publish-to-html
6:  <<html>>)
 1: ("Emacs config"
 2:  :publishing-directory ,eli/blog-publish-dir
 3:  :base-directory ,user-emacs-directory
 4:  :include ("config.org")
 5:  :publishing-function eli/org-blog-publish-to-html
 6:  :html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/style.css\" />
 7:   <link rel=\"stylesheet\" type=\"text/css\" href=\"/css/htmlize.css\" />
 8:   <script src=\"/scripts/script.js\"></script>
 9:   <script src=\"/scripts/toc.js\"></script>"
10:  :html-preamble t
11:  :html-preamble-format (("en" "<nav class=\"nav\">
12:    <a href=\"/index.html\" class=\"button\">Home</a>
13:    <a href=\"/rss.xml\" class=\"button\">RSS</a>
14:    <a href=\"/config.html\" class=\"button\">Literate Emacs Config</a>
15:  </nav>
16:  <hr>"))
17:  :html-postamble t
18:  :html-postamble-format (("en" "<hr class=\"Solid\">
19:  <div class=\"info\">
20:    <span class=\"author\">Author: %a (%e)</span>
21:    <span class=\"date\">Create Date: %d</span>
22:    <span class=\"date\">Last modified: %C</span>
23:    <span>Creator: %c</span>
24:  </div>"))
25:  :with-creator nil)

总结

以下代码完整地包括了前文提到的内容:

 1: <<blog-helper--functions>>
 2: 
 3: <<rss-helper--functions>>
 4: 
 5: <<config-helper--functions>>
 6: 
 7: (setq org-publish-project-alist `(
 8:                                   <<my-blog>>
 9:                                   <<rss>>
10:                                   <<emacs-config>>
11:                                   ))
  1: (defun eli/kill-sitemap-buffer (project)
  2:   (let* ((sitemap-filename (plist-get project :sitemap-filename))
  3:          (base-dir (plist-get project :base-directory))
  4:          (sitemap-filepath (expand-file-name sitemap-filename base-dir)))
  5:     (when-let ((sitemap-buffer (find-buffer-visiting sitemap-filepath)))
  6:       (kill-buffer sitemap-buffer))))
  7: 
  8: (defun eli/blog-publish-completion (project)
  9:   (let* ((publishing-directory (plist-get project :publishing-directory))
 10:          (sitamap (file-name-with-extension eli/blog-sitamap "html"))
 11:          (orig-file (expand-file-name sitamap publishing-directory))
 12:          (target-file (expand-file-name
 13:                        sitamap
 14:                        (file-name-directory publishing-directory))))
 15:     (rename-file orig-file target-file t)))
 16: 
 17: (add-to-list 'org-export-global-macros
 18:              '("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@"))
 19: 
 20: (defun eli/sitemap-dated-entry-format (entry _style project)
 21:   "Sitemap PROJECT ENTRY STYLE format that includes date."
 22:   (let* ((file (org-publish--expand-file-name entry project))
 23:          (parsed-title (org-publish-find-property file :title project))
 24:          (title
 25:           (if parsed-title
 26:               (org-no-properties
 27:                (org-element-interpret-data parsed-title))
 28:             (file-name-nondirectory (file-name-sans-extension file)))))
 29:     (org-publish-cache-set-file-property file :title title)
 30:     (if (= (length title) 0)
 31:         (format "*%s*" entry)
 32:       (format "{{{timestamp(%s)}}}   [[file:%s][%s]]"
 33:               (car (org-publish-find-property file :date project))
 34:               (concat "articles/" entry)
 35:               title))))
 36: 
 37: (defun eli/org-publish-sitemap (title list)
 38:   "Generate the sitemap with title."
 39:   (concat "#+TITLE: " title
 40:           "\n"
 41:           "#+DATE: 2023-10-10"
 42:           "\n\n"
 43:           (org-list-to-org list)))
 44: 
 45: (setq eli/blog-base-dir "path-to/blog/"
 46:       eli/blog-publish-dir "your-blog-site-dir"
 47:       eli/blog-sitamap "index.org")
 48: 
 49: ;;;###autoload
 50: (defun eli/org-blog-publish-to-html (plist filename pub-dir)
 51:   "Publish an org file to HTML.
 52: 
 53: FILENAME is the filename of the Org file to be published.  PLIST
 54: is the property list for the given project.  PUB-DIR is the
 55: publishing directory.
 56: 
 57: Return output file name."
 58:   (org-publish-org-to 'blog filename
 59:                       (concat (when (> (length org-html-extension) 0) ".")
 60:                               (or (plist-get plist :html-extension)
 61:                                   org-html-extension
 62:                                   "html"))
 63:                       plist pub-dir))
 64: 
 65: (defvar eli/blog-giscus-script "<script src=\"https://giscus.app/client.js\"
 66:           data-repo=\"Elilif/Elilif.github.io\"
 67:           data-repo-id=\"MDEwOlJlcG9zaXRvcnkyOTgxNjM5ODg=\"
 68:           data-category=\"Announcements\"
 69:           data-category-id=\"DIC_kwDOEcWfFM4Cdz5V\"
 70:           data-mapping=\"pathname\"
 71:           data-strict=\"0\"
 72:           data-reactions-enabled=\"1\"
 73:           data-emit-metadata=\"0\"
 74:           data-input-position=\"top\"
 75:           data-theme=\"light\"
 76:           data-lang=\"zh-CN\"
 77:           crossorigin=\"anonymous\"
 78:           async>
 79:   </script>")
 80: 
 81: (defun eli/blog-build-giscus (info)
 82:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
 83:     (unless (string-equal input-file eli/blog-sitamap)
 84:       eli/blog-giscus-script)))
 85: 
 86: (defvar eli/blog-status-format "<span><i class='bx bx-calendar'></i>
 87: <span>%d</span></span>\n<span><i class='bx bx-edit'></i><span>%C</span></span>")
 88: (defvar eli/blog-history-base-url "https://github.com/Elilif/Elilif.github.io/commits/master/orgs/")
 89: 
 90: (defun eli/blog-build-article-status (info)
 91:   (let ((input-file (file-name-nondirectory (plist-get info :input-file))))
 92:     (unless (string-equal input-file eli/blog-sitamap)
 93:       (let ((spec (org-html-format-spec info))
 94:             (history-url (concat eli/blog-history-base-url input-file)))
 95:         (concat
 96:          "<div class=\"post-status\">"
 97:          (format-spec eli/blog-status-format spec)
 98:          (format "<span><i class='bx bx-history'></i><span><a href=\"%s\">history</a></span></span>"
 99:                  history-url)
100:          "</div>")))))
101: 
102: (defun eli/org-blog-template (contents info)
103:   "Return complete document string after HTML conversion.
104: CONTENTS is the transcoded contents string.  INFO is a plist
105: holding export options."
106:   (setq eli-test info)
107:   (concat
108:    (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
109:      (let* ((xml-declaration (plist-get info :html-xml-declaration))
110:             (decl (or (and (stringp xml-declaration) xml-declaration)
111:                       (cdr (assoc (plist-get info :html-extension)
112:                                   xml-declaration))
113:                       (cdr (assoc "html" xml-declaration))
114:                       "")))
115:        (when (not (or (not decl) (string= "" decl)))
116:          (format "%s\n"
117:                  (format decl
118:                          (or (and org-html-coding-system
119:                                   ;; FIXME: Use Emacs 22 style here, see `coding-system-get'.
120:                                   (coding-system-get org-html-coding-system 'mime-charset))
121:                              "iso-8859-1"))))))
122:    (org-html-doctype info)
123:    "\n"
124:    (concat "<html"
125:            (cond ((org-html-xhtml-p info)
126:                   (format
127:                    " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
128:                    (plist-get info :language) (plist-get info :language)))
129:                  ((org-html-html5-p info)
130:                   (format " lang=\"%s\"" (plist-get info :language))))
131:            ">\n")
132:    "<head>\n"
133:    (org-html--build-meta-info info)
134:    (org-html--build-head info)
135:    (org-html--build-mathjax-config info)
136:    "</head>\n"
137:    "<body>\n"
138:    (let ((link-up (org-trim (plist-get info :html-link-up)))
139:          (link-home (org-trim (plist-get info :html-link-home))))
140:      (unless (and (string= link-up "") (string= link-home ""))
141:        (format (plist-get info :html-home/up-format)
142:                (or link-up link-home)
143:                (or link-home link-up))))
144:    ;; Preamble.
145:    (org-html--build-pre/postamble 'preamble info)
146:    ;; Document contents.
147:    (let ((div (assq 'content (plist-get info :html-divs))))
148:      (format "<%s id=\"%s\" class=\"%s\">\n"
149:              (nth 1 div)
150:              (nth 2 div)
151:              (plist-get info :html-content-class)))
152:    ;; Document title.
153:    (when (plist-get info :with-title)
154:      (let ((title (and (plist-get info :with-title)
155:                        (plist-get info :title)))
156:            (subtitle (plist-get info :subtitle))
157:            (html5-fancy (org-html--html5-fancy-p info)))
158:        (when title
159:          (format
160:           (if html5-fancy
161:               "<header>\n<h1 class=\"title\">%s</h1>\n%s</header>"
162:             "<h1 class=\"title\">%s%s</h1>\n")
163:           (org-export-data title info)
164:           (if subtitle
165:               (format
166:                (if html5-fancy
167:                    "<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n"
168:                  (concat "\n" (org-html-close-tag "br" nil info) "\n"
169:                          "<span class=\"subtitle\">%s</span>\n"))
170:                (org-export-data subtitle info))
171:             "")))))
172:    ;; add article status
173:    (eli/blog-build-article-status info)
174:    contents
175:    (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
176:    ;; gisus
177:    (eli/blog-build-giscus info)
178:    ;; Postamble.
179:    (org-html--build-pre/postamble 'postamble info)
180:    ;; Possibly use the Klipse library live code blocks.
181:    (when (plist-get info :html-klipsify-src)
182:      (concat "<script>" (plist-get info :html-klipse-selection-script)
183:              "</script><script src=\""
184:              org-html-klipse-js
185:              "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
186:              org-html-klipse-css "\"/>"))
187:    ;; Closing document.
188:    "</body>\n</html>"))
189: 
190: (defun eli/org-blog-footnote-reference (footnote-reference _contents info)
191:   "Transcode a FOOTNOTE-REFERENCE element from Org to HTML.
192: CONTENTS is nil.  INFO is a plist holding contextual information."
193:   (concat
194:    ;; Insert separator between two footnotes in a row.
195:    (let ((prev (org-export-get-previous-element footnote-reference info)))
196:      (when (eq (org-element-type prev) 'footnote-reference)
197:        (plist-get info :html-footnote-separator)))
198:    (let* ((n (org-export-get-footnote-number footnote-reference info))
199:           (id (format "fnr.%d%s"
200:                       n
201:                       (if (org-export-footnote-first-reference-p
202:                            footnote-reference info)
203:                           ""
204:                         ".100"))))
205:      (format
206:       (concat (plist-get info :html-footnote-format)
207:               "<input id=\"%s\" class=\"footref-toggle\" type=\"checkbox\">")
208:       (format "<label for=\"%s\" class=\"footref\">%s</label>"
209:               id n)
210:       id))))
211: 
212: (defun eli/org-blog-src-block (src-block _contents info)
213:   "Transcode a SRC-BLOCK element from Org to HTML.
214: CONTENTS holds the contents of the item.  INFO is a plist holding
215: contextual information."
216:   (if (org-export-read-attribute :attr_html src-block :textarea)
217:       (org-html--textarea-block src-block)
218:     (let* ((lang (org-element-property :language src-block))
219:            (code (org-html-format-code src-block info))
220:            (label (let ((lbl (org-html--reference src-block info t)))
221:                     (if lbl (format " id=\"%s\"" lbl) "")))
222:            (klipsify  (and  (plist-get info :html-klipsify-src)
223:                             (member lang '("javascript" "js"
224:                                            "ruby" "scheme" "clojure" "php" "html")))))
225:       (if (not lang) (format "<pre class=\"example\"%s><code>\n%s</code></pre>" label code)
226:         (format "<div class=\"org-src-container\">\n%s%s\n</div>"
227:                 ;; Build caption.
228:                 (let ((caption (or (org-export-get-caption src-block)
229:                                    (org-element-property :name src-block))))
230:                   (if (not caption) ""
231:                     (let ((listing-number
232:                            (format
233:                             "<span class=\"listing-number\">%s </span>"
234:                             "Listing: ")))
235:                       (format "<div class=\"org-src-name\">%s%s</div>"
236:                               listing-number
237:                               (org-trim (org-export-data caption info))))))
238:                 ;; Contents.
239:                 (if klipsify
240:                     (format "<pre><code class=\"src src-%s\"%s%s>%s</code></pre>"
241:                             lang
242:                             label
243:                             (if (string= lang "html")
244:                                 " data-editor-type=\"html\""
245:                               "")
246:                             code)
247:                   (format "<pre class=\"src src-%s\"%s><code>%s</code></pre>"
248:                           lang label code)))))))
249: 
250: (org-export-define-derived-backend 'blog 'html
251:   :translate-alist '((src-block . eli/org-blog-src-block)
252:                      (footnote-reference . eli/org-blog-footnote-reference)
253:                      (template . eli/org-blog-template)))
254: 
255: (defun eli/org-publish-rss-feed (plist filename dir)
256:   "Publish PLIST to Rss when FILENAME is rss.org.
257: DIR is the location of the output."
258:   (if (equal "rss.org" (file-name-nondirectory filename))
259:       (org-rss-publish-to-rss plist filename dir)))
260: 
261: (defun eli/org-publish-rss-sitemap (title list)
262:   "Generate a sitemap of posts that is exported as a RSS feed.
263: TITLE is the title of the RSS feed.  LIST is an internal
264: representation for the files to include.  PROJECT is the current
265: project."
266:   (concat
267:    "#+TITLE: " title
268:    "\n\n"
269:    (org-list-to-subtree list)))
270: 
271: (defun eli/org-publish-rss-entry (entry _style project)
272:   "Format ENTRY for the posts RSS feed in PROJECT."
273:   (let* ((file (org-publish--expand-file-name entry project))
274:          (preview (eli/blog-get-preview file))
275:          (parsed-title (org-publish-find-property file :title project))
276:          (title
277:           (if parsed-title
278:               (org-no-properties
279:                (org-element-interpret-data parsed-title))
280:             (file-name-nondirectory (file-name-sans-extension file))))
281:          (root (org-publish-property :html-link-home project))
282:          (link (concat
283:                 "articles/"
284:                 (file-name-sans-extension entry) ".html"))
285:          (pubdate (car (org-publish-find-property file :date project))))
286:     (org-publish-cache-set-file-property file :title title)
287:     (format "%s
288: :properties:
289: :rss_permalink: %s
290: :pubdate: %s
291: :end:\n%s\n[[%s][Read More]]"
292:             title
293:             link
294:             pubdate
295:             preview
296:             (concat
297:              root
298:              link))))
299: 
300: (defun eli/org-export-src-babel-duplicate (backend)
301:   "Duplicate every src babels in the current buffer.
302: 
303: add \":noweb yes\" to duplicated src babels."
304:   (when (eq backend 'blog)
305:     (save-excursion
306:       (goto-char (point-min))
307:       (while (re-search-forward org-babel-src-block-regexp nil t)
308:         (let ((end (copy-marker (match-end 0)))
309:               (string (match-string 0))
310:               (block (org-element-at-point)))
311:           (goto-char (org-element-property :begin block))
312:           (insert "#+begin_multilang")
313:           (insert "\n")
314:           (goto-char end)
315:           (insert "\n")
316:           (insert string)
317:           (save-excursion
318:             (goto-char (1+ end))
319:             (end-of-line)
320:             (insert " :noweb yes"))
321:           (insert "\n")
322:           (insert "#+end_multilang"))))))
323: 
324: (add-hook 'org-export-before-processing-functions #'eli/org-export-src-babel-duplicate)
325: 
326: (setq org-publish-project-alist `(
327:                                   ("eli's blog"
328:                                    :base-directory ,eli/blog-base-dir
329:                                    :publishing-directory ,(expand-file-name "articles" eli/blog-publish-dir)
330:                                    :base-extension "org"
331:                                    :recursive nil
332:                                    :htmlized-source t
333:                                    :publishing-function eli/org-blog-publish-to-html
334:                                    :exclude "rss.org"
335: 
336:                                    :auto-sitemap t
337:                                    :preparation-function eli/kill-sitemap-buffer
338:                                    :completion-function eli/blog-publish-completion
339:                                    :sitemap-filename ,eli/blog-sitamap
340:                                    :sitemap-title "Eli's Blog"
341:                                    :sitemap-sort-files anti-chronologically
342:                                    :sitemap-function eli/org-publish-sitemap
343:                                    :sitemap-format-entry eli/sitemap-dated-entry-format
344: 
345:                                    :html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/style.css\" />
346:                                     <link rel=\"stylesheet\" type=\"text/css\" href=\"/css/htmlize.css\" />
347:                                     <script src=\"/scripts/script.js\"></script>
348:                                     <script src=\"/scripts/toc.js\"></script>"
349:                                    :html-preamble t
350:                                    :html-preamble-format (("en" "<nav class=\"nav\">
351:                                      <a href=\"/index.html\" class=\"button\">Home</a>
352:                                      <a href=\"/rss.xml\" class=\"button\">RSS</a>
353:                                      <a href=\"/config.html\" class=\"button\">Literate Emacs Config</a>
354:                                    </nav>
355:                                    <hr>"))
356:                                    :html-postamble t
357:                                    :html-postamble-format (("en" "<hr class=\"Solid\">
358:                                    <div class=\"info\">
359:                                      <span class=\"author\">Author: %a (%e)</span>
360:                                      <span class=\"date\">Create Date: %d</span>
361:                                      <span class=\"date\">Last modified: %C</span>
362:                                      <span>Creator: %c</span>
363:                                    </div>"))
364:                                    :with-creator nil
365:                                    )
366:                                   ("eli's blog rss"
367:                                    :publishing-function eli/org-publish-rss-feed
368:                                    :auto-sitemap t
369:                                    :sitemap-function eli/org-publish-rss-sitemap
370:                                    :sitemap-title "Eli's Blog"
371:                                    :sitemap-filename "rss.org"
372:                                    :sitemap-sort-files anti-chronologically
373:                                    :sitemap-format-entry eli/org-publish-rss-entry
374:                                    :preparation-function eli/kill-sitemap-buffer
375:                                    :publishing-directory ,eli/blog-publish-dir
376:                                    :base-directory ,eli/blog-base-dir
377:                                    :rss-extension "xml"
378:                                    :base-extension "org"
379:                                    :html-link-home "https://elilif.github.io/"
380:                                    :html-link-use-abs-url t
381:                                    :html-link-org-files-as-html t
382:                                    :include ("rss.org")
383:                                    :exclude "index.org")
384:                                   ("Emacs config"
385:                                    :publishing-directory ,eli/blog-publish-dir
386:                                    :base-directory ,user-emacs-directory
387:                                    :include ("config.org")
388:                                    :publishing-function eli/org-blog-publish-to-html
389:                                    :html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/style.css\" />
390:                                     <link rel=\"stylesheet\" type=\"text/css\" href=\"/css/htmlize.css\" />
391:                                     <script src=\"/scripts/script.js\"></script>
392:                                     <script src=\"/scripts/toc.js\"></script>"
393:                                    :html-preamble t
394:                                    :html-preamble-format (("en" "<nav class=\"nav\">
395:                                      <a href=\"/index.html\" class=\"button\">Home</a>
396:                                      <a href=\"/rss.xml\" class=\"button\">RSS</a>
397:                                      <a href=\"/config.html\" class=\"button\">Literate Emacs Config</a>
398:                                    </nav>
399:                                    <hr>"))
400:                                    :html-postamble t
401:                                    :html-postamble-format (("en" "<hr class=\"Solid\">
402:                                    <div class=\"info\">
403:                                      <span class=\"author\">Author: %a (%e)</span>
404:                                      <span class=\"date\">Create Date: %d</span>
405:                                      <span class=\"date\">Last modified: %C</span>
406:                                      <span>Creator: %c</span>
407:                                    </div>"))
408:                                    :with-creator nil)
409:                                   ))

Footnotes:

1

GitHub - niklasfasching/go-org :the goal for the parser is to support a reasonable subset of Org mode. Org mode is huge and I like to follow the 80/20 rule.

2

代码块中如果有 <<xxx>> 之类的文本,可以点击代码块上的 expand 按钮展开,也可以直接点击 <<xxx>> 跳转到定义位置。

3

比如说这条注释。

4

具体可以查阅变量 org-html-preamble-format 的文档。

5

具体可以查阅变量 org-html-postamble-format 的文档。

6

具体可以查阅变量 org-html-head 的文档。


Author: Eli Qian Email: eli.q.qian@gmail.com Create Date: 2024-02-21 Last modified: 2024-03-09 Creator: Emacs 29.2 (Org mode 9.6.15)