Adding a minimal Go backend to my Hugo static site
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:
- hide the email address until the visitor clicks “reveal”
- 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:
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!