Home Projects Blog Dotfiles About Me RSS Git

On Switching to Org-Mode for my Website

When I first created this website, I built everything in raw HTML. I liked the control I had over it. It was also a real pain to update it though. Components such as the navigation bar at the top had to be copied to every HTML file along with CSS styling. This could have been made easier with JavaScript, but I consider that to be an abuse of scripting. One of the primary design decisions of this site was avoiding JavaScript whenever possible and there had to be another way.

Enter Emacs Org-Mode.

Emacs Org-Mode at its core is a very powerful note-taking software with a lot of depth that I have not come close to. What I do know is that it is quite a bit more ergonomic than Markdown for my needs which is why I prefer .org over .md whenever I have to write documentation. I've been using Emacs for a while, but hadn't really considered its utility as a highly configurable static site generator before now. Having done so though, I have to say that I really enjoy this new setup. I have set up a build script using sourcehut's build configuration that automatically compiles my .org files into .html and publishes it. The key line is

emacs --batch -f package-initialize --script ~/website/publish.el

which calls Emacs with a publishing script I made in Emacs Lisp.

This elisp script that is called is a fair bit more complicated than the .build.yml file and it definitely took me a while to get it to where I was happy with it. First, I ensure I have installed all the packages I need with use-package.

(require 'package)
(add-to-list 'package-archives '("gnu"   . "https://elpa.gnu.org/packages/"))
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
(package-initialize)

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))
(eval-and-compile
  (setq use-package-always-ensure t
        use-package-expand-minimally t))

(use-package org
  :ensure org-plus-contrib)
(use-package ox-rss
  :after org)

(require 'org)
(require 'ox-html)

use-package is maybe a little heavier than what I need for package management in a script like this, but ox-rss, the package used to generate my RSS feed was a challenge to get installed correctly. It must be installed after org is set up.

The core of publishing .org files to .html is the org-publish-project-alist variable. It allows me to define different sources for the site, each with different configuration options.

The first source contains the core of my website. Most of the pages outside of my blog appear here. These all reside in the same repository as my website build scripts. Also note the :html-preamble and :html-head fields. The ability to include my navigation bar and style-sheet in every file automatically made things so much nicer for me.

("org-core"
        :base-directory "~/website/"
        :base-extension "org"
        :publishing-directory "~/public_html/"
        :recursive t
        :publishing-function org-html-publish-to-html
        :with-toc nil
        :headline-levels 4
        :section-numbers nil
        :html-head "<link rel='stylesheet' href='/css/stylesheet.css' type='text/css'/>"
        :html-preamble "<div class='topnav'><a href='/'>Home</a><a href='/projects.html'>Projects</a><a href='/blog'>Blog</a><a href='/config.html'>Dotfiles</a><a href='/about.html'>About Me</a><a href='/rss.xml'>RSS</a></div>"
        :html-postamble nil)

Also in this repository, I have my styling and images (at this point it's literally a single .css file) which is included from the second entry.

("org-static"
         :base-directory "~/website/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
         :publishing-directory "~/public_html"
         :recursive t
         :publishing-function org-publish-attachment)

The blog generation was something of an abuse of Org-Mode's publishing features. It includes the ability to generate a sitemap automatically. I don't particularly need (or want) a true sitemap though. This entry is largely the same as my core website configuration, but I have it generate my sitemap into a file called index.org which is then compiled into HTML like every other page allowing me to have https://jjanzen.ca/blog point to a list of every page in my blog repository and have that list update automatically.

("org-blog"
        :base-directory "~/blog/"
        :base-extension "org"
        :publishing-directory "~/public_html/blog"
        :recursive t
        :publishing-function org-html-publish-to-html
        :with-toc nil
        :with-properties t
        :with-date t
        :with-timestamps nil
        :headline-levels 4
        :section-numbers nil
        :html-head "<link rel='stylesheet' href='/css/stylesheet.css' type='text/css'/>"
        :html-preamble "<div class='topnav'><a href='/'>Home</a><a href='/projects.html'>Projects</a><a href='/blog'>Blog</a><a href='/config.html'>Dotfiles</a><a href='/about.html'>About Me</a><a href='/rss.xml'>RSS</a></div>"
        :html-postamble nil
        :auto-sitemap t
        :sitemap-filename "index.org"
        :sitemap-format-entry my/org-publish-org-sitemap-format
        :sitemap-sort-files anti-chronologically
        :sitemap-title "Blog")

The default formatting for that list just shows the title, but I do think it's valuable to also list the date for blog posts. To remedy this, I define my own sitemap format function here.

(defun my/org-publish-org-sitemap-format (entry style project)
  (cond ((not (directory-name-p entry))
         (format "(%s) [[file:blog/%s][%s]]\n"
                 (format-time-string "%Y-%m-%d"
                                     (org-publish-find-date entry project))
                 entry
                 (org-publish-find-title entry project)))))

The most challenging portion to implement was the RSS feed though. This was a feature I wanted and never bothered to implement with the pure HTML setup. With the complete refactor though, I figured it would be nice to have an automatically generating RSS feed. The issue is that ox-rss, the package used for generating RSS feeds only works on single files. The blog isn't one file though so we need to combine all the files into one and abuse sitemaps once again to create an RSS feed. I found this blog post helpful in implementing the RSS feed. I ended up using the same configuration for it as this blog though because I found some functions in the first one didn't seem to work correctly anymore.

Finally, by using Org-Mode to generate the site, I was able to include by system configuration files in my site. I use Doom Emacs literate configuration to automatically generate all of my configuration files and since this is just a .org file, I can also compile that into HTML to easily display.

("org-config"
         :base-directory "~/.doom.d/"
         :base-extension "org"
         :html-head "<link rel='stylesheet' href='/css/stylesheet.css' type='text/css'/>"
         :html-preamble "<div class='topnav'><a href='/'>Home</a><a href='/projects.html'>Projects</a><a href='/blog'>Blog</a><a href='/config.html'>Dotfiles</a><a href='/about.html'>About Me</a><a href='/rss.xml'>RSS</a></div>"
         :html-postamble nil
         :publishing-directory "~/public_html"
         :publishing-function org-html-publish-to-html
         :headline-levels 4
         :auto-preamble t)

Finally, I just publish it to HTML with

(org-publish-all t)

Although this was a little bit of a challenge to setup, I appreciate that I now effectively have my own highly-configurable static-site generator and no longer have to write all of this in raw HTML. It makes for a much more comfortable experience and should be stable enough that I shouldn't have to deal with debugging weird elisp issues for a while.