Right, you want to peek under the hood and maybe even tweak the engine. Good for you. Setting up Hugo’s dev environment isn’t the mystical ritual some projects make it out to be, but it does have a few quirks you need to get right, or you’ll be chasing phantom errors for hours. I’ve been there, and my goal is to make sure you aren’t.

First things first: you absolutely must use the version of Go that Hugo specifies. This isn’t a gentle suggestion; it’s the law around these parts. Hugo uses Go modules and leverages specific features of the language that can change between minor versions. Using the wrong version is the single biggest cause of “but it compiles on my machine” problems.

The One-True Go Version

Check the go.mod file at the root of the Hugo repository. The first line holds the gospel. At the time of writing, it’s probably something like go 1.21. That’s not a suggestion; it’s a mandate. Use goenv, gvm, or just download it directly, but make sure your go version matches. I use goenv because it’s trivial to switch.

# Clone the repository. You can do this anywhere.
git clone https://github.com/gohugoio/hugo.git
cd hugo

# The command to find your required Go version. Do this.
cat go.mod | grep ^go
# Output: go 1.21.3

# Now, ensure your CLI uses this exact version.
go version
# Should output something with: go version go1.21.3 linux/amd64

If that doesn’t match, stop and fix it. I’ll wait. Trust me, debugging a failed build because of a version mismatch is more boring than watching a static site generate.

Building the Binary

Hugo uses a massive, and I mean massive, number of dependencies. This is where the Go module system shines. To build your own development version of the hugo binary, it’s straightforward:

# From the root of the hugo repo, just run:
go build -o hugo_dev main.go

This compiles the code and spits out a binary named hugo_dev (name it whatever you want, I like to distinguish it from my system-wide install). You can now run ./hugo_dev in any directory just like the regular hugo command. This binary is your new best friend; it’s the key to testing your changes.

Pro Tip: Don’t use go run main.go. It’s slow for initial builds and doesn’t handle the CLI flags correctly for all cases. Always build a binary.

The Golden Path: Testing Your Changes

You’ve changed some code. Now what? You can’t just build and assume it works. You need tests. Hugo’s test suite is extensive, which is both a blessing and a curse.

# Run the entire test suite (grab a coffee, maybe a vacation)
go test ./...

# Run tests for a specific module (this is your real workflow)
go test ./resources/... -v

# Run a single specific test function (your best friend for rapid iteration)
go test -run TestFoo ./resources/...

The test suite is your safety net. The sheer scope of Hugo means a change in one place can break something in a seemingly unrelated module. Running the relevant tests is non-negotiable.

The Live Development Server

This is the fun part. You’re almost certainly making changes to see them reflected in a real site. Here’s the workflow:

  1. Make your code changes in the Hugo source.
  2. Rebuild your dev binary: go build -o hugo_dev main.go
  3. cd to your test site directory.
  4. Run your dev server: path/to/your/hugo_dev server -D

Now you can see your changes in action. The live reload will still work. This is the tightest feedback loop you can get.

The Gotchas: Dependencies and the tools.go File

Now for the weird bit. Hugo uses some non-Go tools, like the CSS preprocessor for the docs site. How does it manage those? With a brilliant hack called tools.go.

Look in the /docs directory. You’ll see a tools.go file. Its sole purpose is to import tool dependencies so go mod will recognize and vendor them. This means if you want to work on the documentation site itself, you need to install these tools:

# From the /docs directory
go install -tags extended github.com/gohugoio/hugo
go run install.go

The first command builds Hugo with the extended Sass/SCSS features (required for the docs), and the second runs a script that uses that Hugo binary to install the needed Node.js tools. It’s a bit meta, but it works. If you’re not touching the docs, you can mostly ignore this, but if you see bizarre import errors related to docs, this is why.