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:
- I can write using tools of my choice,
- 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:
- org-mode: https://orgmode.org/
- Hugo: https://gohugo.io/
- GitHub Pages: https://pages.github.com/
- actions-hugo: https://github.com/peaceiris/actions-hugo
- github-pages-deploy-action: https://github.com/JamesIves/github-pages-deploy-action
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.