Right, so you’ve got your data/ directory humming along nicely. You’re pulling in a single config.yaml file and feeling pretty good about yourself. I get it. But your project is growing up, and you’re starting to realize that dumping everything into one massive file is like trying to cook a five-course meal on a single burner. It’s time to get organized. This is where nesting comes in, and Hugo’s data templates are about to become your new best friend.

The beauty—and occasional headache—of the data/ directory is that it mirrors your filesystem structure. Hugo doesn’t just read files; it builds a whole tree of data you can walk through. This means you can structure your data logically, breaking it down into categories, years, authors, or whatever makes sense for your content.

The Magic of the Scoped Data Folder

Let’s say you’re building a site about classic video games. You could put everything in a games.yaml file. But don’t. Instead, structure your data/ directory like this:

data/
└── games/
    ├── action/
    │   ├── doom.yaml
    │   └── contra.yaml
    ├── rpg/
    │   ├── chrono-trigger.yaml
    │   └── final-fantasy-vi.yaml
    └── adventure/
        └── monkey-island.yaml

Now, to access this in a template, you use the .Site.Data object and simply… navigate. Want a list of all games in the action genre?

{{ with .Site.Data.games.action }}
  <h2>Action Games</h2>
  <ul>
  {{ range . }}
    <li>{{ .title }} ({{ .year }}) - Rating: {{ .rating }}/5</li>
  {{ end }}
  </ul>
{{ end }}

The key here is that {{ .Site.Data.games.action }} returns a slice (an array) of the data defined in all the files within the data/games/action/ directory. Hugo automatically reads every file in a directory and makes its content available as a collection. This is incredibly powerful for organization.

Accessing a Single Nested File

But what if you want to get the data from just one specific file, say doom.yaml? You just keep drilling down. The filename (without the extension) becomes the key.

{{ $doom := .Site.Data.games.action.doom }}
<h3>{{ $doom.title }}</h3>
<p>Platform: {{ delimit $doom.platforms ", " }}</p>

This assumes your doom.yaml looks something like this:

title: "Doom"
year: 1993
rating: 5
platforms:
  - MS-DOS
  - Super Nintendo
  - PlayStation
  - [Every device known to humanity]

The Index File Trick

Here’s a pro-tip that the docs often gloss over. What if you need some metadata about the genre itself, not just the games in it? Let’s say you want a description of the “action” genre. This is where _index.yaml (or _index.json) comes to the rescue.

Create a file at data/games/action/_index.yaml:

description: "Games characterized by fast-paced gameplay, often involving combat and precision."
era: "1980s-Present"

Now, watch this. The data from _index.yaml is attached to the collection itself. You access it with the magical .Params key on the collection.

{{ $actionGenre := .Site.Data.games.action }}
<h2>Action Games</h2>
<p><em>{{ $actionGenre.Params.description }}</em></p> <!-- This is from _index.yaml -->
<ul>
{{ range $actionGenre }} <!-- This loops through the game files -->
  <li>{{ .title }}</li>
{{ end }}
</ul>

It’s a bit of a weird syntax, I’ll admit. The designers decided that the index file’s data shouldn’t muddy the waters of the actual range-able items, so they stashed it in .Params. It’s not intuitive, but it works and keeps things clean.

Common Pitfalls and How to Avoid Them

  1. Typos and Casing: This is the number one cause of “why is my template empty?!” headaches. The filesystem is case-sensitive on many platforms. Games/ is not the same as games/. Be consistent, and prefer lowercase for safety. Check your paths twice.

  2. Mixing Data Types in a Directory: Hugo will happily read both .yaml and .json files in the same directory. This is fine… until it isn’t. The structure of your data objects should be consistent. If one file has a rating key as a string and another has it as a number, you’re going to have a bad time when you try to sort them. Be consistent in your schema.

  3. Over-Nesting: Just because you can nest things ten levels deep doesn’t mean you should. {{ .Site.Data.content.blog.posts.2023.07.draft.my_post }} is a nightmare to type and maintain. Structure logically, but keep it relatively flat. Two to three levels of nesting is often the sweet spot.

  4. The Empty Directory Gotcha: If a directory exists but is empty, .Site.Data.games.action will return nil. Always use with or if to check for its existence before ranging, or you’ll throw a wrench into your template rendering.

The real power here is that you’re no longer just dumping data; you’re architecting it. You’re building a structured content API that lives right in your repo, and your templates can traverse it with precision. It’s one of those features that feels a bit quirky at first, but once you get it, you’ll wonder how you ever lived without it.