Right, so you’ve picked a theme. Good for you. It probably looks… fine. Perfectly adequate. But you’re not here for ‘adequate,’ you’re here to make it yours. That’s where params.toml (or params.yaml or params.json—I don’t judge, but TOML is the default for a reason) comes in. Think of this file as the single source of truth for all the knobs, levers, and slightly confusing dials your theme exposes for you to tweak without having to write a single line of CSS. It’s the control panel.

The genius—and the occasional frustration—of this system is that Hugo’s themes are designed to expect certain values from this file. It’s a contract between you and the theme developer. They say, “If you provide a value for projectKPI in your params, I promise to display it prominently in the footer.” You hold up your end of the bargain by defining it, and Hugo dutifully makes it available everywhere via the .Site.Params object. It’s dependency injection for the static site set. Beautiful, really.

Where to Put the Thing and How to Structure It

This isn’t rocket surgery. You’ll create a file named params.toml in your site’s config/_default/ directory. If you’re using environment-specific configs (config/production/), that’s great, but the _default/ one is your baseline. The structure inside this file is a nested tree of key-value pairs, and it mirrors exactly how you’ll access it in your templates.

Let’s say you want to configure a copyright message and a list of social media handles. Your config/_default/params.toml would look like this:

copyright = "© 2023 My Brilliant Site. All Rights Reserved."

[social]
  twitter = "myhandle"
  github = "myorg"
  linkedin = "myprofile"

In your layout, you’d access these with .Site.Params.copyright and .Site.Params.social.twitter. See? The [social] block becomes a nested dictionary under .Params. This structure is your best friend for organizing complex configuration.

The Reality of Theme Defaults and Overrides

Here’s the first ‘gotcha.’ Your params.toml doesn’t extend the theme’s default params; it overrides them completely. The theme’s own config/_default/params.toml is effectively ignored once you define your own. This is both a blessing and a curse.

The blessing: total control. The curse: if the theme adds a new, useful param six months from now, you won’t get it automatically. You’ll have to check their updated config file and add it to yours. It’s a trade-off for stability, and honestly, I prefer it this way. My site shouldn’t break because a theme author decided to rename description to bio on a whim.

A Real-World Example: Configuring a Hero Section

Let’s get concrete. Most modern themes have a hero section on the homepage. Configuring it via params is classic. The theme’s layout code is looking for specific values under .Site.Params.hero.

[hero]
  headline = "Welcome to the Jungle"
  subtitle = "We've got fun and grids."
  buttonText = "Get Started"
  buttonURL = "/docs/"
  # This image path is relative to your 'assets' folder!
  image = "img/hero.svg"

And the corresponding part in a layout file (layouts/index.html) would look something like this:

<section class="hero">
  <div class="hero-content">
    <h1>{{ .Site.Params.hero.headline }}</h1>
    <p>{{ .Site.Params.hero.subtitle }}</p>
    <a href="{{ .Site.Params.hero.buttonURL }}" class="btn">
      {{ .Site.Params.hero.buttonText }}
    </a>
  </div>
  <div class="hero-image">
    {{ with resources.Get .Site.Params.hero.image }}
      <img src="{{ .RelPermalink }}" alt="Hero image">
    {{ end }}
  </div>
</section>

Notice the smart use of with to check if the image exists before trying to render it. This is how you avoid silent failures.

Common Pitfalls: Type Safety and the Nil Problem

Hugo’s templates are dynamically typed, which is a fancy way of saying it will try its best not to crash when you ask for .Site.Params.hero.superCoolNewFeature that you haven’t defined yet. It will simply return nil. This is mostly fine, unless you start calling methods on that nil value.

For example, this will break spectacularly if you haven’t defined hero.headline:

<h1>{{ .Site.Params.hero.headline | upper }}</h1>

Why? Because nil doesn’t have an upper method. The safe way is to check for existence first or use default:

<h1>{{ with .Site.Params.hero.headline }}{{ . | upper }}{{ else }}DEFAULT HEADLINE{{ end }}</h1>
<!-- Or, more succinctly -->
<h1>{{ .Site.Params.hero.headline | default "DEFAULT HEADLINE" | upper }}</h1>

When Params Aren’t Enough: Moving to Front Matter

The params.toml file is for global configuration. When you need to set a value on a per-page basis—like a specific thumbnail image for a blog post—you don’t put it in params.toml. You define it in the content’s front matter. This is a crucial distinction. Params are for site-wide settings; front matter is for page-specific settings. The theme will typically look for a value first in the page’s front matter (e.g., .Params.thumbnail), and if it doesn’t find it, it might fall back to a global setting in .Site.Params (e.g., .Site.Params.defaultThumbnail). A well-built theme will handle this fallback logic for you.

The power move is when you start using your params.toml to define default values that can be overridden by individual page’s front matter. That’s the sweet spot for flexible, maintainable site design. You’ve centralized the defaults but left the door open for exceptions. Now you’re not just using a theme; you’re collaborating with it.