Org blog with RSS

Let's add RSS feed to blog

Table of Contents

Why do you even need RSS?

RSS might seem like an outdated, marginal thing. But it still has at least one benefit—you can use an RSS feed as a sitemap for search engines. Plus, it's pretty geeky.

Add RSS feed

So, what's happening here? Let's start by integrating our templating functions into the build.

Use sitemap backend in the build

(setq org-publish-project-alist
      (list
       (list "blog-rss"
	     :author "Alex M"
	     :email "iam@fidonode.me"
	     :base-directory my/blog-src-path
	     :base-extension "org"
	     :recursive t
	     :exclude (regexp-opt '("rss.org" "index.org" "404.org" "posts.org"))
	     :publishing-function 'my/publish-to-rss
	     :publishing-directory my/web-export-path
	     :rss-extension "xml"
	     :html-link-home my/url
	     :html-link-use-abs-url t
	     :html-link-org-files-as-html t
	     :auto-sitemap t
	     :sitemap-filename "rss.org"
	     :sitemap-title "rss"
	     :sitemap-style 'list
	     :sitemap-sort-files 'anti-chronologically
	     :sitemap-function 'my/format-rss-feed
	     :sitemap-format-entry 'my/format-rss-feed-entry)
       ))

How does it work? As you can see, we use the default sitemap generator from Org Export with some additional steps. By default, the sitemap generator collects all org files from :base-directory. It then places links to these files as separate entries into an intermediate org file and publishes this org file. We use custom functions for collecting entries, formatting entries, and publishing the org file.

Publishing and formatting functions

We need a mandatory dependency because we don't want to mess with forming correct XML by ourselves.

(require 'ox-rss)

Here's the core of the process. We need to prepare entries before feeding them to the org-rss-publish-to-rss function. Since we're doing it our own way, we need to start by creating a bullet with a link to the post, and then add RSS_PERMALINK, RSS_TITLE, and PUBDATE. I also add the first two lines of the blog post instead of a description. This is the native way to do a preview in the RSS world.

(defun my/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
	     (org-mode)
	     (insert (format "* [[file:%s][%s]]\n" file title))
	     (org-set-property "RSS_PERMALINK" link)
	     (org-set-property "RSS_TITLE" title)
	     (org-set-property "PUBDATE" date)
	     (let ((first-two-lines (with-temp-buffer
				      (insert-file-contents file)
				      (buffer-substring-no-properties
				       (point-min)
				       (progn (forward-line 2) (point))))))
	       (if (string-suffix-p "\n" first-two-lines)
		   (setq first-two-lines (substring first-two-lines 0 -1)))
	       (insert first-two-lines))
	     (goto-char (point-max))
	     (insert "...")
	     (buffer-string))))
	((eq style 'tree)
	 ;; Return only last subdir.
	 (file-name-nondirectory (directory-file-name entry)))
	(t entry)))

This function creates the content of the intermediate org file. Mostly a rudimentary title, and then we ask Org to unwind the list of collected files with the my/format-rss-feed-entry function.

(defun my/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"
	  "#+STARTUP: showall \n\n"
	  (org-list-to-subtree list 1 '(:icount "" :istart ""))))

This function replaces the default publishing function to filter everything except the intermediate file before publishing.

(defun my/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)))

Voila! Now you have an rss.xml in your export path.