Right, so you’ve built a theme. It looks sharp. But here’s the problem: it’s a dictatorship, not a democracy. You’ve hard-coded the accent color to that specific shade of electric teal you’re so fond of, and the site title is set in stone. What if your user wants… gasp… maroon? We’re not barbarians. We need to hand over the reins, but in a controlled, sensible way. That’s where Hugo’s Params come in. Think of them as the control panel for your theme, letting the user tweak things without ever having to touch a line of Go template code.

Params are defined in the site’s config.toml (or config.yaml/config.json) file, under the [params] section. This is a deliberate separation of church and state. Your theme provides the potential for configuration (the variables), and the user’s config file provides the actual values (the settings). This keeps your theme portable and your user’s preferences safe from a theme update.

Defining Your Theme’s Parameters

Start by deciding what should be configurable. Common candidates are strings (site title, author name), colors, copyright info, social media handles, and boolean toggles (e.g., showRelatedPosts). Let’s add a few to our hypothetical config.

# In the user's config.toml
baseURL = "https://example.com/"
languageCode = "en-us"
title = "My Excellent Blog"

[params]
  author = "Anastasia Pixel"
  description = "A blog about web dev and cat videos"
  favoriteColor = "#ff3e00" # That's a nice Hugo orange
  showCopyright = true

Notice how author and description are under [params], while baseURL and title are top-level keys. The top-level ones are for Hugo’s core configuration. Your custom settings always go under [params].

Accessing Params in Your Templates

This is where the magic happens. Within your templates, you access these values through the .Site.Params object. It’s a huge map of keys and values that you can dig into. To grab our author’s name from the config above, you’d write:

<footer>
  <p{{ .Site.Params.author }}, 2024</p>
  <p>Made with Hugo and {{ .Site.Params.favoriteColor }} enthusiasm.</p>
</footer>

When Hugo builds the site, it will plop “Anastasia Pixel” and “#ff3e00” right into those spots. If the user changes the value in their config.toml and rebuilds, the output changes instantly. You’ve just created a contract: your theme promises to look for a parameter called author, and the user promises to provide it.

Handling Optional Params and Defaults

Here’s a classic rookie mistake: assuming a param will always be set. What if a user doesn’t define favoriteColor in their config? Your template will try to render <p>Made with Hugo and enthusiasm.</p>. That’s broken, and worse, it’s boring.

You must always code defensively. The default function is your best friend here. It says, “Try to use this value, but if it’s empty or missing, use this fallback instead.”

{{ $color := .Site.Params.favoriteColor | default "#2b2d42" }}
<style>
  a { color: {{ $color }}; }
</style>

Now, if the user has set favoriteColor, $color will be their value. If they haven’t, it will gracefully fall back to a nice dark blue (#2b2d42). Your theme now has sensible defaults and is much more robust. This is non-negotiable for a professional theme.

The Power of Structured Data (Nested Params)

Simple strings and booleans are great, but sometimes you need more. What if you want to let the user configure multiple social media icons? You could do it with a dozen individual params like githubHandle, twitterHandle, etc. Don’t. It’s messy and doesn’t scale. Instead, use Hugo’s support for structured data via nested params.

[params]
  favoriteColor = "#ff3e00"

  [[params.social]]
    name = "GitHub"
    icon = "github"
    url = "https://github.com/youruser"

  [[params.social]]
    name = "Mastodon"
    icon = "mastodon"
    url = "https://mastodon.social/@youruser"

This defines a list of maps under params.social. To range over it in your template:

<nav class="social-links">
  {{ range .Site.Params.social }}
    <a href="{{ .url }}" aria-label="{{ .name }}">
      <!-- You'd use an icon font or SVG here -->
      <span class="icon-{{ .icon }}"></span>
    </a>
  {{ end }}
</nav>

This is infinitely cleaner. The user can add or remove social links just by adding or removing blocks in their config file, and your template code doesn’t have to change one bit. You’re future-proofing your theme against the user’s inevitable desire to join yet another social network.

The golden rule? Never assume, always defend with default, and use structured data for anything more complex than a single value. This approach transforms your theme from a rigid sculpture into a flexible tool, which is exactly what makes a theme truly great.