Right, the params section. This is where Hugo, in a moment of clarity, gives you a key to a locked drawer in its brain. You can put whatever you want in that drawer—a string, a number, a list of your favorite 80s action heroes—and then later, in your templates, you can pull it out and use it. It’s the primary way you pass custom configuration from your central hugo.toml file directly into your templates to control how they look and behave.

Think of it as your site’s control panel. The stuff in the [params] table isn’t for Hugo’s core engine; it’s for your logic. Want a custom copyright message that changes based on the environment? params. Need to toggle a “beta feature” banner on and off? params. Want to define the default image for social media sharing? You get the idea.

Here’s the basic syntax. You declare everything under the [params] key in your hugo.toml:

baseURL = "https://example.com/"
languageCode = "en-us"
title = "My Incredible Website"

[params]
brandColor = "#ff2d20" # A simple string
copyright = "© 2023 Me, Myself & I" # Another string
enable_teams_feature = false # A boolean! Very useful.
favorite_bands = ["Devo", "Talking Heads", "The B-52's"] # An array

Accessing params in templates

This is the whole point. Accessing these values in your templates is straightforward, thanks to the .Site.Params object. It’s your best friend. Need that brand color in a CSS style tag? No problem.

<html>
<head>
  <style>
    .brand { color: {{ .Site.Params.brandColor }}; }
  </style>
</head>
</html>

Want to conditionally show a beta feature based on that boolean? Easy.

{{ if .Site.Params.enable_teams_feature }}
  <section class="teams-beta">
    <h2>Check out our new Teams feature!</h2>
  </section>
{{ end }}

And you can iterate through that array like any other.

<ul>
{{ range .Site.Params.favorite_bands }}
  <li>{{ . }}</li>
{{ end }}
</ul>

Organizing with nested tables (a.k.a. dictionaries)

Don’t just dump everything at the top level like a savage. When you have more than a handful of params, especially for a larger project, you need to get organized. TOML’s nested tables are perfect for this. Let’s say you have params for your social media links and your site’s footer. Group them!

[params]
brandColor = "#ff2d20"

[params.social]
twitter = "https://twitter.com/yourhandle"
github = "https://github.com/yourhandle"

[params.footer]
show_newsletter_signup = true
show_recent_posts = 5

Accessing these is just a matter of continuing the dot-notation chain in your template:

<a href="{{ .Site.Params.social.twitter }}">Follow me on Twitter</a>

{{ if .Site.Params.footer.show_newsletter_signup }}
  <!-- Newsletter signup form HTML here -->
{{ end }}

This isn’t just tidy; it’s a lifesaver when you’re trying to find a specific parameter six months later. Trust me.

The critical gotcha: page vs. site params

Here’s where everyone trips up, and it’s Hugo’s fault for making the naming ambiguous.

  • .Site.Params refers to the stuff you defined in hugo.toml. Global, site-wide settings. This is what we’ve been talking about.
  • .Params (without the Site) refers to page-level parameters, typically defined in a content file’s front matter.

They are completely different namespaces. If you try to access {{ .Params.brandColor }} in a template for a regular page, you’ll get nothing, because that variable doesn’t exist in the page’s front matter—it lives up in the site config. Mixing these up is the number one cause of templates that render nothing and leave you staring at the screen wondering if you’ve forgotten how to type. I’ve done it. You’ll do it. Now you know why it happens.

Using params with Hugo’s environment system

This is where params becomes genuinely powerful. Remember the HUGO_ENV environment variable we set? You can use it to have environment-specific parameters. The most common use case is for analytics or tracking scripts. You don’t want your development Google Analytics tag sending a bunch of nonsense pageview data from your localhost to your production account.

You could use a conditional in your template:

{{ if eq hugo.Environment "production" }}
  <!-- Insert very real, very serious production analytics script -->
{{ else }}
  <!-- Insert a fake script that just logs to console -->
{{ end }}

But that gets messy. A cleaner way is to define the script itself as a param that changes with the environment. You can use Hugo’s config directory feature or environment-based config files to override values. For instance, you could have a config file config/production/hugo.toml that contains only this:

[params]
google_analytics_id = "UA-XXXXX-1"

And in your default hugo.toml, you set it to something empty or fake:

[params]
google_analytics_id = "GTM-DEV"

Now, your template is clean, robust, and environment-aware:

<script src="https://www.googletagmanager.com/gtag/js?id={{ .Site.Params.google_analytics_id }}"></script>

When you run hugo server, it uses the “dev” ID. When you build with HUGO_ENV=production hugo, it swaps in the real one. Elegant, maintainable, and you didn’t have to litter your templates with ugly if statements. This is the way.