Blogging with org-mode

I thought I'd christen my new blog with a post about how I'll be publishing it. For the uninitiated, org-mode is markdown before markdown was cool. You can do everything you can do with markdown, and a whole bunch of other things (TODO lists, calendars, spreadsheets etc.).

Most importantly for my purposes, org-mode supports publishing HTML documents. What is old is new again, and static site generators are the new hotness. The internet is brimming with shitty static site generators1 and an alarming number of posts about how to host HTML documents, but before there was "static site generators" and "markdown middleware", there was org-mode.

The setup

(setq my-html-preamble (format "<a href=\"%s\"><img src=\"%s\" style=\"border: 0\" width=\"16\" height=\"16\" /></a>"
                               (concat blog-home-link "index.xml")
                               (concat blog-home-link "images/feed-icon.png")))

(setq org-publish-project-alist
         :base-directory ,blog-base-directory
         :base-extension "org"
         :section-numbers nil
         :publishing-directory ,my-org-mode-blog-url
         :publishing-function org-html-publish-to-html
         :auto-sitemap t
         :sitemap-title "Blog"
         ;; :sitemap-sort-files anti-chronologically
         :sitemap-filename "index.org"
         :sitemap-function my-sitemap-publish
         :with-toc nil
         :html-link-home ,blog-home-link
         :html-link-up ,blog-home-link
         :recursive t
         :preparation-function org-mode-blog-prepare
         :html-preamble ,my-html-preamble
         :html-postamble nil)
         :base-directory ,(concat blog-base-directory "/images")
         :base-extension "jpg\\|gif\\|png"
         :publishing-directory ,(concat my-org-mode-blog-url "/images")
         :publishing-function org-publish-attachment
         :recursive t)
         :base-directory ,blog-base-directory
         :base-extension "org"
         :publishing-directory ,my-org-mode-blog-url
         :publishing-function org-rss-publish-to-rss
         :html-link-home ,blog-home-link
         :html-link-use-abs-url t
         :include ("index.org")
         :exclude ".*")
        ("blog" :components ("blog-pages" "blog-images" "blog-rss"))))

This is all fairly well explained in the org-mode docs with a few exceptions. my-org-mode-blog-url contains a TRAMP URL pointing to my remote webroot. Emacs generates the HTML, and deploys it to my webserver with TRAMP.


This is the tricky bit. Org-mode contrib includes ox-rss, which will publish an org file as an RSS feed, but doesn't include a mechanism for generating the org file. Luckily, org-mode allows you to generate a sitemap, which can double as an RSS feed. The sitemap function provided with org-mode does not include the date each page was published, so I wrote my own which includes properties for the RSS feed.

(defun my-sitemap-publish (project &optional sitemap-filename)
  (let* ((project-plist (cdr project))
         (sitemap-title (plist-get project-plist :sitemap-title))
         (dir (file-name-as-directory
               (plist-get project-plist :base-directory)))
         (exclude-regexp (plist-get project-plist :exclude))
         (files (nreverse
                 (org-publish-get-base-files project exclude-regexp)))
         (sitemap-filename (concat dir (or sitemap-filename "sitemap.org")))
          (plist-get project-plist :sitemap-sans-extension))
         (visiting (find-buffer-visiting sitemap-filename))
         file sitemap-buffer)
    (let ((sitemap-entries (get-sitemap-entries files dir)))
          (let ((org-inhibit-startup t))
            (setq sitemap-buffer
                  (or visiting (find-file sitemap-filename))))
        (insert (format "#+TITLE: %s\n\n" sitemap-title))
        (dolist (entry sitemap-entries)
           (format "* [[file:%s][%s]]
Last update: %s\\\\
Published: %s
                   (plist-get entry :path)
                   (car (plist-get entry :title))
                   (format-time-string (cdr org-time-stamp-formats) (plist-get entry :parsed-date))
                   (concat (file-name-sans-extension (plist-get entry :path)) ".html")
                   (plist-get entry :description)
                   (format-time-string "%Y-%m-%d" (plist-get entry :git-date))
                   (format-time-string "%Y-%m-%d" (plist-get entry :parsed-date)))))
    (or visiting (kill-buffer sitemap-buffer))))

(defun get-sitemap-entries (files dir)
  "Hydrate sitemap entries with custom keywords."
  (let (entries)
    (dolist (file files)
      (catch 'stop
        (let* ((env (org-combine-plists (org-babel-with-temp-filebuffer file (org-export-get-environment))))
               (date (or (apply 'encode-time (org-parse-time-string
                                              (or (car (plist-get env :date)) (throw 'stop nil))))))
               (git-date (date-to-time (magit-git-string "log" "-1" "--format=%ci" file)))
               (path (file-relative-name file dir))
               (description (org-babel-with-temp-filebuffer file (my-org-get-keyword "DESCRIPTION"))))
          (plist-put env :path path)
          (plist-put env :parsed-date date)
          (plist-put env :git-date git-date)
          (plist-put env :description description)
          (push env entries))))
    (sort entries (lambda (a b) (time-less-p (plist-get b :parsed-date) (plist-get a :parsed-date))))))

(defun my-org-get-keywords ()
  (org-element-map (org-element-parse-buffer 'element) 'keyword
    (lambda (keyword) (cons (org-element-property :key keyword)
                            (org-element-property :value keyword)))))

(defun my-org-get-keyword (keyword)
  (cdr (assoc keyword (my-org-get-keywords))))

This way I include a DATE and a DESCRIPTION keyword in each post, and they'll be automagically included in my sitemap and RSS feed. The last modified date is taken from git.

#+TITLE: Blogging with org-mode
#+DATE: 2017-03-25
#+DESCRIPTION: First post


Comment systems are lame, if I decide to solicit comments on a post I'll just ask people to email me and manually include any comments that pass muster.


TODO Investigate whether org-mode will sort by DATE keyword on its own

Currently the code does its own anti-chronological sorting, this might not be necessary.

TODO Use org date format for DATE

Currently the DATE field is parsed outside of org, it should use org's datetime format.

TODO Multiline descriptions


The future

I could include custom CSS and jazz this up a little, but I think the default styles are readable, and it renders fine on my phone.

Watch this blog for:

  1. Pictures of my fish
  2. Technical content that sucks less than this
  3. Shameless self-promotion



I thought I was being trolled until I saw the list of sites using it.