Blogging With Emacs

Table of Contents

1 Why

I wanted to use a static site generator because for me it seems a nice compromise between raw HTML, which is lightweight but cumbersome, and a full-blown CMS, which would be heavyweight and cumbersome. I used Jekyll in 2009 back when it was lightweight (albeit less powerful), but in 2016 it looked like I'd need to master too many things that I've no use for otherwise. In contrast, I use Emacs and Org Mode on a daily basis. Consequently, leveraging and deepening my knowledge of these tools through daily calisthenics with a blog just seemed like a worthwhile thing to do. YMMV, natch.

2 How

2.1 Org Mode Publishing

Publishing documents (blog posts, articles, about pages, etc.) are kept as Emacs Org Mode text files with a .org extension. They're stored in a simple directory tree that's a Git repository whose "origin" remote is in GitHub. Org Mode supports not just exporting individual text files as HTML (among other things), but also publishing a related set of text files with shared characteristics. That's accomplished by configuring a key Org Mode variable. Now, there are many ways to do that (e.g, during Emacs initialization). My preferred way is to use Emacs directory local variables.

2.2 Directory Local Variables

I use Emacs directory local variables to great effect in setting up projects in EDE and thought I could apply the same pattern here. "File local variables" are bound automatically for a single file and "directory local variables are bound automatically for all files visited within a directory tree. That's perfect for Org Mode publishing!

;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")

Directory local variables are bound for file buffers in a given mode and support pseudo-variables like eval in order to use side-effects.

