31.1 Deploying to Netlify: netlify.toml, Build Settings, and Environment Variables
Alright, let’s get your Hugo site live on Netlify. This is arguably the easiest and most pleasant deployment experience you’ll have, which is why we’re starting here. Netlify’s founders basically looked at the modern web dev workflow, said “this is absurdly painful,” and built a product that does the hard parts for you. We love to see it.
The core of this magic is a single file: netlify.toml. This is your deployment configuration, your build command, your redirect rules—your everything. It lives in the root of your repository, and Netlify will automatically find it and obey its every command. You can configure all this through Netlify’s web UI, but that’s a sucker’s game. It’s clicky, it’s fragile, and it’s completely disconnected from your codebase. The file is the source of truth. Always.
Your netlify.toml File, Demystified
This isn’t just a config file; it’s a declaration of intent. Here’s a robust starting point that handles 95% of Hugo sites:
[build]
publish = "public"
command = "hugo --gc --minify"
[build.environment]
HUGO_VERSION = "0.128.0"
HUGO_ENABLEGITINFO = "true"
NODE_ENV = "production"
[context.production.environment]
HUGO_ENV = "production"
[context.deploy-preview.environment]
HUGO_ENV = "development"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Let’s break it down. The [build] section tells Netlify what command to run (hugo --gc --minify to build and clean up garbage and minify output) and where to find the built static files (public/). The [build.environment] sets variables for the build process itself. This is crucial: here we pin the exact version of Hugo. Netlify’s default version can be wildly out of date, and you do not want your site building with Hugo v0.54 when you developed on v0.128. This is the most common “it works on my machine” pitfall, solved.
The [context.*] blocks are brilliantly useful. The production context sets an environment variable when deploying from your main branch. The deploy-preview context sets variables for every Pull Request preview Netlify automatically builds for you. I use HUGO_ENV to control things like enabling Disqus comments only in production builds, not in every PR preview.
The Environment Variables Tango
Speaking of environment variables, this is how you handle secrets and configuration that shouldn’t be hardcoded into your repo. Your Google Analytics tag, API keys for headless CMS previews, etc.
In your netlify.toml, you define them for the build:
[build.environment]
MY_SECRET_API_KEY = "my-secret-value"
But wait! You wouldn’t put a real secret value in there, would you? Of course not. That file is in git. For actual secrets, you define them in the Netlify UI (Site settings > Environment variables). The UI-set variables take precedence over the ones in the toml file. So the pattern is: set non-secret defaults in the toml, and override them with secrets in the UI.
You can then access these in your Hugo templates with getenv:
{{ with getenv "MY_SECRET_API_KEY" }}
<script>console.log('The key is: {{ . }}')</script>
{{ end }}
Build Settings and the Quirks of Hugo
Now, a word on build settings. If you have a slightly non-standard setup—say, your config.toml is in a subdirectory or you’re using a non-standard theme—you might be tempted to use the “Base directory” or “Build command” fields in the Netlify UI. Don’t. Fight that urge. Configure it all in your netlify.toml instead. It’s more portable and version-controlled.
For example, if your config file is config/production/config.toml:
[build]
command = "hugo --gc --minify --configDir config/production"
Another common head-scratcher: if you’re using Hugo Modules, your build might need the GO_VERSION environment variable set. Netlify’s Go environment can be a bit particular.
[build.environment]
GO_VERSION = "1.21"
Push your code, connect your repo in the Netlify UI, and it will pick up the netlify.toml and just work. The first time you see it automatically build a deploy preview for a pull request, you’ll feel a little bit of that web dev frustration melt away. I promise.