Writing a Blog with Org-mode

I’ve always thought I should write a blog, but I just never got around to setting it up. I know there are services you can simply sign up to and start writing, but that isn’t for me. I have two requirements for this thing:

  1. I can write using tools of my choice,
  2. I can host it anywhere.

My tool of choice for writing anything is emacs and, for natural language in particular, org-mode. This is a bit like markdown, but better. For version control and deployment I use git.

I also want to be able to host it anywhere because I don’t want to be tied to a host and, ideally, I don’t want to pay for it either. Back in the day it was common to use a dynamic site for a blog. Your content would live in a database and was served up using some backend process like WordPress. But that’s too expensive and places too many requirements on the host.

With that in mind, I’ve decided to use a static site generator. This is ideal as it means I don’t have to write raw HTML myself (although you can) but the output can be hosted anywhere. I’ve decided to use Hugo simply because it looks good, seems fast, well maintained, supports the workflow I want and, most importantly, supports org-mode.

Using org-mode with Hugo

First of all, you set up your Hugo project by following the quickstart guide.

The next thing I did was install the PaperMod theme, as it seems like a decent default for a blog.

Now, to start a new page using org-mode, you first need to install an archetype. These are essentially templates that Hugo uses to start new content. By default it comes with a markdown archetype in archetypes/default.md. You should add the following code in archetypes/default.org:

1#+TITLE: {{ replace .Name "-" " " | title }}
2#+DATE: {{ .Date }}
3#+DRAFT: true
4#+DESCRIPTION:
5#+CATEGORIES[]:
6#+TAGS[]:
7#+KEYWORDS[]:
8#+SLUG:
9#+SUMMARY:

Now you can start a new org-mode post by running: hugo new posts/my-org-post.org. You’ll find your org-mode file ready to edit in content/posts/my-org-post.org. The metadata is pretty self-explanatory, but you can just play around with it.

Deploying with Github Actions

First of all, before you build or commit anything, add a .gitignore file:

/.hugo_build.lock
/public/*
!/public/.nojekyll

This will ensure you don’t accidentally commit your locally built version of the site.

You should also add the .nojekyll file to stop GitHub trying to run Jekyll (another static site generator) on your stuff. I’m not sure if this is still necessary but it can’t harm:

1mkdir -p public
2touch public/.nojekyll

Now commit the .gitignore and .nojekyll files.

To publish your site you simply run hugo. This builds the site, including all articles that are not marked as draft, and puts it all into the /public/ directory. Now, you could simply copy the contents of that directory to a web server of your choice. That’s how we did it back in the day. This is how it meets my “can host anywhere” requirement.

But I’m lazy and I want it to be easier. I just want the site to build and deploy when I push my changes to git. This is actually remarkably simple to achieve with modern CI tooling such as GitHub Actions. Although, note: I won’t be tied to GitHub or GitHub Actions in any meaningful way, it’s essentially a glorified copy at the end of the day and I can always build my site on my own computer and copy the output the “old-fashioned” way.

To build using GitHub simply add the following to /.github/workflows/hugo.yml:

 1name: hugo
 2
 3on:
 4  push:
 5    branches: [master]
 6
 7permissions:
 8  contents: write
 9
10jobs:
11  deploy:
12    runs-on: ubuntu-latest
13
14    steps:
15      - name: Checkout
16        uses: actions/checkout@v3
17        with:
18          submodules: true
19
20      - name: Setup Hugo
21        uses: peaceiris/actions-hugo@v2
22        with:
23          hugo-version: '0.115.2'
24          extended: true
25
26      - name: Build
27        run: hugo --minify
28
29      - name: Deploy
30        uses: JamesIves/github-pages-deploy-action@v4
31        with:
32          branch: gh-pages
33          folder: public

This pipeline is triggered by pushes to the master branch. It checks out the code, sets up Hugo with the same version that I used locally, builds using --minify (I don’t like minified pages generally, but the source is available freely so might as well save bandwidth) and deploys it to the gh-pages branch. Note that the source will live on the master branch (or any other branch), the built version will end up on the gh-pages branch, which will then be deployed to Github Pages itself.

Conclusion

This should be everything needed to get started writing a blog (or any static site) with Hugo and hosting it on Github. If you are reading this then I guess it worked!

Links to the tools in use:

Addendum

Now that I’ve written a few posts I’ve found the built-in org support of Hugo pretty limiting. It doesn’t have first-class support like Markdown does. Thankfully there is the ox-hugo package which can export org-mode files to Markdown, before being read by Hugo.

The layout for the project is a bit different as it leverages org-mode to handle tags and categories in a nicer way, but it’s mostly the same (I didn’t really have to convert my existing posts, but I did anyway). The main difference is in how the project is built. The GitHub Actions pipeline contains one new entry to set up Emacs:

 1name: deploy
 2
 3on: push
 4
 5permissions:
 6  contents: write
 7
 8jobs:
 9  deploy:
10    runs-on: ubuntu-latest
11
12    steps:
13      - name: Checkout
14        uses: actions/checkout@v3
15        with:
16          submodules: true
17
18      - name: Setup Emacs
19        uses: purcell/setup-emacs@master
20        with:
21          version: 29.1
22
23      - name: Setup Hugo
24        uses: peaceiris/actions-hugo@v2
25        with:
26          hugo-version: '0.118.2'
27          extended: true
28
29      - name: Build
30        run: make
31
32      - name: Deploy
33        uses: JamesIves/github-pages-deploy-action@v4
34        with:
35          branch: gh-pages
36          folder: public
37        if: github.ref == 'refs/heads/master'

The build step is now container within a Makefile and looks like this:

1build:
2        cd content-org && emacs --batch -Q --load ../publish.el --funcall gpk-publish-all
3        hugo --minify

This runs Emacs in batch mode. The file publish.el contains settings and functions necessary for running ox-hugo:

 1;;; publish.el --- publish org-mode blog                     -*- lexical-binding: t; -*-
 2;;; Commentary:
 3;;; original influence: https://github.com/NethumL/nethuml.github.io/
 4
 5;;; Code:
 6(defconst gpk-content-files
 7  '("life.org"
 8    "networking.org"
 9    "programming.org"
10    "software.org"
11    "technology.org"
12    "thoughts.org"))
13
14;; Install packages
15(require 'package)
16(package-initialize)
17(unless package-archive-contents
18  (add-to-list 'package-archives '("nongnu" . "https://elpa.nongnu.org/nongnu/") t)
19  (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
20  (package-refresh-contents))
21(dolist (pkg '(org-contrib ox-hugo))
22  (package-install pkg))
23
24(require 'url-methods)
25(url-scheme-register-proxy "http")
26(url-scheme-register-proxy "https")
27
28(require 'org)
29(require 'ox-extra)
30(require 'ox-hugo)
31(ox-extras-activate '(ignore-headlines))
32
33(defun gpk-publish-all ()
34  "Publish all content files"
35  (message "Publishing from emacs...")
36  (dolist (file gpk-content-files)
37    (find-file file)
38    (org-hugo-export-wim-to-md t)
39    (message (format "Exported from %s" file)))
40  (message "Finished exporting to markdown"))
41
42;;; publish.el ends here

As you can see from the comment, this was “influenced” (ie. taken) from another blogger and can be found here.