Blogging with Org mode

And using Emacs as a build tool

Posted June 2017

As my love-affair with Org mode continue to deepen, blogging with Markdown Jekyll was starting to chafe. I wanted to write my blog posts in Org mode instead. I looked around to find a Jekyll plugin that would allow that, campaigned to have it updated to Jekyll 3, and opened a pull request against pages-gem to whitelist this plugin for use with GitHub Pages. Unfortunately, this PR was given the silent treatment and I ended up closing it: given all the attention my one-line change was not getting, it was clear they were not keen to merge it. Thus I decided to transform by blog into an Org mode project, and I now use Org mode's publishing to generate the HTML pages.

Why move away from Jekyll? I could have moved to just use the plugin locally, but given I had to check in generated HTML anyway I saw few benefits to it. I was in a bad place with Jekyll: for various boring reasons almost every time I wanted to blog I had to install ruby & Jekyll anew—and potentially struggle to install nokogiri again.

What are the benefits to going full Org mode? Well, the editing experience is just so much better, IMO. It supports a rich & extensible set of markup that is nevertheless very easy to get right. The table support is fantastic. Inter-linking of documents works incredibly well, and works both in source form and exported form. The support for editing code examples with syntax highlighting provided by the major mode is outstanding. Org mode supports inline images in the source version. Finally it's written in—and can be customised with—Emacs lisp, which is much more approachable (to me) than Ruby.

In short, I'm hoping that this transformation will reduce my (mostly artificial) barrier to blogging, aiding me to blog more often. To further reduce the barrier to entry I have an Org template for blog posts, which you can see in listing 1. Org mode will prompt for all the %^{Something} entries.

#+title: %^{Title}
#+date: %<%Y-%m-%d>
#+begin_abstract
%^{Abstract}
#+end_abstract
#+index: %^{Concept Index Entry}

%?

I include this in my capture templates in a slightly unusual way. Usually Org is expecting to capture into existing files, but I want a new file created each time with a name I specify. So at capture time I separately query for a title to use in the URL—aka "the slug". I cannot take this from the capture template expansion of %^{Title}, regretfully, because the prompts for that don't run until the file has been created. The capture entry can be seen in listing 2, and the function to prompt for the slug in listing 3.

(add-to-list 'org-capture-templates
             '("b" "Blog Post" plain
               (file (capture-blog-post-file))
               (file "tpl-blog-post.org")))
(defun capture-blog-post-file ()
  (let* ((title (read-string "Slug: "))
         (slug (replace-regexp-in-string "[^a-z]+" "-" (downcase title))))
    (expand-file-name
     (format "~/blog/articles/%s/%s.org"
             (format-time-string "%Y" (current-time))
             slug))))

Finally, the code to build the blog are available in a script called build.el in my blog's root. It can be run from the command line with Emacs in batch mode, or I could eval the configuration into my running Emacs instance and publish individual pages while I'm working on them. The current version of this script can be seen in listing 4.

#!/usr/bin/env emacs --script

(require 'package)
(package-initialize)

;; Require modes used for syntax highlighting of code examples
(require 'clojure-mode)
(require 'scala-mode)
(require 'cc-mode)
(require 'sh-script)

;; Require org export
(require 'ox)


(setq project-path (file-name-directory
                    (or (buffer-file-name)
                        load-file-name))

      ;; Avoid foo~ backup files everywhere
      backup-directory-alist `(("." . ,(concat user-emacs-directory "backups")))

      ;; For now deploy side-by-side, since I cannot figure out how to
      ;; simulate a Jekyll site so it will deploy from _site :-(
      publish-path project-path ;; (concat project-path "_site/")

      org-html-doctype "html5"

      org-html-home/up-format "
<div id=\"org-div-home-and-up\">
  <img src=\"/images/logo.png\" alt=\"Superloopy Logo\"/>
  <nav>
    <ul>
      <!-- <li><a accesskey=\"h\" href=\"%s\"> Up </a></li>\n -->
      <li><a accesskey=\"H\" href=\"%s\"> Home </a></li>
      <li><a accesskey=\"a\" href=\"/articles\"> Articles </a></li>
      <li><a accesskey=\"p\" href=\"/publications.html\"> Publications </a></li>
      <li><a accesskey=\"A\" href=\"/about.html\"> About </a></li>
    </ul>
  </nav>
</div>
"

      org-html-head (concat "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/main.css\" />\n"
                            "<link rel=\"icon\" type=\"image/png\" href=\"/images/icon.png\" />")

      org-html-scripts (concat org-html-scripts
                               "
<script type=\"text/javascript\">
if(/superloopy\.io/.test(window.location.hostname)) {
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  ga('create', 'UA-4113456-6', 'auto');
  ga('send', 'pageview');
}
</script>")

      org-html-link-home "/"
      org-html-link-up "/"

      org-export-with-toc nil
      org-export-with-author t
      org-export-with-email nil
      org-export-with-creator nil
      org-export-with-section-numbers nil

      org-html-preamble nil
      org-html-postamble 'auto

      org-publish-project-alist
      `(("static"
         :base-directory ,project-path
         :base-extension "css\\|png\\|jpg\\|pdf"
         :exclude "_site"
         :publishing-directory ,publish-path
         :publishing-function org-publish-attachment
         :recursive t)

        ("home"
         :base-directory ,project-path
         :publishing-directory ,publish-path
         :publishing-function org-html-publish-to-html)

        ("articles"
         :base-directory ,(concat project-path "articles")
         :makeindex t
         :publishing-directory ,(concat publish-path "articles")
         :publishing-function org-html-publish-to-html
         :recursive t)))

(org-publish-all)

Some of my recent posts had been written in Org mode already, but had been exported to HTML for publication as per my previous post on blogging. Those were easy to convert, and required only minor edits. Once it was clear that the concept would work, I wrote a script that did a decent first pass of transforming the existing articles from Markdown to Org using Pandoc, and then fixed the remaining doodahs manually. You can see my hacky conversion script in listing 5.

function md2org --description 'Convert .md blog post to Org'
        for file in $argv
                set year (echo $file | cut -d- -f1)
                set date (echo $file | cut -d- -f1-3)
                set slug (echo $file | cut -d- -f4- | cut -d. -f1)

                mkdir -p ~/blog/articles/$year
                set f ~/blog/articles/$year/$slug.org

                echo "#+"(grep -m 1 '^title:' $file) > $f
                echo "#+date: $date" >> $f

                if set abstract (grep -m 1 '^abstract:' $file)
                        set abstract (echo $abstract | sed 's/abstract: //')
                        echo "#+begin_abstract" >> $f
                        echo $abstract >> $f
                        echo "#+end_abstract" >> $f
                end

                if set tags (grep -m 1 '^tags:' $file)
                        set tags (sed 's/tags: //' | tr -d '[,]')
                        for tag in $tags
                                echo "#+index: $tag" >> $f
                        end
                end

                echo >> $f

                sed '/^---$/,/^---$/d' $file \
                | pandoc --no-highlight \
                -f markdown+backtick_code_blocks+fenced_code_attributes \
                -t org \
                >> $f

                echo $year -- $date -- $slug
        end
end

Preview. One of the nice things Jekyll provided was a web server, so you could preview things in a browser properly. In this new solution I've been using Python's SimpleHTTPServer instead. I cd into the blog directory in a Terminal window and issue the following command, which serves files from the current directory as a web site.

python -m SimpleHTTPServer

What is missing? Obviously things are not perfect. If they were, there would be no reason to continue this blog! Here are two specific gripes I haven't managed to solve yet: