Right, so you’ve got your data files all set up in data/, looking clean and organized. But now you want to actually use that data in your content without copy-pasting HTML all over the place. This is where Hugo’s data-driven shortcodes come in. They’re the perfect bridge between your structured data and your unstructured content pages. Think of them as little factory functions; you feed them a key from your data, and they spit out the same complex HTML every time. It’s consistency and DRY principles for the win.

The Basic Anatomy of a Data Shortcode

Let’s say you have a data/team.yaml file with your brilliant colleagues:

- name: Alex Chen
  role: Senior Wizard of DevOps
  twitter: the_real_alex
- name: Sam Jones
  role: Documentation Dynamo
  twitter: sam_writes_it_all

You want a nice, consistent card to insert for any team member. You wouldn’t hardcode that. You create layouts/shortcodes/team.html:

{{ $username := .Get "who" }}
{{ $team := site.Data.team }}
{{ $member := where $team "name" $username }}

{{ with index $member 0 }}
<div class="team-card">
    <h3>{{ .name }}</h3>
    <p><strong>{{ .role }}</strong></p>
    {{ with .twitter }}<a href="https://twitter.com/{{ . }}">@{{ . }}</a>{{ end }}
</div>
{{ else }}
{{ errorf "Could not find team member named '%s' in data/team.yaml" $username }}
{{ end }

Now, in any Markdown file, you simply write:

Here's the person responsible for this masterpiece:

{{</* team who="Alex Chen" */>}}

Hugo finds “Alex Chen” in your data, populates the template, and injects the finished HTML. Clean, maintainable, and you only had to write the HTML once.

Why where and Not Something Else?

You’ll notice I used where $team "name" $username instead of something like index $team $username. This is a classic Hugo gotcha. Your data is a slice (an array), not a map. You can’t just key into it directly with a string. The where function is your filter—it sifts through the slice to find the first object where the name field matches our input. We then use index $member 0 to pluck the first (and hopefully only) result from the filtered slice. If your data was a map, this would be different, but for lists of objects, where is your best friend.

Handling the “Not Found” Case Gracefully

This is non-negotiable. What if you typo the name? &#123;&#123;</* team who="Alxe Chen" */>&#125;&#125;? The shortcode would silently output nothing, leaving a confusing empty spot on your page. That’s a terrible user experience. This is why I used the {{ else }} clause with the errorf function.

errorf is brilliant. It won’t break the build of your entire site, but it will loudly complain in the terminal during generation, telling you exactly which shortcode call has the bad data. It’s like having a friendly, paranoid code reviewer built right into the template. Always code defensively around data lookups.

Looping Inside Shortcodes for Lists

Sometimes you don’t want to pull one record; you want to pull a whole category. Let’s use a different example: data/testimonials.yaml.

- quote: "It changed my life!"
  author: "A. Customer"
  product: "widget-pro"
- quote: "Meh, it's okay."
  author: "B. Skeptic"
  product: "widget-basic"

You can create a shortcode that renders all testimonials for a specific product:

{{ $product := .Get "product" }}
{{ $testimonials := where site.Data.testimonials "product" $product }}

<section class="testimonials">
    <h2>What people are saying about {{ $product }}</h2>
    {{ range $testimonials }}
    <blockquote>
        <p>{{ .quote }}</p>
        <footer>— {{ .author }}</footer>
    </blockquote>
    {{ else }}
    <p>No testimonials yet for this product. Be the first!</p>
    {{ end }}
</section>

The key here is the range block. It iterates over the filtered slice, and the {{ else }} clause on a range executes if the slice is empty—perfect for handling products with no reviews yet.

The Gotcha: Shortcodes and Page Scope

Here’s the thing Hugo’s documentation might not scream loudly enough about: The site.Data you access in a shortcode is the global site data. It is completely unaware of the page-scoped data you might have defined in the page or section data files for the specific page calling it. This is by design, but it can trip you up. A shortcode is a standalone component; it only knows about what you pass to it and the global data store. If you need page-specific data, you must pass it in as a parameter. Remember, shortcodes are for consistency, and consistency often means ignoring the local context in favor of a global truth.