Using Content Adapters to Add Pages- Nested Structure

I have developed a go template (_content.gotmpl) which generates pages from the site/data folder and adds taxonomies from the JSON. The issue I am having is figuring out how to parse a JSON that contains multiple levels. The ideal scenario would be the content adapter adds a page for each entry in the JSON, and for child entries it creates sub pages, and recursively goes down through each level populating pages with both taxonomies and child pages.

I have prepared a JSON that follows this structure (items in parenthesis would be taxonomies):

  • School: Greenwood High (Location: New York)
  • Class: Grade 1
    • Student: Alice (ID: 101, Age: 6)
    • Student: Bob (ID: 102, Age: 7)
  • Class: Grade 2
    • Student: Charlie (ID: 201, Age: 7)
    • Student: Daisy (ID: 202, Age: 8)
  • School: Sunnydale Academy (Location: Los Angeles)
  • Class: Grade 3
    • Student: Ethan (ID: 301, Age: 10)
    • Student: Fiona (ID: 302, Age: 11)
  • Class: Grade 4
    • Student: George (ID: 401, Age: 12)
    • Student: Hannah (ID: 402, Age: 12)
  • School: Riverside School (Location: Chicago)
  • Class: Grade 5
    • Student: Ivy (ID: 501, Age: 9)
    • Student: Jack (ID: 502, Age: 10)
  • Class: Grade 6
    • No students

The expected result where I put the go template would be: 3 page links for each school

When I click on the school - I would see the classes
When I click on the class I would see students

{{ range .Site.Data.nested_school_data }}
    {{ $content := dict "mediaType" "text/markdown" "value" .school_name }}

    // Add taxonomies
    {{ $params := dict }}
    {{ range $key, $value := . }}
        {{ if eq $key "location" }}
            {{ $taxonomy := printf "%ss" $key }}
            {{ $params = merge $params (dict $taxonomy $value) }}
        {{ end }}
    {{ end }}
  
    // Hide taxonomy subpages from the sidebar
    {{ $params = merge $params (dict "toc_hide" true) }}
    
    {{ $page := dict 
        "kind" "page" 
        "type" "docs" 
        "path" .school_name  
        "title" .school_name 
        "author" ""
        "weight" 1
        "params" $params
        "content" $content 
    }}
    {{ $.AddPage $page }}
  {{ end }}

Above is my content adapter which loops over schools- works as intended as it reads only the top level of the JSON–

{{ range .Site.Data.nested_school_data }}
    {{ $school := . }}
    
    {{/* Construct the school page */}}
    {{ $schoolPath := $school.school_name | lower | replace " " "-" }}
    {{ $schoolContent := dict "mediaType" "text/markdown" "value" $school.school_name }}
    {{ $schoolTaxonomies := dict "locations" (slice $school.location) }}
    {{ $schoolPage := dict 
        "kind" "page"
        "type" "docs" 
        "path" $schoolPath 
        "title" $school.school_name
        "params" (merge $schoolTaxonomies (dict "toc_hide" true))
        "content" $schoolContent
    }}
    {{ $.AddPage $schoolPage }}
    
    {{ range $class := $school.nested_classes }}
        {{/* Construct the class page under the school */}}
        {{ $classPath := printf "%s/%s" $schoolPath ($class.class_name | lower | replace " " "-") }}
        {{ $classContent := dict "mediaType" "text/markdown" "value" $class.class_name }}
        {{ $classPage := dict 
            "kind" "page"
            "section" $schoolPath  
            "type" "docs"
            "path" $classPath
            "title" $class.class_name
            "params" (dict "toc_hide" true)
            "content" $classContent
        }}
        {{ $.AddPage $classPage }}
        
        {{ range $student := $class.students }}
            {{/* Construct the student page under the class */}}
            {{ $studentPath := printf "%s/%s" $classPath ($student.name | lower | replace " " "-") }}
            {{ $studentContent := dict "mediaType" "text/markdown" "value" (printf "Name: %s\nAge: %d" $student.name $student.age) }}
            {{ $studentParams := dict "student_id" $student.student_id "age" $student.age }}
            {{ $studentPage := dict 
                "kind" "page"
                "section" $classPath  
                "type" "docs"
                "path" $studentPath
                "title" $student.name
                "params" $studentParams
                "content" $studentContent
            }}
            {{ $.AddPage $studentPage }}
        {{ end }}
    {{ end }}
{{ end }}

Above is one of my many attempts to do nested loops and add pages within pages.

My question is: what are the limitations of go templates, is it possible to use the addPage method multiple in a nested loop, or am I limited to Hugos integration of golang and templates?A recursive loop seems like it would be relatively simple in base go- but in the hugo framework things are evidently more complex.

Any help is appreciated!

1 Like

This works as expected with your data.

content/docs/_content.gotmpl
{{/* Initialize. */}}
{{ $schoolName := "" }}
{{ $className := "" }}
{{ $studentName := "" }}

{{/* Create school pages. */}}
{{ range site.Data.nested_school_data }}
  {{ $schoolName = .school_name }}
  {{ $content := dict
    "mediaType" "text/markdown"
    "value" ""
  }}
  {{ $params := dict
    "location" .location
  }}
  {{ $page := dict
    "content" $content
    "kind" "section"
    "linkTitle" $schoolName
    "path" $schoolName
    "params" $params
    "title" (printf "%s Classes" $schoolName)
    "type" "school"
  }}
  {{ $.AddPage $page }}

  {{/* Create class pages. */}}
  {{ range .nested_classes }}
    {{ $className = .class_name }}
    {{ $content := dict
      "mediaType" "text/markdown"
      "value" ""
    }}
    {{ $params := dict }}
    {{ $page := dict
      "content" $content
      "kind" "section"
      "linkTitle" $className
      "path" (path.Join $schoolName $className)
      "params" $params
      "title" (printf "%s Students" $className)
      "type" "class"
    }}
    {{ $.AddPage $page }}

    {{/* Create student pages. */}}
    {{ range .students }}
      {{ $studentName = .name }}
      {{ $content := dict
        "mediaType" "text/markdown"
        "value" ""
      }}
      {{ $params := dict
        "student_id" .student_id
        "age" .age
      }}
      {{ $page := dict
        "content" $content
        "kind" "page"
        "path" (path.Join $schoolName $className $studentName)
        "params" $params
        "title" $studentName
        "type" "student"
      }}
      {{ $.AddPage $page }}
    {{ end }}
  {{ end }}
{{ end }}

