4.4 Environment-Specific Config: config/_default/, config/production/
Right, let’s talk about environments. Because your local machine is a pristine, forgiving paradise, and the public internet is a chaotic, resource-starved hellscape. You and I develop in the former and deploy to the latter, and we’d be fools to use the same configuration for both. Hugo knows this, and its environment-based configuration system is one of its most brilliant, sanity-saving features. We’re going to set it up so you can swear freely in your development console without it showing up on your company’s homepage.
The magic happens in your config directory. Forget the lone hugo.toml at your project’s root for a moment; that’s just the tip of the iceberg. The real power is in the config directory structure. The standard practice is to have config/_default/ for your universal, baseline settings, and then override them in environment-specific directories like config/production/.
The Hierarchy of Configuration: It’s Overrides All the Way Down
Hugo doesn’t just pick one config file and run with it. It layers them. It’s like getting dressed: you start with your baseline outfit (_default), then maybe add a raincoat for a specific weather condition (production). The order of precedence is crucial, and it works like this, from lowest to highest priority:
config/_default/(The foundation)config/production/(Overrides the default for production)config/development/(Overrides the default for development)- …and so on for any environment you create.
Hugo merges these settings, with the more specific environment directory winning any arguments. This means you define a setting once in _default and only override the bits that need to change for a specific environment. Don’t repeat yourself. Let’s see it in action.
First, create the directory structure:
mkdir -p config/_default config/production config/development
Now, move that root-level hugo.toml file into config/_default/. It’s found its true home. Your file tree should look like this:
your-project/
├── config/
│ ├── _default/
│ │ └── hugo.toml
│ ├── production/
│ └── development/
├── content/
└── ...other directories
Setting the Stage: Define Your Environments
Hugo won’t automatically know what production means. You have to tell it which environment it’s operating in by using the --environment flag with the hugo command. This is the trigger that tells Hugo to look in the config/production/ or config/development/ directory.
- For local development: You’ll run
hugo server --environment development. This is so common you should probably alias it. - For building to deploy: You’ll run
hugo --environment production.
If you don’t specify the --environment flag, Hugo defaults to production. A sensible, if slightly terrifying, default. It assumes you’d rather accidentally build for production than accidentally show your draft content to the world.
A Practical Example: Taming Analytics and Drafts
Let’s implement the most common use case: enabling Google Analytics only in production and showing draft posts only in development.
In your config/_default/hugo.toml, you set the safe, conservative, global defaults:
baseURL = 'https://my-awesome-site.com/'
languageCode = 'en-us'
title = 'My Awesome Site'
# Disable analytics by default. We'll turn it on only in production.
disableKinds = ['RSS', 'robotsTXT', 'taxonomy', 'term']
# Don't show drafts by default. We'll override this in development.
enableRobotsTXT = true
buildDrafts = false
[params]
company = "My Awesome Co."
# Let's set a universal, slightly cheeky parameter
motto = "We're Pretty Good, Actually"
Now, in your config/production/hugo.toml, you only include the settings that need to be different for production. This file can be tiny.
# Turn ON Google Analytics for production
googleAnalytics = 'UA-XXXXXXXX-X'
# We want sitemaps and RSS feeds for real builds
disableKinds = ['taxonomy', 'term'] # Overrides the default: enables RSS & robotsTXT
[params]
# Maybe we want a more professional motto for the public?
motto = "Enterprise-Grade Solutions"
Finally, for your config/development/hugo.toml:
# Show drafts when in development mode!
buildDrafts = true
# Definitely don't run analytics on localhost
googleAnalytics = '' # Explicitly set it to empty
[params]
# Let's get silly, it's just us here.
motto = "We're Basically Just Goofing Off (Development Build)"
Why This is a Godsend and How to Screw It Up
The beauty here is isolation and safety. You can set baseURL = "http://localhost:1313" in your development config without fear of it ever being deployed. You can use a different, minimal theme for development to speed up rendering. You can enable verbose logging for debugging locally without spamming your production build logs.
The most common pitfall is duplication. The whole point is to override, not to copy-paste. Your environment-specific files should be small, containing only the deltas from the default. If you find yourself copying the entire hugo.toml into production/, you’ve missed the point entirely and are creating a maintenance nightmare.
Another edge case: Hugo’s merge is smart, but it’s not psychic. For slice or map structures within [params], the entire slice or map is replaced by the most specific config. It doesn’t merge individual elements. So if you have [params.social] in your default config, and you define [params.social] in production, the entire production definition wins. Plan your parameter structure with this in mind.
This system is Hugo refusing to be boring about a problem that plagues every web project. Use it. Your future self, who isn’t debugging why their Google Analytics is tracking localhost, will thank you.