Right, let’s talk about the data/ directory. This is Hugo’s secret weapon for when you need to manage structured information that isn’t a page. Think of it as your project’s personal database, but infinitely simpler and stored as text files. It’s where you put all the stuff that makes your site dynamic—product listings, team member bios, a curated list of your favorite terrible movies, you name it.

The genius here is that you can access this data from any template, anywhere on your site. No more hard-coding the same list of links in twenty different pages. You define it once in data/ and then iterate over it. It’s the DRY (Don’t Repeat Yourself) principle, and Hugo enforces it with the subtlety of a sledgehammer, which I appreciate.

You can use JSON, YAML, TOML, or even CSV files in here. The choice is yours, but it’s not a free choice; it’s a “which flavor of syntactic nonsense do you prefer?” situation. I’m team YAML for complex, nested data (it’s easier to read) and TOML for simpler stuff (it’s less finicky about commas and quotes than JSON). I use CSV only if I’m being actively threatened.

Accessing Your Data is Stupidly Simple

Hugo automatically loads every file in your data/ directory and makes it available via the .Site.Data function. The filename (without the extension) becomes the top-level key. It’s so straightforward it almost feels like magic, but it’s the good kind of magic, not the kind that requires a blood sacrifice.

Let’s say you have a file at data/authors/jane-doe.yaml. Here’s what that file might look like:

name: Jane Doe
role: Chief Chaos Engineer
bio: Jane specializes in creating problems she then gets paid to solve.
social:
  - platform: Twitter
    handle: '@janedoe'
  - platform: GitHub
    handle: 'janedoe'

To slurp this data into a template, you just reference its path from .Site.Data:

{{ with index .Site.Data "authors" "jane-doe" }}
  <h3>{{ .name }}</h3>
  <p>{{ .role }}</p>
  <ul>
    {{ range .social }}
      <li>{{ .platform }}: <a href="https://{{ .platform }}.com/{{ .handle }}">{{ .handle }}</a></li>
    {{ end }}
  </ul>
{{ end }}

See? No convoluted import statements or configuration. It’s just… there. The with block is a nice touch to ensure the data exists before we try to use it, saving you from a silent failure that makes you question your life choices for an hour.

The Perilous Subtleties of YAML vs. JSON vs. TOML

This is where the designers’ choice to be format-agnostic introduces some delightful little footguns.

  • YAML is great for human-edited data because it doesn’t require brackets and quotes everywhere. But beware: its syntax is deceptively complex. Forget a space? That’s an error. Mix tabs and spaces? May God have mercy on your soul. Also, parsing dates in YAML can sometimes make Hugo look at you funny.
  • JSON is unambiguous and universally understood. It’s the safe choice, but it’s also noisy and a pain to write by hand. You will spend 90% of your time making sure your commas are in the right place. It’s fine for machine-generated data, though.
  • TOML is a nice middle ground. It’s more forgiving than YAML but more human-friendly than JSON. Its arrays of tables are particularly elegant for certain data structures. This is my default recommendation for most projects.

The key thing to remember: Hugo parses these files at build time, not runtime. If you have a malformed YAML file, your entire site will refuse to build. There’s no “try/catch” here. It’s all or nothing.

Organizing Your Data Directory: A Dose of Sanity

Don’t just throw 50 files into the root of data/. You’ll regret it immediately. Use subdirectories to group related content, just like we did with authors/ above. Hugo is smart enough to namespace it all correctly.

So, data/products/light-sabers.yaml becomes accessible at .Site.Data.products.light-sabers. This keeps things clean, logical, and manageable. You can even use multiple files for the same “type” of data. For a list of products, you might have:

data/
└── products/
    ├── light-sabers.yaml
    ├── droids.yaml
    └── hyperdrives.yaml

Then, to get all products across all files, you’d have to range through .Site.Data.products. This is a powerful pattern for breaking down large datasets.

The CSV Wildcard

Ah, CSV. The format everyone loves to hate. Hugo supports it, but with a major caveat: it’s not for structured data in the same way. A CSV file like data/products.csv:

name,price,description
Lightsaber,29999.99,Elegant weapon for a more civilized age.
Droid,4999.99,Protocol and astromech units available.

Gets parsed into an array of rows, where the first row is assumed to be the header. You access it like this:

{{ range $index, $row := .Site.Data.products }}
  {{ if ne $index 0 }} <!-- Skip the header row -->
    <div>
      <h4>{{ $row.name }}</h4>
      <p>Price: {{ $row.price }}</p>
    </div>
  {{ end }}
{{ end }}

It works, but it’s clunky. You’re fighting against the template syntax to skip the header row. Use CSV only if you’re truly trapped in a spreadsheet-based workflow. For anything else, use a proper structured format where the first item isn’t a landmine.