A clean and simple approach I use to create repeating elements on Hugo sites.

For this example we’ll make a block to display user testimonials.

The testimonials might be shown in more than one place on the site, so it’s important that the data be stored in a single place (otherwise when testimonials change in future we’ll need to change it everywhere manually, risking missing some instances).

Create data file: /data/testimonials.toml

For this project the testimonials require a name, location and a little bit of text. To keep it super simple the name and location will be displayed in a single element, we’ll use just 2 fields, heading and body.

Saving the TOML to a dedicated testimonials.toml file in the /data directory makes it easy to get at from a partial.

# /data/testimonials.toml

[[testimonials]]
heading = "Alan Best, Brighton"
body = "A professional, trustworthy service." 

[[testimonials]]
heading = "Christine Danes, London"
body = "Couldn't ask for more!"

[[testimonials]]
heading = "Elliot Fox, Bristol"
body = "I couldn't be happier!"

TOML syntax is weird with the repeating [[testimonials]]. For comparison, this is the same data as JSON:

"testimonials": [
    {
        "heading": "Alan Best, Brighton",
        "body": "A professional, trustworthy service." 
    }, {
        "heading": "Christine Danes, London",
        "body": "Couldn't ask for more!"
    }, {
        "heading": "Elliot Fox, Bristol",
        "body": "I couldn't be happier!"
    }
]

Create partial: /layouts/partials/testimonials.html

The /data/testimonials.toml file we just created automatically becomes accessible in code as .Site.Data.testimonials.

We want to output the data as an HTML unordered list (<ul>). So we loop through the data, creating a list item (<li>) for each testimonial.

<!-- /layouts/partials/testimonials.html -->

<ul>
{{- range .Site.Data.testimonials -}}
  {{- range . -}}
    <li>
        <h3>{{- index . "heading" -}}</h3>
        <p>{{- index . "body" -}}</p>
    </li>
  {{- end }}
{{- end }}
</ul>

To explain the two nested range blocks:

  • The outer {{- range .Site.Data.testimonials -}} loops through the 3 testimonials, returning each as a map
  • The inner {{- range . -}} accesses the map itself

Create shortcode: /layouts/shortcodes/testimonials

We’ve made the partial, but those only work in .html files. For .md files we need a shortcode, so we make a simple wrapper (all it does is call the partial, passing the same context via the dot).

This may seem redundant, it’s just a pattern I like to follow as I often end up wanting to use the same functionality in both .html and .md files. If I make the shortcode first (as I used to) then when I decide I need a partial I have to convert the shortcode to a partial then make a wrapper like this… it’s happened enough that I just do it this way by habit now.

{{/* /layouts/shortcodes/testimonials */}}

{{- partial "testimonials" . -}}

Usage

To add the testimonials to a page we just add {{< testimonials >}} to the corresponding .md file in our Hugo site.