I’ve refined the above to include page parameters.

1 Like

Thanks @jmooring and @ethanjose1 - this is great - I have a similar use case that has recursion that could go down deeper - I would want to almost have a json that could extend here - so you could do a family tree:

data
[
  {
    "node_type": "entity",
    "name": "Grandparent 1",
    "location": "Location A",
    "education_level": "High School",
    "children": [
      {
        "node_type": "group",
        "name": "Parent 1",
        "education_level": "Undergraduate",
        "children": [
          {
            "node_type": "individual",
            "name": "Child 1",
            "age": 30,
            "education_level": "Graduate",
            "children": [
              {
                "node_type": "individual",
                "name": "Grandchild 1",
                "age": 10,
                "education_level": "Elementary",
                "children": [
                  {
                    "node_type": "individual",
                    "name": "Great-Grandchild 1",
                    "age": 3,
                    "education_level": "N/A",
                    "children": [
                      {
                        "node_type": "individual",
                        "name": "Great-Great-Grandchild 1",
                        "age": 1,
                        "education_level": "N/A",
                        "children": [
                          {
                            "node_type": "individual",
                            "name": "Great-Great-Great-Grandchild 1",
                            "age": 0,
                            "education_level": "N/A",
                            "children": [
                              {
                                "node_type": "individual",
                                "name": "Great-Great-Great-Great-Grandchild 1",
                                "age": 0,
                                "education_level": "N/A",
                                "children": [
                                  {
                                    "node_type": "individual",
                                    "name": "Great-Great-Great-Great-Great-Grandchild 1",
                                    "age": 0,
                                    "education_level": "N/A",
                                    "children": [
                                      {
                                        "node_type": "individual",
                                        "name": "Great-Great-Great-Great-Great-Great-Grandchild 1",
                                        "age": 0,
                                        "education_level": "N/A",
                                        "children": [
                                          {
                                            "node_type": "individual",
                                            "name": "Great-Great-Great-Great-Great-Great-Great-Grandchild 1",
                                            "age": 0,
                                            "education_level": "N/A"
                                          }
                                        ]
                                      }
                                    ]
                                  }
                                ]
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  }
]

This cascading of information and presentation into a site has a ton of use cases - so thanks for helping us here guys - in my json the educational level would be a taxonomy term.

1 Like

The template example above explicitly creates three levels… it is not recursive. You could certainly create a recursive content adapter, but it could get tricky if the level structures are not consistent.

1 Like

Thank you for the response! This is great!

Would it be expected for taxonomy tags to appear at each individual page? If I click “Bob” do I need to change the params definition at the student level to show “age” and “student_id” as taxonomy tags? (given I have configured a taxonomies.toml, and params.toml to render them)

1 Like

You would need to set a tags (or whatever taxonomy) key under the params key. I didn’t do that in the example above.

Keep in mind that you cannot define a taxonomy within a content adapter. You have to know the taxonomies ahead of time so that you can define them in your site configuration.

And looking at the metadata in your JSON, it wasn’t clear to me why any of these would be a taxonomy, with the possible exception of location.

2 Likes

@ethanjose1 I made a couple of small edits to the example; I removed three useless initializations.

2 Likes

I see, location would indeed seem to be the only useful taxonomy term in this example.

    // Add taxonomies
    {{ $params := dict }}
    {{ range $key, $value := . }}
        {{ if eq $key "location" }}
            {{ $taxonomy := printf "%ss" $key }}
            {{ $params = merge $params (dict $taxonomy $value) }}
        {{ end }}
    {{ end }}
  
    // Hide taxonomy subpages from the sidebar
    {{ $params = merge $params (dict "toc_hide" true) }}

So I assume I could just build this logic at each level of the hierarchy for the loops. Again thank you for the help here!

1 Like

If location is the only taxonomy for schools, and there are no other front matter parameters, there’s no need to loop.

{{ $params := dict "locations" (slice .location) }}

In the above note that we’re setting locations to a slice, despite the fact that there’s only one value. Most templates/themes that I have seen expect a slice, so it’s good to get into the habit.

This assumes you’ve defined the taxonomy in your site configuration:

[taxonomies]
location = 'locations'
1 Like

Got it, thank you- I have adjusted accordingly.

1 Like

Hi @jmooring @Adam_Fermier thank you for supporting this wonderful discussion.

I can safely say that my issue is resolved - using explicit declarations of each level.

I must admit I am curious how a recursive go template could be created to do this in a one-size-fits all sort of way (in terms of JSON structure) this would allow for bulk page creation without needing to define levels in the go template- but rather let the loop crank through all levels.

I would love to explore how Hugo would handle this tree like structure for pages- as it could offload a lot of manual work structuring the site and designing loops.

My ask is: could I declare this issue solved and open a discussion to investigate this? Any help from the community would be amazing!

Again thanks for all of the help here!

Yeah, mark this as resolved an open a new topic titled “How to make a recursive template”.

1 Like