Paul Fawkesley

Adding a minimal Go backend to my Hugo static site

Go code showing an HTTP handler returning the result of a function called random email

This site is built with Hugo, a static site generator. The site lives on a tiny VM as a bunch of HTML files. I use nginx with Let’s Encrypt to serve the directory of files.

The site doesn’t have a backend: there’s no server-side rendering and no API calls. Any dynamic functionality must be implemented with Javascript.

This has served me well for a decade. But recently I’ve wanted to try out a few ideas where a backend would be helpful.

I had a spare hour and I wanted to throw up a minimal backend written in Golang. Speed and simplicity were the most important thing.

Here’s how it works

Idea: randomly generated contact email address

I’m minorly tired of all the spam I get to the email address I used to publish on the site’s contact page.

I thought it would be fun to try two things:

  1. hide the email address until the visitor clicks “reveal”
  2. generate a random email address for every site visitor (and log their IP and user agent)

I hope that 1. will thwart basic scrapers and bots. I’m assuming most bots won’t click “reveal”.

When I do get a spam email, I can look up the random email address in my logs and find the IP address that scraped the page. Then I’ve got the option to send an abuse report to the administrator of the IP address.

Frontend code to reveal the email address

This year I keep coming across HTMX. I love its simplicity and the associated book, Hypermedia Systems is an interesting technical read.

With HTMX, the frontend code is extremely simple:

<a hx-get="/email" hx-trigger="click" hx-swap="outerHTML">
Click to reveal email address
</a>

Translated, that means, “when the <a> tag is clicked, fetch /email and replace the <a> tag with the result.”

Here’s how that renders:

Click to reveal email address

Go server to generate email addresses

I wanted the email address to be consistent for a given User Agent, IP Address combination. This way, a user will normally see the same email address if they reload the page.

To achieve this, the codes takes the SHA256 hash of the User Agent and IP Address. It uses the first 8 bytes as the seed to Go’s random function. It then generates an email address with 10 randomly picked characters, e.g. hi.xgdjqkpxvr@example.com

Here’s the code:

// main.go

package main

import (
	"crypto/sha256"
	"encoding/binary"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"strings"
	"time"
)

func main() {
	http.HandleFunc("/email", func(w http.ResponseWriter, r *http.Request) {
		ip := r.Header.Get("x-forwarded-for")
		ua := r.Header.Get("user-agent")
		eml := randomEmail(ip, ua)

		fmt.Printf("%s|%s|%s|%q\n", time.Now().Format(time.RFC3339), eml, ip, ua) // log to stdout
		fmt.Fprintf(w, "<a href=\"mailto:%s\">%s</a>", eml, eml)
	})

	log.Fatal(http.ListenAndServe(":8081", nil))
}

func randomEmail(ip, userAgent string) string {
	hasher := sha256.New()
	hasher.Write([]byte(ip))
	hasher.Write([]byte(userAgent))

	sha := hasher.Sum(nil)

	fmt.Sprintf("hash: %s\n", string(sha))

	seed := int64(binary.BigEndian.Uint64(sha))
	rand.Seed(seed)

	chars := make([]string, 10)

	for i := 0; i < 10; i++ {
		randi := rand.Intn(len(letters))
		chars[i] = letters[randi : randi+1]
	}

	return fmt.Sprintf("hi.%s@example.com", strings.Join(chars, ""))
}

var letters = "abcdefghjkmnpqrstuvwxyz23456789"

Building and deploying with make and scp

I used a simple Makefile to build and deploy the Go code:

# Makefile

randomemail: main.go
	go build -o randomemail main.go

.PHONY: run
run: main.go
	go run main.go

.PHONY: deploy
deploy: deploy_binary deploy_supervisor_config

.PHONY: deploy_binary
deploy_binary: randomemail
	scp randomemail paul.fawkesley.com:/opt/randomemail/randomemail.new
	ssh paul.fawkesley.com 'mv /opt/randomemail/randomemail.new /opt/randomemail/randomemail && sudo supervisorctl restart randomemail'

.PHONY: deploy_supervisor_config
deploy_supervisor_config
	scp config/etc/supervisor/conf.d/randomemail.conf paul.fawkesley.com:/etc/supervisor/conf.d/randomemail.conf
	ssh paul.fawkesley.com 'sudo supervisorctl reload'

Running the server with Supervisor

I used supervisor to start the server and keep it running if it crashes.

This took a single config file:

# /etc/supervisor/conf.d/randomemail

[program:randomemail]
directory=/usr/local
command=/opt/randomemail/randomemail
autostart=true
autorestart=true
stderr_logfile=/var/log/randomemail.err
stdout_logfile=/var/log/randomemail.log

Reverse proxying the server from nginx

Requests to /email need to be directed to the backend rather than from static files.

I added a location rule to the server section of my nginx config:

# /etc/nginx/sites-available/paul.fawkesley.com_HTTPS

server {
    server_name paul.fawkesley.com;

    ...

    location /email {
        # forward to the `randomemail` service located at /opt/randomemail/randomemail
        proxy_pass http://localhost:8081;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

And that’s it… have a play and let me know what you think!

Click to reveal email address


Thoughts? Get in touch