((org-mode
  .
  ((eval
    .

Moreover, applying dir-locals-find-file avoids hard-coding the project root directory.

(let ((project-root (car (dir-locals-find-file (buffer-file-name)))))

The rest largely comprises advising functions and binding buffer local variables to fine-tune project publishing.

2.3 Advising Emacs Functions

Org Mode publishing has many options that one can just "set and forget." That covers most of the bases, but when it doesn't then dropping down into Emacs Lisp is the way to go. A nice non-invasive way of doing that is to "advise functions:" equipping them with pre-processors, post-processors, or wrapping them in facades.

For example, in the next section of code the problem I'm solving here is this. The Facebook Comments Plugin relies on a <div> element whose data-href attribute has the URL for the page, and the output file-name is part of that. The <div> can be added with the Org Mode org-html-postamble-format variable, which renders a template. That template can inject many interesting values (e.g., the title, date, etc.), but the output file-name isn't among them so I just have to give it a little "advice" on how to do that.

;; Define advice
(defun add-org-html-format-spec (old-function &rest args)
  "Add a new format spec for the :output-file"
  (append (apply old-function args) `((?f . ,(file-name-nondirectory (plist-get info :output-file))))))
;; Add advice
(advice-add 'org-html-format-spec :around #'add-org-html-format-spec)

First, I define the add-org-html-format-spec named function that uses a macro to add a new format-spec assoc-list, from %f to the output file-name. Second, I add the new function as advice to the org-html-format-spec function.

I can then use the %f format-spec in the template for org-html-postamble-format so that it dynamically picks up the file-name. I also set the org-html-preamble-format template variable to the scriptlet supplied by the Facebook Comment Plugin, though this doesn't need customization.

Note that, while Org Mode supplies convenient keywords for many of its customization variables, it doesn't for all of them. This is why the (setq-local ...) forms appear here and elsewhere.

2.4 Customizing the Site-Map and Index

Next, I customize the behavior of the Org Mode auto-generated site-map and index. Both work well. However, there are two small problems. First, by default Org Mode wants to generate a site-map and an index for everything. I'm using the auto-generated site-map as the front-page article list and consequently want to avoid having the index, the about page, etc., be included. Second, by default Org Mode places all of the content for every site-map list entry into a single tag (an anchor, as it happens), which makes it difficult to style different components of the entry differently. The first problem can be fixed with a little more advice.

Once again, a little elisp "advice" is all that's needed: a wrapper to org-html--build-pre/postamble that omits a hard-coded list of files.

(defun skip-preamble-postamble-for-sitemap (old-function &rest args)
  "Skip preamble and postamble for sitemap"
  (let* ((info (nth 1 args))
         (output-file (file-name-nondirectory (plist-get info :output-file))))
    ;; List of files in which the preamble is omitted
    (cond ((string= output-file "index.html") "")
          ((string= output-file "theindex.html") "")
          ((string= output-file "about.html") "")
          ((string= output-file "colophon.html") "")
          (t (apply old-function args)))))
(advice-add 'org-html--build-pre/postamble :around #'skip-preamble-postamble-for-sitemap)

The solution to the second problem is a little more involved. The way that the site-map is generated is buried deep within the org-mode function org-publish-org-sitemap function and it cannot be patched up with any amount of advice. Wholesale replacement of this function is what is required. To do that, I define a new function, dav-org-publish-org-sitemap, which is a shameless copy of the stock org-publish-org-sitemap save for a few small changes. First, I bind a new variable prefix to a concatenation of the article's (formatted) date, along with a colon (:) separator. Second, I then inject that prefix into the site-map's article list here, here, and here. The result is that in the list the title is contained with an anchor tag while the date is not. This is necessary in order to style the date in monospace.

      (defun dav-org-publish-org-sitemap (project &optional sitemap-filename)
        "Create a sitemap of pages in set defined by PROJECT.
Optionally set the filename of the sitemap with SITEMAP-FILENAME.
Default for SITEMAP-FILENAME is 'sitemap.org'."
        (let* ((project-plist (cdr project))
               (dir (file-name-as-directory
                     (plist-get project-plist :base-directory)))
               (localdir (file-name-directory dir))
               (indent-str (make-string 2 ?\ ))
               (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-title (or (plist-get project-plist :sitemap-title)
                                  (concat "Sitemap for project " (car project))))
               (sitemap-style (or (plist-get project-plist :sitemap-style)
                                  'tree))
               (sitemap-sans-extension
                (plist-get project-plist :sitemap-sans-extension))
               (visiting (find-buffer-visiting sitemap-filename))
               (ifn (file-name-nondirectory sitemap-filename))
               file sitemap-buffer)
          (with-current-buffer
              (let ((org-inhibit-startup t))
                (setq sitemap-buffer
                      (or visiting (find-file sitemap-filename))))
            (erase-buffer)
            (insert (concat "#+TITLE: " sitemap-title "\n\n"))
            (while (setq file (pop files))
              (let ((fn (file-name-nondirectory file))
                    (link (file-relative-name file dir))
                    (oldlocal localdir)
                    ;; bind new variable prefix
                    (prefix (concat                                                       
                             (format-time-string org-publish-sitemap-date-format (org-publish-find-date file))
                             " : ")))
                (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))
                  (if (eq sitemap-style 'list)
                      (message "Generating list-style sitemap for %s" sitemap-title)
                    (message "Generating tree-style sitemap for %s" sitemap-title)
                    (setq localdir (concat (file-name-as-directory dir)
                                           (file-name-directory link)))
                    (unless (string= localdir oldlocal)
                      (if (string= localdir dir)
                          (setq indent-str (make-string 2 ?\ ))
                        (let ((subdirs
                               (split-string
                                (directory-file-name
                                 (file-name-directory
                                  (file-relative-name localdir dir))) "/"))
                              (subdir "")
                              (old-subdirs (split-string
                                            (file-relative-name oldlocal dir) "/")))
                          (setq indent-str (make-string 2 ?\ ))
                          (while (string= (car old-subdirs) (car subdirs))
                            (setq indent-str (concat indent-str (make-string 2 ?\ )))
                            (pop old-subdirs)
                            (pop subdirs))
                          (dolist (d subdirs)
                            (setq subdir (concat subdir d "/"))
                            (insert (concat indent-str
                                            " + "
                                            prefix  ;; insert prefix
                                            d "\n"))
                            (setq indent-str (make-string
                                              (+ (length indent-str) 2) ?\ )))))))
                  ;; This is common to 'flat and 'tree
                  (let ((entry
                         ;; Invoke new helper function
                         (dav-org-publish-format-file-entry
                          org-publish-sitemap-file-entry-format file project-plist))
                        (regexp "\\(.*\\)\\[\\([^][]+\\)\\]\\(.*\\)"))
                    (cond ((string-match-p regexp entry)
                           (string-match regexp entry)
                           (insert (concat indent-str
                                           " + " (match-string 1 entry)
                                           prefix  ;; insert prefix
                                           "[[file:" link "]["
                                           (match-string 2 entry)
                                           "]]" (match-string 3 entry) "\n")))
                          (t
                           (insert (concat indent-str
                                           " + "
                                           prefix  ;; insert prefix
                                           "[[file:" link "]["
                                           entry
                                           "]]\n"))))))))
            (save-buffer))
          (or visiting (kill-buffer sitemap-buffer))))

One more function definition is necessary in order to make this work. It turns out that the org-publish-org-sitemap function calls the org-publish-format-file-entry in order to format the site-map list entry, but that function is hard-coded to pass in the 'date' format code. I can't have that since I've already injected the date using the prefix as shown above. The solution is to define a new function dav-org-publish-format-file-entry (again, a shameless copy of the old one) that omits the date, and then invoke that one instead.

(defun dav-org-publish-format-file-entry (fmt file project-plist)
  (format-spec
   fmt
   `((?t . ,(org-publish-find-title file t))
     (?a . ,(or (plist-get project-plist :author) user-full-name)))))

The last thing to do is to configure the project to use the new site-map function, as shown below.

2.5 Customizing the Title

My final bit of advice teaches Org Mode to customize the <head><title>...</title></head> elements within an exported HTML file so that it hard-codes my name.

The Org Mode org-html--build-meta-info function is just a little too inflexible at the moment, but that's easily-solved by massaging the data it sees. The trick is to fix the title temporarily, call the target function and save its value, restore the old title*, and return the saved value so that callers are none the wiser.

*Note: This is necessary because the plist-put Emacs Lisp function is destructive, modifying the property list in place. Without restoring the old value, the modified title will appear in other places that it's not wanted.

2.6 Facebook Comments

I think using the Facebook Comments Plugin for a blog comments section is a really nice idea, and here's why.

  1. Everyone has a Facebook account.
  2. No one has the alternatives (e.g., Google+).
  3. Consequently, a unified comment system like Disqus is superfluous.
  4. Moreover, Disqus sucks.

Anyway, here's the code that sets up the Org Mode preamble/postamble templates to support the plugin.

      ;; Facebook comment plugin script
      (setq-local org-html-preamble-format
                  (quote
                   (("en" "
<div id=\"fb-root\"></div>
<script>(function(d, s, id) {
  var js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src = \"//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.7&appId=895590110546846\";
  fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>"))))

      ;; Facebook comment plugin in page footer
      (setq-local org-html-postamble-format
                  (quote
                   (("en"
                     "
<div class=\"fb-comments\" data-href=\"http://davidaventimiglia.com/%f\" data-numposts=\"5\"></div>
<div class=\"fb-like\" data-href=\"http://davidaventimiglia.com/%f\" data-layout=\"button\" data-action=\"like\" data-size=\"small\" data-show-faces=\"true\" data-share=\"true\"></div>
<script>renderMathInElement(document.body);</script>"))))

2.7 Page Header

After setting some Org Mode customizations (sadly, ones without keywords) in what is hopefully a self-explanatory way,

(setq-local org-html-mathjax-template "")
;; Add the postamble to pages
(setq-local org-html-postamble t)
;; Publish all pages, even unchanged ones
(setq-local org-publish-use-timestamps-flag nil)

I then define the masthead for the site (i.e., set the org-html-home/up-format buffer local variable).

      ;; Masthead definition
      (setq-local org-html-home/up-format "
<ul class=\"navigation\">
<li class=\"site-name\">David A. Ventimiglia</li>
<li><a accesskey=\"a\" href=\"%s\">Articles</a></li>
<li><a accesskey=\"c\" href=\"https://github.com/dventimi\">Code</a></li>
<li><a accesskey=\"p\" href=\"http://bit.ly/2b4iHyX\">Publications</a></li>
<li><a accesskey=\"A\" href=\"about.html\">About</a></li>
<li><a accesskey=\"i\" href=\"theindex.html\">Index</a></li>
</ul>")

The key is that, unless I make other allowances, the org-html-home/up-format template will be inserted as the "navigate home or up" component on every page in the site. That makes it an ideal place to build a masthead (or "navigation bar")! I model that as an unordered list because semantically that makes sense to me. The CSS that massages this into a masthead is left as an exercise for the reader.

      ;; Metadata
      (setq-local org-html-head "
<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.css\">
<script src=\"https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js\"></script>
<script src=\"https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js\"></script>
<link rel=\"stylesheet\" type=\"text/css\" href=\"stylesheets/base.css\" />
<meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\" />
<style>@import 'https://fonts.googleapis.com/css?family=Quattrocento';</style>
<link href=\"images/favicon.ico\" rel=\"icon\" type=\"image/x-icon\" />")

2.8 Project components

All that's left is actually to set the key org-publish-project-alist variable in order to teach Org Mode about this project. This contains both a root-level module ("blog") and leaf-level sub-modules ("articles" and "about") each of which can be tailored individually. Fortunately, all of the hard parts are done, and so the rest is just setting Org Mode provided keywords.

;; Project customization
(setq-local org-publish-project-alist
            `(("blog"
               :components ("articles" "about"))
              ("about"
               :base-directory ,(concat project-root "/about")
               :publishing-function org-html-publish-to-html
               :publishing-directory ,project-root
               :with-toc nil
               :html-link-up "index.html"
               :html-link-home "index.html")
              ("articles"
               :base-directory ,(concat project-root "/articles")
               :publishing-directory ,project-root
               :publishing-function org-html-publish-to-html
               :exclude "\\(^theindex\.*\\|^_.*\.*\\)"        ;; Ignore drafts
               :htmlized-source t
               :sitemap-file-entry-format "%t"
               :auto-sitemap t
               :with-tags nil
               :with-title t
               :with-toc t
               :html-link-up "index.html"
               :html-link-home "index.html"
               :sitemap-filename "index.org"
               :sitemap-title "Articles"
               :sitemap-function dav-org-publish-org-sitemap  ;; New sitemap fn
               :makeindex t
               :sitemap-sort-files anti-chronologically))))))))

Again, most of this is hopefully self-explanatory. One minor wrinkle that I rather like is that it's trivial to teach Org Mode about article "drafts": just ignore from publishing anything that begins with an underscore (_). Oh, and also skip theindex.org, since that's auto-generated.

And that's really all there is to it!

Update: 2016-11-01

I suppose I ought to supply an example of what an Org Mode article looks like in its "source" form. Here's the beginning of this very entry.