After reviewing a list of org-mode1 capable static website generators2, I decided to see if org-static-blog3 could suffice my simple needs. My criteria for choosing an org-mode static site generator was:

  • it must be actively maintained,

  • it must be simple to set up with customizations,

  • and it must work with Emacs 26 and later.

    This ruled out quite a few right away. I didn’t attempt using org-publish, as it looked like a great deal of configuration to achieve a minimum viable web-page for this project.

§Configuration of org-static-blog

Following the org-static-blog README documentation, it is very straight forward to get a minimal viable website generated. I added the following to my init.el:

(add-to-list 'auto-mode-alist (cons (concat org-static-blog-posts-directory ".*\\.org\\'") 'org-static-blog-mode))
(setq org-static-blog-publish-title "blog.winny.tech")
(setq org-static-blog-publish-url "https://blog.winny.tech/")
(setq org-static-blog-publish-directory "~/projects/blog/")
(setq org-static-blog-posts-directory "~/projects/blog/posts/")
(setq org-static-blog-drafts-directory "~/projects/blog/drafts/")
(setq org-static-blog-enable-tags t)

(setq org-static-blog-page-header
<link href=\"static/style.css\" rel=\"stylesheet\" type=\"text/css\" />

I opted to setq all the configuration variables; I will likely switch to using M-x customize-group RET org-static-blog RET in the future however. It’s a better experience.

Then I simply create a new post with M-x org-static-blog-create-new-post RET <TITLE> RET, edit the buffer, save it, then run M-x org-static-blog-publish RET. I also added some styling to my style.css4 based off the Tachyons CSS framework5. I had previously used Bootstrap6 for styling but was hoping to avoid adding framework and other extra tooling that shouldn’t be necessary to generate a simple site like this one.

§Deploying with Caddy and GitHub

I am a fan of the Caddy7 web server which offers automatic HTTPS via LetsEncrypt with only a few lines of configuration. In addition Caddy has a plugin named git which offers the ability to automatically deploy content from git repositories with webhook support. To deploy the following steps are taken:

  1. M-x org-static-blog-publish RET from Emacs to regenerate the static site,

  2. commit the changes in git,

  3. and finally push the git branch to GitHub.

    After these steps, GitHub automatically sends a HTTP POST request to my Caddy server with information about the new git commits and Caddy pulls the git repository. If everything went well and the webhook successfully fired, the website is now deployed.

§Server Configuration

I switched most of my personal internet-related services to Docker8 in conjunction with docker-compose9 last year. The main rationale is I can move my configuration without dealing with systems package versions. I already had Caddy set up, so it was as simple as adding this to my Caddyfile:

blog.winny.tech {
        root /srv/www/blog.winny.tech
        log /logs/blog.winny.tech.log
        git https://github.com/winny-/blog.winny.tech {
                hook /webhook top-secret-password-redacted

The relevant lines of my docker-compose.yml looks like this:

version: "2.1"
    image: abiosoft/caddy:no-stats
    ports:  # Expose the webserver ports to the internet
      - "80:80"
      - "443:443"
      # This is where caddy places certs after ACME negotiation.
      CADDYPATH: "/etc/caddycerts"
      ACME_AGREE: "true"
      - /srv/caddy/certs:/etc/caddycerts
      - /srv/caddy/Caddyfile:/etc/Caddyfile  # Configuration
      - /srv/www:/srv/www  # the websites
      - /srv/caddy/logs:/logs

A keen docker-compose-savy reader will notice I did not specify a restart: always entry. I had Caddy configured to always restart, however, when requesting new HTTPS certificates from LetsEncrypt, there is a tendency to misconfigure the domain configuration or Caddyfile, and if Caddy requests too many HTTPS certificates in a short amount of time, LetsEcrypt will rate-limit my future requests. Usually this is only requires an hour or two of waiting, but is frustrating to deal with when trying to fix my configuration. Instead I rather Caddy exit after failing to activate all the domains and fix my configuration first.

§GitHub Configuration

Simply create the GitHub repository, then add a webhook. It is important to note the webhook must send a JSON payload. By default a newly created webhook will send a application/x-www-form-urlencoded payload and will not work.


With this simple setup I can write posts. I can easily move the configuration to a new host at will. In addition my setup does not depend on future use of GitHub as GitLab, Gogs, and other git hosts offer webhook support in the same way. Most importantly, I can author org-mode files and have a better balance between features and ease of use than what markdown offers.

Web dev is one of my least favorite programming exercises. Between all the testing necessary to ensure a simple site works across many platforms, the trend to use a very complex system such as webpack and many other tools to produce simple websites, and the perpetual flux-and-flow between vendors only partially implementing good features, web dev just doesn’t do it for me. Hence, I am very pleased how simple this project turned out to be.