Blogging with Org mode

This blog was built with Org mode, which uses a special syntax for defining the structure of documents, similar to Markdown and other markup languages, allowing Org files to be exported to a variety of formats including HTML. In this post, I describe the steps for creating a blog with Org mode.

Org is a multipurpose plain-text system written in Lisp, so there’s really no one recipe for building a blog with it. In fact, there are quite a few tools for the purpose already. In my case, however, I wanted to understand the publishing process better and have greater control over it, which meant keeping things as simple as possible. As usual, I went on to read what others were doing and stumbled across two compelling solutions (here and here), both using Org’s ox-publish library and a few lines of Lisp to accomplish the task.

The second part of their setup include Gitlab CI for building the website and Gitlab Pages for uploading its content, making Org a lot more desirable for working with websites without having to commit changes to every HTML file produced. This is as simple as untracking the public folder containing the rendered files, which can still be used to preview the website locally. In general, a publishing project consists of a base directory, where the Org files are kept, and a publishing directory, where the HTML files are placed, namely the public folder.

The last part of the process is tweaking the publish.el script and css file, as well as adding a .gitlab-ci.yml file to the root directory which should trigger the CI pipeline after each commit checking if any of the tests fail. With all this in place, the website can be produced with a single command:

emacs --batch --no-init-file --load publish.el --funcall org-publish-all

which takes a few long seconds to execute (fixed in 27cd623). Overall, most default settings worked well, except I had to fine-tune the site-map functions, which serve as a kind of template for the blog’s main page, to create an index of posts that looked more like a blog, with title, date of publication and a short summary. I also wanted to include the RSS code in the blog’s header, with the help of the ox-rss library, and exclude all files containing a “draft_” prefix, so that when a draft is ready for publication, I simply rename it.

(defvar site-blog-header-template
  "\n#+html_head_extra: <link href=\"blog.xml\" type=\"application/rss+xml\" rel=\"alternate\" title=\"Blog\">")

(defun site-blog-publish-sitemap (title list)
  "Publish the blog's site map file."
  (setcdr list (seq-filter
                (lambda (file)
                  (not (string-match "file:draft_" (car file))))
                (cdr list)))
  (mapconcat #'identity
             (list (concat "#+title: " title
                   (org-list-to-subtree list))

(defun site-blog-format-entry (entry style project)
  "Format the blog's main page."
  (when (not (directory-name-p entry))
    (format "[[file:%s][%s]]
#+include: \"%s::preview\"
\[[[file:%s][Read more]]\]"
            (org-publish-find-title entry project)
            (format-time-string "%F" (org-publish-find-date entry project))

To start working on a new draft, all I have to do is create a new file with a default template (see below). In line 5, the date macro formats the date and wraps it in a date block. The text of the post begins on line 9 so that the first paragraph of each post is always reused in the blog’s main page (the Org manual explains how this is done).

 1: #+title:
 2: #+date:
 3: #+description:
 4: #+begin_date
 5: {{{date(%d %b %Y)}}}
 6: #+end_date
 8: #+name: preview
11: [[file:blog.html][Other posts]]\\
12: [[][Comments]]

Lastly, I use a function that changes the post amble conditionally. Namely, it checks the document’s language and translates the post amble accordingly. It also adds the publication date and the date of when the file was last modified.

(defvar site-blog-postamble-format
  '(("en" "footer code")
    ("pt-BR" "something else")))

(defun site-find-keyword-value (keyword)
  "Return the KEYWORD value in an Org file."
  (let* ((parsetree (org-element-parse-buffer 'element))
         (keywords (org-element-map parsetree 'keyword
                     (lambda (kw)
                       (cons (org-element-property :key kw)
                             (org-element-property :value kw))))))
    (assoc-default keyword keywords)))

(defun site-blog-postamble (info)
  "Format post amble conditionally."
  (let* ((file (plist-get info :input-file))
         (date (site-find-keyword-value "DATE"))
         (pubdate (if date (replace-regexp-in-string "<\\|\s.+" "" date) ""))
         (modtime (if (site-git-file-tracked-p file)
                      (format "<a href=%s/commits/master/%s>%s</a>"
                              (file-relative-name file site-root)
                              (site-git-last-update-date file))
                    (format-time-string "%F"
                                        (nth 5 (file-attributes file))))))
    (unless (string= (car (plist-get info :title)) "Blog")
      (let ((lang (site-find-keyword-value "LANGUAGE")))
        (format (car (assoc-default (if (string= lang "pt-BR")
                                        "pt-BR" "en")
                pubdate modtime)))))

Of course, none of this is 100% perfect and things are still likely to change, but I’m happy with the result so far. The system is easy to maintain, fully open, and comes with all the benefits of Emacs, which gives me greater control over non-free alternatives and intellectual stimulation over the publishing medium I work with.

🏷️ emacs, web