Blogging using org-mode (and nothing else)

Published: February 01, 2016

Update December 27, 2022: I finally (after 4 years!) fixed a compatibility issue with org-mode > 9.1. See this blog post for an updated org-sitemap-function and an updated project definition. I'm leaving this post up to preserve history.

As you can tell, the look of this website has changed significantly—and it was about time for that. In case you didn't know, this site used to be hosted on http://web.ics.purdue.edu/, which provides free webspace for Purdue students. I used to generate the static HTML pages from plaintext markdown files using the Python-based static site generator Pelican. It worked well for a while, but I ended up having a few issues with that setup:

So, of course, I looked for an alternative.

Since I was predisposed towards using Org-mode for this, Org's publishing feature was the first alternative I investigated. I don't have high demands—all I need is a lightweight, stable, static site generator. I don't need tag clouds, sophisticated pagination, theme support (I'm fine with hacking together my own CSS), or any kind of plugin support; all I really need is a small org-to-HTML converter that can be hacked using Elisp and that I can bolt the extras that I want onto. After browsing around a little bit, I found some neat examples here, here and here. These sites are exactly what I wanted—minimalist, simple, and based on pure Org.

Obviously, the Org publishing feature was all that I needed. I whipped up a nice little configuration that produces this website from a set of Org source files, some custom CSS and HTML, and some custom Elisp. This is what happens at a high level:

Here's the obligatory screenshot of me editing this post in Emacs:

blogging_with_org_small.png

There are a few tricks involved in doing this—mostly concerning the generation of the sitemap—, but I will let the relevant section of my config (Update April 24, 2020: This has moved to here) speak for themselves, since they're pretty well commented (I think):

UPDATE February 02, 2016:

I've received a few questions asking whether there exists an RSS feed for this blog. There wasn't until today, but I think this will work. I'm using ox-rss.el to generate XML from the blog's sitemap. I had to trick it into doing a few things like generating the correct pubdates and permalinks, but I think it works fine for what I need. The code below is the updated version, with RSS.

The publishing uses the Org HTML export backend a lot, so to start off, we require it here, along with the RSS publishing backend.

