How I build this website

When I rebuilt this website recently, after many years of neglect, I decided I wanted to use Swift. There have been many versions of this website first starting with all hand-built html/css, a Perl/cgi-bin version, then I moved to a PHP-based template kind-of thing, then to XML content and a .NET JIT rendering engine, then an Objective-C rendering engine with GNUStep, and now after many years it was time to give it another go. I picked Swift for a couple of reasons, it's a very enjoyable language, it's the language I'm using most often now and it's certainly the "New Hot Thing". I had a few basic requirements:

  1. It should be a baked website, in that the HTML/CSS is generated, written to disk and served. The pages shouldn't be generated at request time.
  2. Content should be markdown-based. All my existing content is in Markdown, which was converted from a bunch of XML files long ago.
  3. There should be a flexible template rendering system

I started writing my own, and kept adding features. It was coming along nicely. But then I came across the Publish Site Generator by John Sundell, and it was exactly what I needed -- lots of configurability, templates built in Swift, and content written in Markdown.

I set it all up, designed a new layout and brought in my content. Next I wanted an automated build workflow. My ideal setup is write content on my iPad, push to git and the site should automatically rebuild and publish. To do so I decided to use GitHub actions to try out this fancy new CI technology.

The configuration is pretty straight forward. Create a directory named .github in the root of your repository. Within this, create a workflows directory, and a file named build-site.yml (or whatever name you want). The configuration below is what I'm using to build the site.

name: Build Site

    branches: [ main ]

    - cron:  '0 10,15,19,22 * * *'

    runs-on: ubuntu-latest
    timeout-minutes: 30
      image: swift:5.4
      options: --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined
      - name: Update system & install sudo
        run: apt update && apt install sudo
      - name: Update git
        run: sudo apt-get install -y software-properties-common && sudo apt-get update && sudo add-apt-repository -y ppa:git-core/ppa && sudo apt-get update && sudo apt-get install -y git
      - name: Checkout
        uses: actions/checkout@v2
      - name: Install Dependencies
        run: sudo apt-get install libsqlite3-dev
      - name: Get Current Date And Time
        run: |
          echo "CURRENTTIMESTAMP=$(date "+%Y-%m-%d_%H-%M")" >> $GITHUB_ENV
      - name: Generate blog
        run: swift run PerceiveNet

      - name: Add & Commit changes
        uses: EndBug/add-and-commit@v7
          default_author: github_actions
          message: "Site Build: ${{ env.CURRENTTIMESTAMP }}"
          add: "--all"
          tag: "Build-${{ env.CURRENTTIMESTAMP }}"
          push: true

This configures two build triggers. The first is anytime content is pushed to the branch named main, and the second is on a schedule which builds it 4 times a day. I have external content that is pulled in at build time so the pull trigger alone isn't sufficient.

The build itself creates a new ubuntu instance from the swift5.4 image. It then updates the OS, and pulls in a bunch of necessary packages. After that, it checks out my code, installs some other dependencies, and generates the blog. After this completes it commits the outputted content back to git.

Now for the step that publishes this to my webserver. Initially I had a CI step that used SFTP to push the content to my server. This took a good 15-20 minutes every run. Since it doesn't need to run often, this shouldn't matter, but I also want to keep the CI bill down, and every minute costs money. So I decided on a pull method.

Since the updated content is committed back to the git repository in the last build step, I decided to just use git for deployment. So I setup a periodic cron job to pull changes from the repository to my server. This was reasonably straight forward. My crontab for this looks like this (which pulls every 15 minutes):

0,15,30,45 * * * * cd /www/perceive-net/ && git pull origin master

I also had to change the URL for the git origin in my .git/config file in the repository to make sure it uses a personal access token to pull, since this is a private repository.

[remote "origin"]
	fetch = +refs/heads/*:refs/remotes/origin/*

Now I can add and edit content on my webite from any device that can use git. On iOS I use Working Copy to fetch the repository. It handles all of the git interaction, and exposes the repository as a source in the iOS Files app (and thereby to any other app that works with that). Works like a charm.