rw-r--r--

chown -R toon blog/ && chmod -R u=rw,go=r $_

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 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)))
: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 and RSS_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.