Overwrite Hugo Config nested values
Table of Contents
Quick note: If you landed here from a search engine and just want the answer, skip the troubleshooting article. The solution is here: prefix your environment variable with HUGO_
, then match the config key path using underscores.
What I want to do
Last week, I decided to add analytics to this blog. Nothing too fancy, just a way to track when my site is visited and which posts are being read.
Thankfully, Hugo makes this incredibly easy, assuming your template supports it (which it probably does, since Hugo has embedded templates specifically for this). All you need is to add your Google Analytics token to your config.toml
file, like so:
[services]
[services.googleAnalytics]
id = ''
Just drop your Google Analytics token into the id
field, and your theme should start embedding the tracker on every page. Hugo even provides a few options under a [privacy]
section to control how the tracker behaves:
[privacy]
[privacy.googleAnalytics]
disable = false
respectDoNotTrack = false
These options can go right alongside your [services]
block. Interestingly, “respectDoNotTrack” is disabled by default. Because of course it is, given the state of the internet these days.
Setting that aside, the setup is refreshingly simple. But there’s a catch: this requires placing your token directly into your site’s config. In my case, that’s a problem, since this blog is open-source.
Now, this particular token isn’t as sensitive as, say, an API key:
- Anyone can find it by inspecting the site’s source code once it’s deployed.
- Google Analytics can be configured to ignore hits from other sites, making it useless to anyone else.
Still, I’d rather not have any kind of token sitting in plain text. Most of the tools I’ve worked with allow injecting secrets during the CI/CD pipeline, and I’d like to keep following that standard. Even if I’m in the minority because, apparently, exposing API keys is a daily occurrence on GitHub.
So, ignoring the abysmal default privacy setting and the security apathy of the masses, I pressed on.
Figuring Out How to Do It
Next step: see if I could reference environment variables from config.toml
instead of hardcoding them. I headed over to Hugo’s documentation, dove into the configuration section, and… nothing. No mention of referencing environment variables, and no hint about how to set nested values dynamically.
I checked the services documentation and found how to read the values inside templates:
{{ .Site.Config.Services.GoogleAnalytics.ID }}
But that’s not helpful here. I wanted to set the value from outside the file.
The Google-Fu Phase
I turned to search engines. Hugo has thousands of documentation pages, and I didn’t feel like skimming through all of them manually. Plus the search option obfuscated what I was looking for because there are multiple parts of Hugo that you can call environment variables from.
- The first link covered
os.Getenv
, but that’s for use inside HTML templates. - The second link explained how to use environment variables in Markdown.
- The third link was close but ultimately didn’t work when I tried it on my current Hugo build.
AI research
To cover all my bases, I turned to AI just to see if anything could shortcut the process.
To their credit, the AI tools at least understood what I was trying to accomplish: referencing environment variables inside my config.toml
. The responses were close. But ultimately, they were all hallucinations.
AI Chat Model | Response |
---|---|
Cursor.AI | id = '{{ getenv "HUGO_GOOGLE_ANALYTICS_ID" }}' |
Meta-LLaMA 3 70B | id = "$GOOGLE_ANALYTICS_API_KEY" with a [security.funcs] section to whitelist it |
ChatGPT | Set HUGO_GOOGLEANALYTICS=xxxx in your environment |
Each model pulled in correct information, but from the wrong parts of Hugo’s system. Most of the sources they cited were about using environment variables in templates or markdown, not configuration files. From there, they adjusted and guessed what the syntax should be for config.toml
, based on the patterns they’d seen elsewhere.
So the answers were close. But close isn’t acceptable for code. In the end, it wasn’t a knowledge issue so much as a misapplied pattern; AI trying to fill in the blanks with best guesses from incomplete context.
Finally, the Answer
Eventually, I found the missing piece. Not in the configuration settings list, but on Hugo’s configuration introduction page:
Environment variables take precedence over values set in the configuration file.
You can also configure settings using OS environment variables:
export HUGO_BASEURL=https://example.org/
export HUGO_ENABLEGITINFO=true
export HUGO_ENVIRONMENT=staging
hugo
So yes, Hugo does support environment variables. You just need to prefix them with HUGO_
, which, to be fair, we already knew from the Google-fu research phase. And yet… when I tried using HUGO_GOOGLEANALYTICS
, it didn’t work.
Why not?
Turns out, it used to work. The answer was correct at the time it was given.
Why the Earlier Advice Failed
Back in the day, you could set HUGO_GOOGLEANALYTICS
and it would work without issue. But starting with version v0.120.0, Hugo changed the structure to have an overarching services. Google Analytics was moved from a top-level config value to a nested [services]
block along with other settings.
The old documentation, as seen on the Wayback Machine, explained how environment variables could overwrite top-level config values. That information is still accurate. It’s just no longer located where most of the configuration settings are. Instead, it was relocated to the configuration introduction section. Though it doesn’t clarify exactly how to handle nested values, which probably is what tripped up the AI Chat models.
So what’s the current answer?
export HUGO_SERVICES_GOOGLEANALYTICS_ID=G-XXXXXXXXXX
It turns out Hugo uses the naming convention you’d expect from object paths! Just flattened with underscores. For example, .services.googleAnalytics.id
becomes HUGO_SERVICES_GOOGLEANALYTICS_ID
.
Sure enough, that worked! Both locally and in my Netlify CI/CD pipeline.
Wrapping Up
So in the end, I did manage to set my Google Analytics ID securely via environment variable. But it took longer than expected due to:
- Changed config structure between versions
- Incomplete or scattered documentation
- AI confidently hallucinating outdated solutions
- I’m impatient and didn’t just finish reading documentation
The silver lining? I got a deeper understanding of Hugo’s config system and a reminder that when all else fails, read the full manual, especially the intro sections.
Next time, I’ll probably start there first.
I’m also leaning toward dropping Google Analytics entirely. Now that I’m more comfortable with Hugo’s internal templates and config references, I’d rather not contribute to the slow erosion of privacy on the web. Besides, self-hosting a new system, such as Plausible sounds like a fun side project.