返回 2026-05-08
🛠 工具 / 开源

RSS 中的文章预览Article previews in RSS

作者长期在 RSS 订阅中仅提供标题和日期,拒绝加入全文或预览内容,认为技术上难以实现。经过三年考虑后,他决定采用 Emacs Lisp 脚本在生成 RSS 时自动提取前几段作为摘要,实现了轻量级文章预览功能。该方案无需外部服务,完全基于本地构建流程,保持了 RSS 的简洁性与可读性。

kqr

大约三年前,这个网站的 RSS 订阅源内容一直非常贫乏。它只包含文章标题和日期,别无其他。许多读者曾请求我加入全文或至少是预览内容,但我一直拖延,因为从技术上实现听起来相当困难。

本网站 RSS 订阅源的生成方式分为两步:

  • 首先在 Emacs Lisp 中运行一个小循环,遍历该项目按排序和过滤后的文件列表中的前几项。该循环构建 RSS 订阅源的 org-element 语法树,并将其渲染为临时 Org 模式文件。
  • 然后,标准的 Org 导出框架接管,使用 ox-rss 后端将该文件导出为 RSS 文件。
  • 在整个过程中,文章内容并未参与其中!订阅源完全基于文件元数据构建。每当我考虑添加内容时,都会被问题的复杂性所困扰:

  • 订阅源内容是否应包含旁注?
  • 是否应执行代码块(例如生成图表)?
  • 如何区分内部链接与图片,以确保各自正确处理?
  • 文章的部分结构在相对受限的 RSS 格式中是否难以完整呈现?
  • 并非说我无法解决所有这些难题,只是不愿为此投入时间,因为这需要耗费大量精力。

    昨天我决定简化问题:不包含内部链接、不执行代码、不显示图片等。只需提取文章的前四个基本元素,如段落、表格和列表。通过类似以下方式,从文件路径获取这些内容是可行的。

    In[1]:

    (defun tw-get-teaser-contents (source-path)
      "Get org-element data of early parts of SOURCE-PATH."
      (with-temp-buffer
        ;; Load file contents into Org mode.
        (insert-file-contents source-path)
        (org-mode)
        ;; Take the first four elements ...
        (seq-take
         ;; ... from a subset of the full parse sequence.
         (org-element-map (org-element-parse-buffer)
             '(paragraph src-block quote-block plain-list)
           #'identity
           nil nil '(quote-block plain-list))
         4)))

    接着,我们必须去除其中的难点(内部链接)、性能问题(代码块执行)和非必要内容(旁注)。虽然存在更好的方法,但我发现了一种迂回的方式来实现。首先,我为 HTML 的一个有限子集创建了一个导出后端。我实际运行的代码稍微复杂一些,以支持我对小型大写字母等的过度使用。但原理是成立的。

    In[2]:

    ;; Custom backend for lo-fi RSS-embedded HTML.
    (org-export-define-derived-backend 'teaser-html 'html
      :translate-alist
      '((link . (lambda (_ contents _) (or contents ""))
        (footnote-reference . (lambda (_ _ _) "")))))

    这可用于生成对 RSS 友好的摘要内容渲染。

    In[3]:

    (defun tw-get-teaser-html (path)
      "Get HTML representing teaser for Org file at PATH."
      ;; Don't syntax highlight code in RSS.
      (let ((org-html-htmlize-output-type nil))
        ;; Practically unbind function to prevent code block
        ;; evaluation without disrupting other babel processing.
        ;;
        ;; This uses cl-letf rather than advice in order to
        ;; revert the change when control leaves this function.
        (cl-letf (((symbol-function 'org-babel-execute-src-block)
                   (lambda (&rest _) "")))
          (org-export-string-as
           ;; Render data back as Org source markup.
           (mapconcat #'org-element-interpret-data
                      (tw-get-teaser-contents path)
                      "\n\n")
           ;; Then export that as limited HTML.
           'teaser-html t '(:with-footnotes nil)))))

    现在我们有低 fidelity 的 HTML,但由于 RSS 文件的源码是 org-element 语法树,因此必须将文章预览作为 HTML 导出块嵌入到语法树中。

    In[4]:

    (defun tw-feed-summary (path)
      (org-element-create 'export-block
        (list :type "HTML"
              :value (tw-get-teaser-html path))))

    这会生成一个 org-element 节点,随后可插入构成 RSS 订阅源的 Org 文档中。

    即便如此,仍有一个问题待解决。ox-rss 导出后端会安装一个输出过滤器,通过调用 indent-region 对整个缓冲区进行处理等方式美化 RSS 文件的 XML 格式。要找出某个函数被调用的位置,可以运行 (debug-on-entry 'indent-region),然后查看堆栈跟踪。在调试器中输入 c 继续执行。之后,通过 cancel-debug-on-entry 可停止重复进入调试器。然而,这种方法不会尊重 CDATA 块中的缩进——尤其是对缩进要求严格的代码块!似乎无法配置此行为,但我们可以覆盖执行此操作的功能,使其直接不做任何事。

    In[5]:

    (advice-add 'org-rss-final-function :override
                (lambda (contents _ _) contents))

    搞定!我认为就是这样了。虽然远非简单(毕竟花了几个小时才搞定),但比我预想的要容易些。

    但更糟糕的是,这段代码简直烂透了。我确信肯定有优雅的实现方式,但我写的显然不是。想想看:这段代码先解析一个 Org 文档,保留部分内容,再将其渲染回 Org 格式,导出为 HTML,嵌入另一个 Org 文件,最后再次导出成 HTML 衍生格式——来来回回,反反复复。

    当有人让我分享用来修补 Org 以构建这个网站的代码时,我总是建议他们直接去读 Emacs 手册和 Org 的源码,因为里面充斥着这种垃圾代码。我对这些细节其实了解不深,只能靠临时拼凑,结果自然很糟。我可不想让别人觉得能从中学到什么好东西!

    需要完整排版与评论请前往来源站点阅读。