(require 'ox-html)
(require 'ox-rss)
(setq org-export-html-coding-system 'utf-8-unix)
(setq org-html-viewport nil)

Next, we define some functions and variables that will be used by org-publish. First, let's define the website headers, footers, and make sure that the exported HTML points to the right style sheets.

(setq my-blog-extra-head
      (concat
       "<link rel='stylesheet' href='/../res/code.css' />\n"
       "<link rel='stylesheet' href='/../res/main.css' />"))

(setq my-blog-header-file "~/repos/blog/header.html")
(defun my-blog-header (arg)
  (with-temp-buffer
    (insert-file-contents my-blog-header-file)
    (buffer-string)))

(setq my-blog-footer
      "<hr />\n
<p><span style=\"float: left;\"><a href= \"/blog.xml\">RSS</a></span>
License: <a href= \"https://creativecommons.org/licenses/by-sa/4.0/\">CC BY-SA 4.0</a></p>\n
<p><a href= \"/contact.html\"> Contact</a></p>\n")

I'd also like to export drawers out to HTML; this idea is ripped directly from here.

(defun my-blog-org-export-format-drawer (name content)
  (concat "<div class=\"drawer " (downcase name) "\">\n"
    "<h6>" (capitalize name) "</h6>\n"
    content
    "\n</div>"))

MathJax usually recommends to use their CDN to load their JavaScript code, but I want to use a version that sits on my server.

(setq my-blog-local-mathjax
      '((path "/res/mj/MathJax.js?config=TeX-AMS-MML_HTMLorMML")
        (scale "100") (align "center") (indent "2em") (tagside "right")
        (mathml nil)))

Now we'll get to some of the customizations I've bolted on Org's publishing features. In it's standard configuration, the sitemap generator produces a plain, kind of boring looking list of posts, which was inadequate for me. After hacking on the sitemap generation function for a little while, I came up with the following solution: When I write a blog post, I enclose the "preview" part of the post in #+BEGIN_PREVIEW...#+END_PREVIEW tags, which my (very simple) parser then inserts into the sitemap page.

(defun my-blog-get-preview (file)
  "The comments in FILE have to be on their own lines, prefereably before and after paragraphs."
  (with-temp-buffer
    (insert-file-contents file)
    (goto-char (point-min))
    (let ((beg (+ 1 (re-search-forward "^#\\+BEGIN_PREVIEW$")))
          (end (progn (re-search-forward "^#\\+END_PREVIEW$")
                      (match-beginning 0))))
      (buffer-substring beg end))))

(defun my-blog-sitemap (project &optional sitemap-filename)
  "Generate the sitemap for my blog."
  (let* ((project-plist (cdr project))
         (dir (file-name-as-directory
               (plist-get project-plist :base-directory)))
         (localdir (file-name-directory dir))
         (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")))
         (sitemap-sans-extension
          (plist-get project-plist :sitemap-sans-extension))
         (visiting (find-buffer-visiting sitemap-filename))
         file sitemap-buffer)
    (with-current-buffer
        (let ((org-inhibit-startup t))
          (setq sitemap-buffer
                (or visiting (find-file sitemap-filename))))
      (erase-buffer)
      ;; loop through all of the files in the project
      (while (setq file (pop files))
        (let ((fn (file-name-nondirectory file))
              (link ;; changed this to fix links. see postprocessor.
               (file-relative-name file (file-name-as-directory
                                         (expand-file-name (concat (file-name-as-directory dir) "..")))))
              (oldlocal localdir))
          (when sitemap-sans-extension
            (setq link (file-name-sans-extension link)))
          ;; sitemap shouldn't list itself
          (unless (equal (file-truename sitemap-filename)
                         (file-truename file))
            (let (;; get the title and date of the current file
                  (title (org-publish-format-file-entry "%t" file project-plist))
                  (date (org-publish-format-file-entry "%d" file project-plist))
                  ;; get the preview section from the current file
                  (preview (my-blog-get-preview file))
                  (regexp "\\(.*\\)\\[\\([^][]+\\)\\]\\(.*\\)"))
              ;; insert a horizontal line before every post, kill the first one
              ;; before saving
              (insert "-----\n")
              (cond ((string-match-p regexp title)
                     (string-match regexp title)
                     ;; insert every post as headline
                     (insert (concat"* " (match-string 1 title)
                                    "[[file:" link "]["
                                    (match-string 2 title)
                                    "]]" (match-string 3 title) "\n")))
                    (t (insert (concat "* [[file:" link "][" title "]]\n"))))
              ;; add properties for `ox-rss.el' here
              (let ((rss-permalink (concat (file-name-sans-extension link) ".html"))
                    (rss-pubdate (format-time-string
                                  (car org-time-stamp-formats)
                                  (org-publish-find-date file))))
                (org-set-property "RSS_PERMALINK" rss-permalink)
                (org-set-property "PUBDATE" rss-pubdate))
              ;; insert the date, preview, & read more link
              (insert (concat date "\n\n"))
              (insert preview)
              (insert (concat "[[file:" link "][Read More...]]\n"))))))
      ;; kill the first hrule to make this look OK
      (goto-char (point-min))
      (let ((kill-whole-line t)) (kill-line))
      (save-buffer))
    (or visiting (kill-buffer sitemap-buffer))))

Next I define some pre-and postprocessors that run during the publishing process. They are used to move around some files before and after publishing.

(setq my-blog-emacs-config-name "emacsconfig.org")
(setq my-blog-process-emacs-config nil)

(defun my-blog-pages-preprocessor ()
  "Move a fresh version of the settings.org file to the pages directory."
  (when my-blog-process-emacs-config
    (let* ((cfg-file (expand-file-name (concat (file-name-as-directory user-emacs-directory)
                                               "settings.org")))
           (destdir (file-name-as-directory (plist-get project-plist :base-directory)))
           (cfg-file-dest (expand-file-name (concat destdir my-blog-emacs-config-name))))
      (copy-file cfg-file cfg-file-dest t))))

(defun my-blog-pages-postprocessor ()
  (message "In the pages postprocessor."))

(defun my-blog-articles-preprocessor ()
  (message "In the articles preprocessor."))

(defun my-blog-articles-postprocessor ()
  "Massage the sitemap file and move it up one directory.

for this to work, we have already fixed the creation of the
relative link in the sitemap-publish function"
  (let* ((sitemap-fn (concat (file-name-sans-extension (plist-get project-plist :sitemap-filename)) ".html"))
         (sitemap-olddir (plist-get project-plist :publishing-directory))
         (sitemap-newdir (expand-file-name (concat (file-name-as-directory sitemap-olddir) "..")))
         (sitemap-oldfile (expand-file-name sitemap-fn sitemap-olddir))
         (sitemap-newfile (expand-file-name (concat (file-name-as-directory sitemap-newdir) sitemap-fn))))
    (with-temp-buffer
      (goto-char (point-min))
      (insert-file-contents sitemap-oldfile)
      ;; massage the sitemap if wanted

      ;; delete the old file and write the correct one
      (delete-file sitemap-oldfile)
      (write-file sitemap-newfile))))

The next preprocessor runs CSSTidy on the site's CSS.

(defun my-blog-minify-css ()
  (let* ((csstidy "csstidy")
         (csstidy-args " --template=highest --silent=true")
         (css-dir (expand-file-name (plist-get project-plist :publishing-directory)))
         (css-files (directory-files css-dir t "^.*\\.css$")))
    (dolist (file css-files)
      (with-temp-buffer
        (insert (shell-command-to-string (concat csstidy " " file csstidy-args)))
        (write-file file)))))

Most of the publishing settings are defined in org-publish-project-alist.

(setq org-publish-project-alist
      `(("blog"
         :components ("blog-articles", "blog-pages", "blog-rss", "blog-res", "blog-images", "blog-dl"))
        ("blog-articles"
         :base-directory "~/repos/blog/blog/"
         :base-extension "org"
         :publishing-directory "~/repos/blog/www/blog/"
         :publishing-function org-html-publish-to-html
         :preparation-function my-blog-articles-preprocessor
         :completion-function my-blog-articles-postprocessor
         :htmlized-source t ;; this enables htmlize, which means that I can use css for code!

         :with-author t
         :with-creator nil
         :with-date t

         :headline-level 4
         :section-numbers nil
         :with-toc nil
         :with-drawers t
         :with-sub-superscript nil ;; important!!

         ;; the following removes extra headers from HTML output -- important!
         :html-link-home "/"
         :html-head nil ;; cleans up anything that would have been in there.
         :html-head-extra ,my-blog-extra-head
         :html-head-include-default-style nil
         :html-head-include-scripts nil
         :html-viewport nil

         :html-format-drawer-function my-blog-org-export-format-drawer
         :html-home/up-format ""
         :html-mathjax-options ,my-blog-local-mathjax
         :html-mathjax-template "<script type=\"text/javascript\" src=\"%PATH\"></script>"
         :html-footnotes-section "<div id='footnotes'><!--%s-->%s</div>"
         :html-link-up ""
         :html-link-home ""
         :html-preamble my-blog-header
         :html-postamble ,my-blog-footer

         ;; sitemap - list of blog articles
         :auto-sitemap t
         :sitemap-filename "blog.org"
         :sitemap-title "Blog"
         ;; custom sitemap generator function
         :sitemap-function my-blog-sitemap
         :sitemap-sort-files anti-chronologically
         :sitemap-date-format "Published: %a %b %d %Y")
        ("blog-pages"
         :base-directory "~/repos/blog/pages/"
         :base-extension "org"
         :publishing-directory "~/repos/blog/www/"
         :publishing-function org-html-publish-to-html
         :preparation-function my-blog-pages-preprocessor
         :completion-function my-blog-pages-postprocessor
         :htmlized-source t

         :with-author t
         :with-creator nil
         :with-date t

         :headline-level 4
         :section-numbers nil
         :with-toc nil
         :with-drawers t
         :with-sub-superscript nil ;; important!!
         :html-viewport nil ;; hasn't worked yet

         ;; the following removes extra headers from HTML output -- important!
         :html-link-home "/"
         :html-head nil ;; cleans up anything that would have been in there.
         :html-head-extra ,my-blog-extra-head
         :html-head-include-default-style nil
         :html-head-include-scripts nil

         :html-format-drawer-function my-blog-org-export-format-drawer
         :html-home/up-format ""
         :html-mathjax-options ,my-blog-local-mathjax
         :html-mathjax-template "<script type=\"text/javascript\" src=\"%PATH\"></script>"
         :html-footnotes-section "<div id='footnotes'><!--%s-->%s</div>"
         :html-link-up ""
         :html-link-home ""

         :html-preamble my-blog-header
         :html-postamble ,my-blog-footer)
        ("blog-rss"
         :base-directory "~/repos/blog/blog/"
         :base-extension "org"
         :publishing-directory "~/repos/blog/www/"
         :publishing-function org-rss-publish-to-rss

         :html-link-home "https://ogbe.net/"
         :html-link-use-abs-url t

         :title "Dennis Ogbe"
         :rss-image-url "https://ogbe.loc/img/feed-icon-28x28.png"
         :section-numbers nil
         :exclude ".*"
         :include ("blog.org")
         :table-of-contents nil)
        ("blog-res"
         :base-directory "~/repos/blog/res/"
         :base-extension ".*"
         :publishing-directory "~/repos/blog/www/res/"
         :publishing-function org-publish-attachment
         :completion-function my-blog-minify-css)
        ("blog-images"
         :base-directory "~/repos/blog/img/"
         :base-extension ".*"
         :publishing-directory "~/repos/blog/www/img/"
         :publishing-function org-publish-attachment
         :recursive t)
        ("blog-dl"
         :base-directory "~/repos/blog/dl/"
         :base-extension ".*"
         :publishing-directory "~/repos/blog/www/dl/"
         :publishing-function org-publish-attachment
         :Recursive t)))

Finally, define a small template for new blog posts.

(add-to-list 'org-structure-template-alist
             '("b" "#+TITLE: ?
#+AUTHOR: Dennis Ogbe
#+EMAIL: [email protected]
#+DATE:
#+STARTUP: showall
#+STARTUP: inlineimages
#+BEGIN_PREVIEW\n\n#+END_PREVIEW\n"))

If you'd like, you can let me know what you think. I appreciate any sorts of feedback.

Dennis