After my previous two posts about Org mode blogging, yet another one.
I’m not sure how many people still care about RSS, but for my blog I still wanted to have an RSS feed. (TBH: I don’t use RSS myself ¯\_(ツ)_/¯)
Luckily, Org mode includes the
ox-rss contrib extension. Such
extensions are bundled with Org mode, but are not loaded by
default. You can use them without additional installation, but you
(require) them explicitly.
ox-rss extension is a
org-export backend derived from the HTML
backend and it exports
.org files to a RSS
One problem though, I’m putting each blog post in a separate file, and the RSS feed should be a single file containing all posts…
(Ab)Using the sitemap functionality
I considered writing some code myself to join all posts together, but
then I realized
org-publish —sort of— has this feature already:
org-publish can generate a sitemap, an index of all
files. And it has sorting built-in. I’m already using it to generate
the landing page of this blog.
To overcome the issue of a 1-to-1 export for RSS, I decided to abuse
the sitemap functionality in
org-publish. This is the block I’m
(list "blog-rss" :base-directory "posts" :base-extension "org" :recursive nil :exclude (regexp-opt '("rss.org" "index.org" "404.org")) :publishing-function 'rw/org-rss-publish-to-rss :publishing-directory "./public" :rss-extension "xml" :html-link-home rw-url :html-link-use-abs-url t :html-link-org-files-as-html t :auto-sitemap t :sitemap-filename "rss.org" :sitemap-title rw-title :sitemap-style 'list :sitemap-sort-files 'anti-chronologically :sitemap-function 'rw/format-rss-feed :sitemap-format-entry 'rw/format-rss-feed-entry)
The most important properties here are:
- These are the same as the HTML export. So I’m handling the same files.
- I’m excluding some files like
It’s a very simple function around
org-rss-publish-to-rss. But it only calls that function when the filename equals
rss.org. Any other file is not published to RSS/XML.
(defun rw/org-rss-publish-to-rss (plist filename pub-dir) "Publish RSS with PLIST, only when FILENAME is 'rss.org'. PUB-DIR is when the output will be placed." (if (equal "rss.org" (file-name-nondirectory filename)) (org-rss-publish-to-rss plist filename pub-dir)))
It’s a function calling
(org-list-to-subtree)to convert the Elisp list of posts to a Org mode list where the top level headlines are the titles of each post.
(defun rw/format-rss-feed (title list) "Generate RSS feed, as a string. TITLE is the title of the RSS feed. LIST is an internal representation for the files to include, as returned by `org-list-to-lisp'. PROJECT is the current project." (concat "#+TITLE: " title "\n\n" (org-list-to-subtree list '(:icount "" :istart ""))))
Normally this would only return the title of each post, but in my case I’m using a function that returns the whole body of the post, with some extra properties like
(defun rw/format-rss-feed-entry (entry style project) "Format ENTRY for the RSS feed. ENTRY is a file name. STYLE is either 'list' or 'tree'. PROJECT is the current project." (cond ((not (directory-name-p entry)) (let* ((file (org-publish--expand-file-name entry project)) (title (org-publish-find-title entry project)) (date (format-time-string "%Y-%m-%d" (org-publish-find-date entry project))) (link (concat (file-name-sans-extension entry) ".html"))) (with-temp-buffer (insert (format "* [[file:%s][%s]]\n" file title)) (org-set-property "RSS_PERMALINK" link) (org-set-property "PUBDATE" date) (insert-file-contents file) (buffer-string)))) ((eq style 'tree) ;; Return only last subdir. (file-name-nondirectory (directory-file-name entry))) (t entry)))
The complete implementation also can be found in the git repo.
Looking back on the code, the solution seems very simple. But to be honest, I took me quite a while to get this working properly. So I hope I can help others with what I’ve built here.
I’ve noticed everybody formats their RSS differently, but it should be easy to customize this solution to anyone’s needs.
Comments are welcome on Reddit.
Also covered at Irreal.