I've written before about blog migrations. This site started out as a Wordpress blog but I migrated to static generation when it got hacked and filled with anti-semitic propaganda. I started with Blogofile in 2011, which solved my problems at the time but was a bit of a pain to work with. I migrated to Jekyll in 2012 because it was better maintained and easier to find help with. I almost migrated to Hugo when I was frustrated with the performance of Jekyll but changing all the templates was too much work and I decided to simplify the site instead, removing most of the messy features that made Jekyll slow.

This week, I've migrated everything to Eleventy. I've been thinking of using a javascript tool for a while since that's what I work with professionally all the time these days. I started by following this post by Alex Pearce and fixed a few more random issues along the way.

The main thing I had to fix for myself was the post iteration on the homepage. I wanted to show the most recent 5 posts and the syntax for that is different from Jekyll's.

{% assign latest_posts = collections.posts | reverse | slice: 0, 5 %}
{% for post in latest_posts %}
{% endfor %}

I've been impressed by how easy it was to work with my existing markdown files from Jekyll. I've also been impressed by how much faster Eleventy is than Jekyll.

For the last few years I've been using an early 2015 MacBook Pro for work and have sometimes suffered from RSI due in part to the terrible ergonomics of the Magic Keyboard. I solved this by buying a Mac-compatible mechanical keyboard from Varmilo.

Unfortunately, MacOS has never correctly detected the layout of this keyboard. It thinks it's ANSI layout (no extra key left of 'z') but it's actually ISO layout (backtick/tidle left of z). The Mac's default behaviour is to swap the section mark and plus-minus (§, ±) keys with the backtick/tilde key. It's usable but annoying. Until recently I solved this by remapping the offending keys using Karabiner Elements.

Fast-forward a few years and I've got a newer MacBook Pro, circa 2019. No matter how many times I go through the setup, Karabiner Elements always fails to install its driver on this machine. I needed a new solution. Forunately, there's a CLI for changing user keymappings on MacOS, hidutil. I found some scripts on the Apple Stack Exchange and rewrote them into a shell utility to make it easier to use.

# Adapted from https://apple.stackexchange.com/a/353941 under CC BY 4.0
set -e


case $1 in
  hidutil property --set '{"UserKeyMapping":[
      "HIDKeyboardModifierMappingSrc": 0x700000035,
      "HIDKeyboardModifierMappingDst": 0x700000064
      "HIDKeyboardModifierMappingSrc": 0x700000064,
      "HIDKeyboardModifierMappingDst": 0x700000035
  hidutil property --set '{"UserKeyMapping":[]}'
  sudo sh -c "cat > ${PLIST}" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  sudo launchctl load -w -- ${PLIST}
  sudo launchctl unload -w -- ${PLIST} && sudo rm -f -- ${PLIST}
  echo "Usage: $0 on|off|install|uninstall" >&2
  exit 1

The way this swaps keymaps, it fixes my external keyboard but also makes the same change to the MacBook's internal keyboard, making that one wrong. For that reason it's convenient to have the commands to disable the change or uninstall the PList file in case I'm traveling or my hardware changes. Thanks to the Covid situation, I haven't been in the office for a year and a half but that may eventually change.

It has been a while since I last wrote anything for this blog but I think about it occasionally. I was planning to get back into writing but the thought of Jekyll's slow build process put me off.

To address the static site build time, I got most of the way through a port to Hugo before I realised that many of Jekyll's performance problems had been solved in the last year or two. I was also concerned that I might struggle to get some of the features to work correctly, since I use an esoteric plugin to build tag index pages.

Well, the tag problem is easily solved - I have removed the plugin and the pages from the site, simplifying the build and reducing the number of things that can slow it down or break the site. This also seemed like a good opportunity to simplify article and post headers and I ended up fiddling with the colours a bit while I was playing around.

In the past, I've used Google Analytics on this site but I've never really paid any attention to the data it produces. Since the value of the data to me is almost zero, I removed it quite some time ago to keep things as simple as possible.

