Alright, let’s talk about the workhorse, the fallback, the template that gets called into action when Hugo can’t find a more specific one: layouts/_default/single.html. This is the “you get what you get and you don’t get upset” of the Hugo templating world. If you create a piece of content—a blog post, a project page, a thought you had at 3 AM about the nature of static sites—and you haven’t created a more specific template for it (like layouts/posts/single.html), Hugo will shrug and hand it to this template. It’s your site’s safety net, and you need to make sure it’s not made of cheap rope.

Think of it this way: if your homepage is the storefront, this template is the main showroom floor for every single item you sell. It needs to be flexible, robust, and handle everything from your meticulously crafted essays to that one page you created two years ago and forgot to add a title to.

The Absolute Bare Minimum

Let’s start with what you absolutely must have. This isn’t a suggestion; it’s the law. If your template doesn’t output the content of the page, you’ve built a very pretty, very empty box.

<h1>{{ .Title }}</h1>
<article>
  {{ .Content }}
</article>

Boom. Done. It works. It’s also painfully boring and misses about 90% of the context a reader (or Google) needs. But it proves the point: .Title pulls from the front matter’s title: field, and .Content is the rendered Markdown or HTML of the page itself. This is the core of every single template.

Building Out the Full Picture

No one runs a site with just a title and content. You need metadata. You need context. This is where Hugo’s page variables (often called “the dot”) come in. Let’s build a proper template.

<article>
  <header>
    <h1>{{ .Title }}</h1>
    {{ with .Date }}
    <time datetime="{{ .Format "2006-01-02T15:04:05Z07:00" }}">
      Published: {{ .Format "January 2, 2006" }}
    </time>
    {{ end }}
    {{ with .Params.Author }}
    <p>By: {{ . }}</p>
    {{ end }}
  </header>

  {{ .Content }}

  <footer>
    {{ with .Params.tags }}
    <div class="tags">
      <strong>Tags:</strong>
      {{ range . }}
        {{ $tag := . | urlize }}
        <a href="{{ "/tags/" | relLangURL }}{{ $tag }}">#{{ . }}</a>
      {{ end }}
    </div>
    {{ end }}
  </footer>
</article>

Let’s break down the new bits. We’re using the {{ with }} statement everywhere, which is Hugo’s way of saying “if this exists, do this.” It’s a clean way to avoid printing empty HTML elements if, say, an author isn’t defined in the front matter. The .Date is built-in, but .Params.Author and .Params.tags are custom fields you’d add to your content’s front matter. This is the power of the _default template: it safely handles content that has these fields and content that doesn’t.

The .Params Trap and How to Avoid It

Here’s the first “trench” lesson. You’ll see a lot of examples that do this: {{ .Params.some_custom_field }}. This is fine… until it isn’t. If some_custom_field doesn’t exist in the front matter, this will return nil and, depending on context, might cause weirdness. The {{ with }} statement is your best friend here. It acts as an if statement, only rendering the block if the value exists and isn’t empty.

Another pitfall? Typos. Hugo won’t yell at you if you write {{ .Params.auhtor }}. It will just silently fail and render nothing. There’s no linter for this. You just have to be meticulous. I keep a frontmatter.md cheat sheet open for every project for this exact reason.

Linking with relLangURL and Absolute URLs

Notice in the tag loop we used {{ "/tags/" | relLangURL }}? This isn’t just academic. Using relLangURL (or its sibling absLangURL) is a best practice. It properly handles your site’s base URL and any language prefixes if you’re running a multilingual site. Building URLs manually with string concatenation is a one-way ticket to broken-link town. Let the Hugo functions do the heavy lifting for you. Always.

The Debugging Nuclear Option

Sometimes, you just can’t figure out what variables are available to you in a particular context. Maybe you’re trying to access a nested parameter and it’s not working. When you’re stuck, drop this into your template temporarily:

<pre>{{ printf "%#v" . }}</pre>

This will dump the entire current page object to the screen. It’s messy, it’s ugly, and it will show you exactly what Hugo sees—every built-in variable and every custom parameter. It’s the equivalent of popping the hood and looking at the engine. Just remember to remove it before you go live, unless you want your readers to see the arcane secrets of your site’s structure.