18.6 Building a Team Page from a Data File
Right, so you’ve got a list of people, and you want to build a team page without copy-pasting a mountain of HTML for every single profile. Welcome to the party. We’ve all been there, staring at a dozen nearly identical <div> blocks, knowing that adding a new team member is an exercise in tedious, error-prone repetition. This is where Hugo’s data templates and the data/ directory come in to save your sanity. Think of it as moving your content out of your templates and into a structured, easily manageable file—like a mini-database for your site.
The concept is gloriously simple. You create a file (YAML, JSON, or TOML, your pick) inside your site’s data/ directory that holds structured information. Then, in your template, you reach in, grab that data, and loop over it. No more hard-coding.
Your First Data File: team.yaml
Let’s start with the data itself. I’m a YAML guy for this kind of thing—it’s clean and easy to read. Create a file at data/team/management.yaml. The subdirectory isn’t strictly necessary, but it keeps things organized when you have multiple data files.
- name: "Anjali Sharma"
role: "Captain of Chaos"
bio: "Anjali keeps the servers running and the coffee flowing. Don't make her deploy on a Friday."
image: "anjali.jpg"
social:
- icon: "github"
url: "https://github.com/anjali"
- icon: "mastodon"
url: "https://social.example/@anjali"
- name: "Marcus Chen"
role: "API Wizard"
bio: "Marcus speaks fluent JSON and once fixed a production bug in his sleep."
image: "marcus.jpg"
social:
- icon: "twitter"
url: "https://twitter.com/marcus"
- icon: "linkedin"
url: "https://linkedin.com/in/marcuschen"
See? Not rocket science. It’s just a list of objects (or arrays of dictionaries, if you want to get technical). Each item has consistent properties. This is the foundation. Everything else builds on this.
Accessing Your Data in a Template
Now, to use this in a template. Hugo magically makes this data available to you under the .Site.Data namespace. The path within the data/ directory becomes the key. Since our file is at data/team/management.yaml, we access it with .Site.Data.team.management.
Here’s how you’d use it in a layouts/team/list.html template:
<section class="team-grid">
{{ range .Site.Data.team.management }}
<div class="team-member card">
<img src="/images/team/{{ .image }}" alt="Portrait of {{ .name }}" class="card-img">
<div class="card-body">
<h3>{{ .name }}</h3>
<p class="role"><strong>{{ .role }}</strong></p>
<p>{{ .bio }}</p>
<div class="social-links">
{{ range .social }}
<a href="{{ .url }}" aria-label="{{ .icon }} profile"><i class="icon-{{ .icon }}"></i></a>
{{ end }}
</div>
</div>
</div>
{{ end }}
</section>
The {{ range }} action is your workhorse here. It loops over every item in the management list. Inside the loop, the dot . context changes. Now it refers to the current team member object, so you can access .name, .role, etc., directly.
Why This Beats the Alternatives
You might be thinking, “Couldn’t I just put this in the front matter of a page?” You could, for a very small list. But it gets messy fast. Front matter is for content specific to a single page. The data/ directory is for site-wide data. You can use the same management.yaml file in a sidebar widget, a dedicated team page, and an about page excerpt without duplicating a single byte of data. This is the DRY (Don’t Repeat Yourself) principle in action, and it’s beautiful.
Handling Missing Data Gracefully
Here’s a common pitfall: what if someone on the team is a ghost and doesn’t have any social links? Or what if the new intern’s bio is still “TBD”? If your template expects those properties to exist, their absence might break your layout or cause unexpected behavior.
This is where the with action and the isset function become your best friends. They let you check for existence before you blindly use a value.
<div class="social-links">
{{ with .social }}
{{ range . }}
<a href="{{ .url }}"><i class="icon-{{ .icon }}"></i></a>
{{ end }}
{{ else }}
<p>This person is mysteriously offline.</p>
{{ end }}
</div>
The {{ with .social }} block says: “If .social exists and isn’t empty, then use its value inside this block.” If it’s empty or nil, it jumps to the {{ else }} clause. It’s a clean, readable way to build defensive templates.
The Power of Data Files Beyond Teams
Don’t limit yourself to team pages. This pattern is your Swiss Army knife. Use it for:
- Client logos:
data/clients.yaml - Product features:
data/features.yaml - FAQ entries:
data/faq.yaml - Navigation items: This is a big one. You can drive complex, multi-level navigation menus from a data file, giving content editors a clear way to change the site nav without understanding Hugo templates.
The data/ directory turns Hugo from a simple static site generator into a powerfully structured content engine. You’re not just writing HTML anymore; you’re building a system. And that, my friend, is how you level up.