Org mode blogging: RSS feed
posted on 2018-12-30
After my previous two posts about Org mode blogging, yet another one.
RSS feed
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
have to (require)
them explicitly.
ox-rss
The ox-rss
extension is a org-export
backend derived from the HTML
backend and it exports .org
files to a RSS .xml
files.
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:
sitemap. 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
using in org-publish-project-alist
.
(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:
:base-directory
&:base-extension
- These are the same as the HTML export. So I’m handling the same files.
:exclude
- I’m excluding some files like
index.org
. :publishing-function
It’s a very simple function around
org-rss-publish-to-rss
. But it only calls that function when the filename equalsrss.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)))
:sitemap-function
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 ""))))
:sitemap-format-entry
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
PUBDATE
andRSS_PERMALINK
.(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.
Conclusion
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.