As for the Hugo port, I haven't decided if I want to commit to it at this stage, but at least it should be easier without the extra weirdness of the tag plugin. I'll stick with Jekyll for now, since my reasons to migrate have largely been mitigated.

I've recently been working with Azure Functions, Microsoft's functions-as-a-service (FAAS) platform. I'm already quite familiar with AWS Lambda so my perspective is coloured by that experience.

First, the good. Functions Apps are pretty flexible. You can host a single entry point per app if you want but it is often more cost-effective to host several functions within the same app and share some resources. I'm using Javascript so the whole app shares a package.json and a node_modules directory etc. The app also has its own Storage Account, which can host blobs (like S3), queues (like SQS) and tables (similar to DynamoDB but works differently) etc.. These resources are easy to use from any function within the app, making it easy to send a message down a queue to model event-driven workflows and perform multiple actions with the same files or database records.

The runtime of a function app environment is versioned separately from the language and language version. There is a list of supported languages and versions for each version of the runtime.

A Function App is connected to an App Service Plan, which defines the virtual machine billing model. There are options for consumption plans on Windows and Linux or you can reserve an auto-scaling pool of virtual machines. It's fairly flexible but quite awkward to configure for Linux. The key when using Terraform or ARM is to make sure the instance is reserved, as that seems to mean "use Linux".

The inputs and outputs of functions can be bound to HTTP requests/responses or tied directly to other Azure event sources, such as blobs, queues, tables etc. This allows you to avoid writing the code to send a message, write a row to a table or any number of other common integrations. This gives Functions a lot more flexibility than AWS Lambda functions, which only lets you bind the input event source.

The documentation for Linux apps is largely missing and often useless. You have to dig pretty deep to discover that the setting for configuring the version of NodeJS you want only works on Windows instances. There is a hidden setting called LinuxFxVersion in a different place (Site Config) that has no UI in the Azure Portal for configuring it on Linux. Fortunately Terraform has a way to address this undocumented property. It appears in ARM as well but I haven't found a way to edit it from the Azure Portal.

I've seen various weird crashes when deploying and restarting function apps. There are at least 3 different undocumented things that can go wrong, leaving only a cryptic error message. Searching for these issues usually fails to yield any relevant results. I've seen more strange errors with version 3 of the functions app runtime than with version 2.

Another frustration is the Azure CLI. The best case when deploying a function app is the message Operation failed with status: 200 Operation completed successfully. As amusing as that is, more often the tool simply fails with a HTTP 400 error: Bad Request with no explanation. Since the CLI hits the API about 8 times during deployment, there is no way to know how far you got or what state it left your app in. This completely breaks any kind of deployment automation. In production, you'll have to pay somebody to hand-hold the build and make sure changes make it out to your customers.

AWS Simple Notification Service is a great tool for broadcasting messages to a topic. Subscribers receive all messages by default, or can use a filter on the messagee's attributes to receive only a subset of the messages sent to the topic.

Message attributes have a few possible data types. Unfortunately the documentation for the Javascript SDK is pretty bad at the time of writing. It's fairly obvious how to set an attribute of type String but it says nothing about how to set an attribute of type String.Array. Fortunately, I guessed correctly when I gave it a try.

const AWS = require('aws-sdk')
const config = require('./config')

AWS.config.region = 'eu-west-1' // or your region

const sns = new aws.SNS()

const notificationGroups = [

async function sendMessage(message, errorCode) {
  const params = {
    Message: message,
    Subject: 'Something happened',
    TopicArn: config.sns.arn,
    MessageAttributes: {
      errorCode: {
        DataType: 'String',
        StringValue: `${errorCode}`
      group: {
        DataType: 'String.Array',
        StringValue: JSON.stringify(notificationGroups)

  await sns.publish(params).promise()

The trick is to call JSON.stringify(someArray) and stuff it into the StringValue key in the MessageAttribute object.