Hugo is the world's fastest static site generator. Here's why it's built in Go and how to test your Hugo themes.
· 4 min read

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.