Hugo: A Go-powered static site generator worth knowing
Every Go developer ends up needing a website eventually. Docs for a library, a personal blog, a landing page for a side project. Hugo is the tool I’d point you at first. It’s genuinely quick, and it’s one of the best-known Go projects there is.
Hugo is a static site generator: it turns Markdown files into a complete website. No database, no server-side code, just HTML, CSS and JavaScript you can host anywhere.
The angle I find interesting isn’t “use Hugo because it’s written in Go”. It’s that Hugo’s templates are Go templates, which means you can test a Hugo site with plain Go.
Why Hugo chose Go
Hugo’s headline feature is speed, and that comes straight from Go. Building a site with thousands of pages takes seconds rather than minutes. The single-binary distribution matters just as much: there’s no Ruby or Node toolchain to install first. Drop the executable on any machine and it works.
A lot of Hugo sits on the standard library. Template rendering is html/template. The development server is net/http. File watching comes from fsnotify, one of the places it does reach for a third-party package. It’s a good reminder of how far the standard library alone will take you.
If you’ve not spent much time with Go’s templating, the text/template package documentation is worth an afternoon.
Testing Hugo templates
Hugo templates are Go templates, which means you can test pieces of them with the same tooling you’d use for any Go code.
Here’s a unit test for a template fragment:
package main
import (
"bytes"
"html/template"
"testing"
)
func TestTitleTemplate(t *testing.T) {
tmpl := `<h1>{{ .Title }}</h1>`
parsed, err := template.New("title").Parse(tmpl)
if err != nil {
t.Fatalf("failed to parse template: %v", err)
}
data := struct {
Title string
}{
Title: "Hello, Hugo!",
}
var buf bytes.Buffer
if err := parsed.Execute(&buf, data); err != nil {
t.Fatalf("failed to execute template: %v", err)
}
expected := "<h1>Hello, Hugo!</h1>"
if buf.String() != expected {
t.Errorf("got %q, want %q", buf.String(), expected)
}
}
This works well for anything self-contained: a date formatter, a title tag, a partial. Once Hugo’s full build pipeline is involved, you’ll want integration tests instead.
Integration testing with Hugo’s test server
Hugo includes a built-in server for development, and you can lean on it in tests to verify the whole site builds and serves:
package main
import (
"net/http"
"os/exec"
"testing"
"time"
)
func TestHugoSiteBuild(t *testing.T) {
// Build the site
cmd := exec.Command("hugo", "--destination", "public_test")
if err := cmd.Run(); err != nil {
t.Fatalf("hugo build failed: %v", err)
}
// Start the server
server := exec.Command("hugo", "server", "--port", "1314", "--disableLiveReload")
if err := server.Start(); err != nil {
t.Fatalf("failed to start server: %v", err)
}
defer server.Process.Kill()
// Wait for server to start
time.Sleep(2 * time.Second)
// Test the homepage
resp, err := http.Get("http://localhost:1314/")
if err != nil {
t.Fatalf("failed to fetch homepage: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("homepage returned %d, want %d", resp.StatusCode, http.StatusOK)
}
}
Tests like this catch broken links, missing templates and configuration mistakes before they reach production. It’s the same shape as testing any Go HTTP service, as covered in testing HTTP handlers.
Testing content with regular expressions
Sometimes you want to assert that specific content actually made it onto a page: a meta description, an analytics tag, an RSS link. Go’s regexp package handles this well:
package main
import (
"io"
"net/http"
"regexp"
"testing"
)
func TestMetaDescription(t *testing.T) {
resp, err := http.Get("http://localhost:1313/blog/my-post/")
if err != nil {
t.Fatalf("failed to fetch page: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read body: %v", err)
}
// Check for meta description
pattern := `<meta name="description" content=".+">`
matched, err := regexp.Match(pattern, body)
if err != nil {
t.Fatalf("regex error: %v", err)
}
if !matched {
t.Error("meta description not found")
}
}
If regex feels unfamiliar, check out the Advent of Code day 3 post where I covered Go’s regex patterns in detail.
Hugo as a documentation tool
Where Hugo really earns its keep is documentation. Plenty of open source projects use it for their docs, and the model suits technical writing well: Markdown files live in version control, and multiple authors contribute through pull requests rather than a CMS admin panel.
For a blog it hits a sweet spot too. Your content stays in plain text files. No database migrations. No security patches for PHP. Nothing to break while you’re on holiday.
Getting started
Install Hugo with a single command:
go install github.com/gohugoio/hugo@latest
Create a new site:
hugo new site my-blog
cd my-blog
hugo server
That’s it. You have a local development server running.
Wrapping up
Hugo shows off the things Go is genuinely good at: fast builds, trivial deployment, and one binary that behaves the same everywhere. Because its templates are Go templates, testing a Hugo site uses skills you already have.
If you’re weighing it up, spend an hour with it before committing. Scaffold a site, pull in a theme, and write one test like the ones above. The official documentation covers everything from basic setup to advanced templating, and Hugo’s own source is a decent repo to read if you want to see a large, long-lived Go codebase in the wild.