Updating a scratch pad with nested data

I’ve successfully configured Hugo with multiple output formats to render pages in different formats (e.g., multiple HTML formats, ICS for internet calendars, and JSON). This setup works great so far.

However, I’m now running into a challenge: I need Hugo to dynamically search for and resize images based on speaker names provided in the page’s front matter data. While I’ve got a working solution for the HTML template, I’m struggling to integrate this functionality into the JSON output.

Current HTML Template

Here’s the working template that locates and resizes images based on the speaker’s name:

{{ $headshot := (printf "headshots/%s.*" (default (.name | urlize | safeURL) .headshot)) -}}
{{ $img := $.Page.Resources.GetMatch $headshot }}

{{ $title := (printf "%s - %s" .name .title) }}
{{ with $img }}
    {{ $img = $img.Resize "x150" }}
    {{ return $img.RelPermalink }}    
{{ end }}

This looks for an image file named headshots/speaker-one.jpg and generates a relative permalink to the resized image. It also allows manual overrides using the .headshot variable for filenames with diacritics or other complexities.

The JSON Output Template

Currently, my JSON output template is straightforward:

{{- .Page.Params.data | jsonify -}}

Which results in:

{
    "name": "Event name",
    "speakers": [
        {
            "name": "Speaker One",
            "title": "Expert at Org Inc."
        },
        ...
    ]
}

When we can’t modify the data, our template becomes significantly more complex and error-prone, as it would mean reconstructing the entire JSON output just to add the resized images.

Front Matter Example

Here’s an example of my page’s front matter:

outputs:
  - html
  - event-data
  - event-overlays
  - event-session
  - event-agenda

data:
  name: Event name
  timezone: America/Chicago

  speakers:
    - name: Speaker One # Locate image based on this name
      title: Expert at Org Inc.

    - name: Speaker Fifty
      title: Expert at Org Ltd.

Some speakers might not have associated headshots, and in such cases, the output should gracefully handle missing images.

Desired scratch pad data

I want to enhance the data.speakers section by adding resized images for each speaker, resulting in something like this:

data:
  speakers:
    - name: Speaker One
      title: Expert at Org Inc.
      headshots:
        "150x": "./speakers/speaker-one-150x.jpg"
        "500x": "./speakers/speaker-one-500x.jpg"

    - name: Speaker Fifty
      title: Expert at Org Ltd.

Scratchpad Issue

I attempted to use Hugo’s scratch pad to dynamically update the data object, but I’m running into issues. For example, the following simple example doesn’t update the data.name value as expected:

{{- $s := newScratch -}}
{{- $s.Set "data" .Page.Params.data -}}
{{- $s.Set "data.name" "test" -}}
{{- $s.Get "data" | jsonify -}}

same for:

{{- $s := newScratch -}}
{{- $s.Set "data" .Page.Params.data -}}
{{- $s.SetInMap "data" "name" "test"  -}}
{{- $s.Get "data" | jsonify -}}

but this ‘works’ (overwrite the entire object):

{{- $s := newScratch -}}
{{- $s.Set "data" .Page.Params.data -}}
{{- $s.Set "data"  "test"  -}}
{{- $s.Get "data" | jsonify -}}

Questions

  1. Can the scratch pad or store handle nested data to begin with?
  2. Is there a better approach than the scratchpad to achieve this transformation?

Any advice or insights would be greatly appreciated. Thanks in advance for your help!

Front matter:

---
title: example
params:
  data:
    colors:
      - red
      - blue
      - green
---

Template:

{{ $s := newScratch }}
{{ $s.Set "data" .Page.Params.data }}
{{ $s.Set "data" (merge ($s.Get "data") (dict "name" "test")) }}

<pre>{{ jsonify (dict "indent" "  ") $s.Values }}</pre>

Result:

{
  "data": {
    "colors": [
      "red",
      "blue",
      "green"
    ],
    "name": "test"
  }
}

Notes:

  • In the first part of your example, there’s no need to pass the value through the safeHTML function… you’re not rendering the value.
  • It’s not clear to me why you’re using a local scratch for this. Seems like it would be easier to use the merge function.

If I got that right our OP

  • has a list of maps in the speaker attribute and
  • wants to add a new field to each map with a calculated values
  • has two (or more) output formats that should use the one time calculated data

input

params:
  data:
    name: Event name
    timezone: America/Chicago
    speakers:
      - name: Speaker One
        title: Expert at Org Inc.
      - name: Speaker Fifty
        title: Expert at Org Ltd.

output:

event: Event name
speakers:
  - headshot: "calculated image: Speaker One-img"
    name: Speaker One
    title: Expert at Org Inc.
  - headshot: "calculated image: Speaker Fifty-img"
    name: Speaker Fifty
    title: Expert at Org Ltd.
zone: America/Chicago

suggestion:

I’m pretty sure @jmooring will have a cleaner approach or even tell s me that his version will handle that… but I wanted to give it a try

I would use variables and a partial to generate the event data.

page frontmatter:

i moved the data into the params section cause custom toplevel fields are deprecated

title: Event One
date: 2023-01-01T15:00:00.000Z
draft: false
params:
  data:
    name: Event name
    timezone: America/Chicago
    speakers:
      - name: Speaker One
        title: Expert at Org Inc.
      - name: Speaker Fifty
        title: Expert at Org Ltd.

event page template:

{{ define "main" }}
   # prepare the data 
   {{ $eventdata := partialCached "speakers" .Params.data }}
  # print the data to the page
   {{ highlight (debug.Dump $eventdata) "JSON" }}
{{ end }}

event json template:

   # prepare the data 
   {{ $eventdata := partialCached "speakers" .Params.data }}
  # dump out json data to the json file
   {{ debug.Dump $eventdata }}

the speakers partial

{{ $speakers := slice }}
{{ range .speakers }}
   {{ $speakers = $speakers | append (dict
      "name" .name
      "title" .title
      "headshot" (printf "calculated image: %s-img" .name
      ))
   }}
{{ end }}
{{ return dict "event" .name "zone" .timezone "speakers" $speakers }}

hint: don’t use debug.Dump in real life cause it is internal and may change without anouncement

used as cached partial to reduce duplicate rendering. If you have speakers talking a lot, maybe have one cached partial per speaker inside the loop or prerender all speakers seperately

and ofc you may save the results to a scratch or store make sure to read the docs carefully then.

A combination of append amd merge did the job indeed:

{{- $speakers := slice -}}
{{- range .Page.Params.data.speakers -}}
    {{- $speaker := . -}}
    {{- $headshot := (printf "speakers/%s.*" (default (.name | urlize | safeURL) .headshot)) -}}
    {{- with $.Page.Resources.GetMatch $headshot -}}
        {{ $x150 := .Resize "x150" }}        
        {{ $x250 := .Resize "x250" }}
        {{ $x600 := .Resize "x600" }}

        {{- $speakers = $speakers | append (merge $speaker (dict "headshot" (dict 
            "x150" $x150.Permalink
            "x250" $x250.Permalink
            "x600" $x600.Permalink
        ))) -}}

    {{- else -}}
        {{- $speakers = $speakers | append $speaker -}}
    {{- end -}}
{{- end -}}
{{- $data := merge .Page.Params.data (dict "speakers" $speakers) -}}
{{- $data | jsonify -}}

Now we can simply move this into a cached partial and we are done :slight_smile:

Thanks!

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.