How did I build this blog with Org Mode?

(Note 2023-06-12: I’ve updated my config since I wrote this blog, so the output, especially the index page, will be different. I’ll update this blog post when I’m free.)

1. Motivation

As a daily user of Org Mode, I want a minimalistic blogging workflow with Org Mode, without too many overheads in hacking and maintaining and with minimal knowledge of HTML. While it’s possible to combine Org Mode with Jekyll or Hugo, it seems blogging with the built-in org-publish module to me is good enough.

2. My config file

Below, I’ll only show chunks of my blog’s config. My whole config is here. I am using Doom Emacs, so my config is actually in .doom.d/config.el.

3. How org-publish works

I started with this example Org Mode website on GitLab pages. In this example, each post is a single org file in the base directory of the website and the job of publish.el is to call org-publish, which converts the org files into HTML pages and automatically generates a sitemap (which is the index page in this example).

By looking into the details of publish.el, I found that the major part is the variable org-publish-project-alist, which basically contains all information required to build a website. In a typical config it is set like this:

(setq org-publish-project-alist
      (list
       (list "post"
             :property1 value1
             :property2 value2
             ; etc
             )
       (list "site" :components '("post"))))

I’ll go through the most useful properties in the coming sections. See this part of the Org Manual for an exhausted list of properties if you need reference later.

4. Customising my blog

I cloned the repo above and successfully built it. Yay! This is essentially what I want — an index page with links to all posts, and each post as a single HTML page. Then, the next thing is to customise it mostly by changing property values in org-publish-project-alist.

4.1. Repo structure

I want to keep the original org files, published HTML files and other files (e.g. images) inside a single repo. So the basic structure of my repo looks like this:

/home/yu/blog
├── misc # other files (e.g. images)
├── post # org files (what I write)
└── public # html files (what people see)

Accordingly, my config should look like this:

(setq org-publish-project-alist
      (list (list "post"
                  ;; ... other properties
                  :base-directory "~/blog/post"
                  :publishing-directory "~/blog/public"
                  ;; ...
                  )))

4.1.1. Tags or subfolders?

Ideally, I’d like my blog to support a tagging system, so each post can be assigned multiple tags, and visitors can view contents by various tags. This is seen in this example, but it’s not simple. The benefit of tags will only emerge with more posts, so I will implement it only when it becomes necessary.

Alternatively, for now, I use subfolders to categorise my posts. For example, now I have two categories of posts.

/home/yu/blog/post
├── about
└── emacs

Accordingly, I have the following settings:

(setq org-publish-project-alist
      (list (list "post"
                  ;; ... other properties
                  :recursive t
                  :sitemap-style 'tree
                  ;; ...
                  )))

such that the public folder mirrors the structure of the post folder:

/home/yu/blog
├── misc
├── post
│   ├── about
│   └── emacs
└── public
    ├── about
    └── emacs

and the homepage shows the tree structure of my blog.

4.2. Auto sitemap

Whenever I publish a new post, I want the sitemap to be automatically updated, with my name as the sitemap’s title. This can be set by the following properties:

(setq org-publish-project-alist
      (list (list "post"
                  ;; ... other properties
                  :auto-sitemap t
                  :sitemap-filename "index.org"
                  :sitemap-title "Yu Huo"
                  :sitemap-format-entry #'my-format-entry
                  ;; ...
                  )))

The fourth property above is a customised format of each sitemap entry, which allows me to show the date of each post on the sitemap. I tried to set the :sitemap-file-entry-format property to "%d *%t*" as a simple solution but it does not seem to work, so I adapted a sitemap entry format from Ravi Sagar to a tree-style sitemap and capitalised titles.

Simply add this function to config:

(defun my-format-entry (entry style project)
  (if (file-directory-p (org-publish--expand-file-name entry project))
      (format "%s" (capitalize (substring entry 0 -1)))
    (format "[[file:%s][%s]] --- %s"
            entry
            (org-publish-find-title entry project)
            (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)))))

4.3. Styling

By default, the styling of exported HTML pages is minimal. I want my blog to look more attractive and include necessary information/links at the top and bottom of the page.

4.3.1. CSS

CSS controls the looking of the website, whilst being agnostic of its content. I found a minimal CSS setting, Simple.css from System Crafters’ post (which is amazing). To use it, I include the following line in my config:

(setq org-html-head "<link href=\"https://cdn.simplecss.org/simple.min.css\" rel=\"stylesheet\" type=\"text/css\" />")

Another CSS setting that is worth mentioning is org-notes-style.

4.3.2. Preamble

I want to keep preambles as simple as possible — that is, just a link back to my homepage:

(setq org-publish-project-alist
      (list (list "post"
                  ;; ... other properties
                  :html-link-up "/"
                  :html-link-home "/"
                  ;; ...
                  )))

For some reason, the “UP” and “HOME” links always appear together, so I set both of them to point to homepage.

4.3.3. Postamble

I adapted a postamble from Ravi Sagar:

(defvar my-html-blog-postamble
  "<div class='footer'> © Yu Huo 2022. Created %d, Last updated %C, built with %c</div>")

(setq org-publish-project-alist
      (list (list "post"
                  ;; ... other properties
                  :html-postamble my-html-blog-postamble
                  ;; ...
                  )))

5. Testing

After all the settings above, I reload My Emacs configurations to include the new org-publish settings. Then we can start testing!

5.1. Making a test page

I extensively use Org Mode’s markup for rich contents, for example, bold, italic and embedded LaTeX. To see if those markups can be properly exported, I set up a test page for markups that I use most.

5.2. Hosting locally

I’d like to first locally preview the outcome. First, I export the files with M-x org-publish, and choose site, and the output HTML will go to the public folder. Instead of directly opening the exported HTML file, I use simple-httpd to locally host the website in Emacs, as recommended by System Crafters. After installing Simple Httpd, serve the ~/blog folder with M-x httpd-serve-directory, and visit http://0.0.0.0:8080/public/ to see the result.

5.3. Forcing sitemap to update

One issue that I encountered while testing is that the sitemap won’t automatically update after I change my post’s title and re-build. I realised that it’s an issue with org-publish caching the post’s title in org-publish-cache. Following this solution, I cleaned the cache and rebuild the blog by running:

(org-publish-remove-all-timestamps)
(org-publish "site" t)

This is probably a feature to speed up sitemap generation.

6. Hosting on Github Pages

With my blog pretty much behaving as desired, it’s time to host it somewhere and start writing! Again to minimise overheads, I chose to host my blog on Github Pages.

6.1. Dummy index.html file

Github Pages requires an index.html file at the root of my repo, so following this solution I created a dummy html file.

7. Further reading