18.2 Accessing Data in Templates: .Site.Data
Right, let’s talk about .Site.Data. This is where Hugo stops being just a static site generator and starts feeling like a proper application framework. It’s the primary way you inject structured, non-content data into your templates. Think of it as your personal data pantry, stocked with JSON, YAML, or TOML goodies that you can pull out and use to build just about anything.
The concept is brilliantly simple: you drop a data file (say, authors.json) into your data/ directory, and Hugo automatically makes it available to you at .Site.Data.authors. No import statements, no configuration, no fuss. It’s just there. This is Hugo’s data-driven design philosophy at its best—convention over configuration, working exactly as you’d hope.
The Anatomy of a Data File
First, let’s stock that pantry. Your data/ directory lives at the root of your project. Inside it, you can create files in JSON, YAML, or TOML. My weapon of choice is YAML for anything I have to write by hand because I hate chasing closing brackets and commas, but JSON is perfect if your data is generated by a script.
Here’s a realistic example. Let’s say we have a list of team members. We’ll create data/team.yaml:
- name: Alice Alvarez
role: Chief Coffee Officer
gravatar: alice@example.com
currently_reading: The Phoenix Project
- name: Bob Benson
role: Debugging Ninja
gravatar: bob@example.com
currently_reading: The Manager's Path
Now, in a template, this entire array of objects is accessible via .Site.Data.team. To loop through it, you’d use Hugo’s range function:
<ul class="team-list">
{{ range .Site.Data.team }}
<li>
<img src="https://www.gravatar.com/avatar/{{ .gravatar | md5 }}?s=200&d=robohash" alt="{{ .name }}">
<h4>{{ .name }}</h4>
<p><strong>{{ .role }}</strong>. Currently reading: <em>{{ .currently_reading }}</em></p>
</li>
{{ end }}
</ul>
Notice how we can access the properties of each team member—.name, .role—directly? That’s because inside the range loop, the context (the dot, .) has been rebinded to each individual element in the array. It’s clean, intuitive, and powerful.
Accessing Nested Data and the “Gotcha”
Things get more interesting with nested data structures. Suppose you have a more complex data file for your site’s configuration, data/site.yaml:
social:
- name: Twitter
handle: '@mycoolblog'
url: 'https://twitter.com/mycoolblog'
- name: GitHub
handle: 'githubuser'
url: 'https://github.com/githubuser'
contact:
email: hello@example.com
support: help@example.com
You might intuitively try to access your Twitter handle with .Site.Data.site.social.twitter.handle. This is wrong, and you will get nothing but a sad, empty value. This is the most common pitfall with this feature.
The .Site.Data.site variable doesn’t refer to the filename site.yaml. It refers to the root key of the data within that file. In YAML and JSON, if your data starts as an array [...] or an object {...}, that’s what .Site.Data.filename becomes.
In our site.yaml example, the data is a dictionary (an object) at its root. So, the correct way to access the Twitter URL is:
{{ (index .Site.Data.site.social 0).url }}
Or, to be less fragile, you should range over the social array:
{{ range .Site.Data.site.social }}
{{ if eq .name "Twitter" }}
<a href="{{ .url }}">Follow me on {{ .name }}</a>
{{ end }}
{{ end }}
The lesson here: the structure in your template must mirror the structure in your data file exactly. There’s no magic namespace flattening. It’s a direct mapping, for better or worse.
Why This Beats The Content Directory
You might be thinking, “Couldn’t I just put this in content/team/ as Markdown files?” You could. But you shouldn’t. This is a crucial design decision.
Use .Site.Data for data, and use the content directory for content. Data is structured information meant to be displayed in a predefined, consistent way—like a team listing, a product catalog, or a list of software versions. Content is for prose—blog posts, articles, pages—where the structure is more flexible and the primary purpose is reading.
Mixing them leads to pain. Creating a content file for every team member just to power an “About Us” page is overkill. You’d have to manage front matter for things that aren’t really content, and querying them with where statements is less efficient than grabbing the ready-made data structure from .Site.Data. This separation is one of Hugo’s best features; it forces you to think clearly about what you’re building.
Best Practices and the One Weird Trick
- Keep it Flat(ish): While you can have deeply nested structures, ask yourself if you should. The flatter your data, the easier it is to reason about in your templates. I try to keep it to two levels of nesting max.
- Use for Localization: This is a killer feature. You can have
data/translations/en.yamlanddata/translations/fr.yaml. Then, based on the site language, you can access.Site.Data.translationsto populate your interface text. It’s far cleaner than stuffing it all in your config file. - Cache Invariance is a Thing: Remember, Hugo is a static generator. The data in
.Site.Datais loaded once at build time. If you change a data file, you must restart the Hugo server or rebuild for the changes to appear. It doesn’t watch data files by default in server mode, a “feature” that has tricked me more than once. Use the--disableFastRenderflag if you need it to pick up data changes on the fly.
So there you have it. .Site.Data is your Swiss Army knife for structured information. Use it wisely, watch your nesting, and for goodness’ sake, keep your JSON valid. Nothing ruins a build faster than a missing comma. Trust me, I’ve been there.