How to modularize go templates and reuse them

Hi all,

I was wondering if anyone has an idea of how to basically create a parametrized, reusable content adapter using modules, so that target sites can reuse it with different input data.

Below is some code to get data and create pages from data. What I wanted to do was to add this to my theme module (or maybe a separate module if that is better - I have no idea :grinning:) and somehow be able to pull this into my sites while allowing a user/dev to change what data is used when they import/mount it.

{{ $dataFile := .dataFile }}
{{ $useHugoData := .useHugoData | default "false" }}
{{ $hugoData := "" }}

{{ if eq $useHugoData "true" }}
  {{ if or (hasPrefix $dataFile "http://") (hasPrefix $dataFile "https://") }}
    {{ with resources.GetRemote $dataFile }}
      {{ $hugoData = . | transform.Unmarshal }}
    {{ end }}
  {{ else }}
    {{ with resources.Get $dataFile }}
      {{ $hugoData = . | transform.Unmarshal }}
    {{ else with fileExists $dataFile }}
      {{ $hugoData = readFile $dataFile }}
    {{ else }}
      {{ errorf "File not found: %s" $dataFile }}
    {{ end }}
  {{ end }}
{{ end }}

{{ return $hugoData }}

I would call this partial in my go template like so:

{{ $dataFile := "path/to/your/data.json or remote location" }}
{{ $hugoData := partial "get-hugo-data.html" (dict "dataFile" $dataFile "useHugoData" "true") }}


{{ with $hugoData }}
  {{ template "walk" (dict "data" .roots "path" "" "ctx" $) }}
{{ end }}

{{ define "walk" }}
  {{ range .data }}
    <!-- Build the path for the current node -->
    {{ $path := path.Join $.path .title }}

    <!-- Render the page content -->
    {{ template "add-page" (dict "data" . "path" $path "ctx" $.ctx) }}


    <!-- Recursively walk through children -->
    {{ with .children }}
      {{ template "walk" (dict "data" . "path" $path "ctx" $.ctx) }}
    {{ end }}
  {{ end }}
{{ end }}

{{ define "add-page" }}
  {{ with .data }}

    <!-- Match and create taxonomies from config -->
    {{ $taxonomies := readFile "config/_default/taxonomies.yaml" | transform.Unmarshal }} 
    {{ $params := dict }}

    {{ range $key, $value := . }}
      {{ if isset $taxonomies $key }}
        {{ $taxonomy := index $taxonomies $key }}
        {{ $params = merge $params (dict $taxonomy $value) }}
      {{ else }}
        {{ $params = merge $params (dict $key $value) }}
      {{ end }}
    {{ end }}
 

    {{ $content := dict "mediaType" "text/markdown" "value" .content }}

    {{$kind := ""}}
    {{$type := ""}}
    {{ if  .children}}
        {{ $kind = "section" }}
        {{ $type = "customTypeForSection"}}
        {{ end }}
    {{ else }}
        {{ $kind = "page" }}
        {{ $type = "docs"}}
    {{ end }}

    {{ $page := dict
      "content" $content
      "kind" $kind
      "params" $params
      "path" $.path
      "title" .title
      "type" $type
    }}

    {{ $.ctx.AddPage $page }}
  {{ end }}
{{ end }}

I am not confident with any approach right now so would love some Hugo wisdom, I know I can add this content adapter to the myThemeModule/site/content folder and bring it in but ideally we would somehow be able to call the content adapter like we would a partial and pass in an argument (from page context or wherever) which basically tells the content adapter what dataset to use when making the pages.

If someone could help me with some ideas that would be much appreciated, and if I need to clarify I can try my best!

mmh, Keep in mind that when running an adapter:

  • you don’t have any pages available to fetch params from.
  • all pages genearated will use a relative path from the adapter location.

But there’s nothing wrong with calling partials in an adapter. Use global site function to access
Data and Params

So you could do something like that (no error checking… just a POC

hugo.toml
[params.Adapter]
   enable = true
   data = [ 'a', 'b']
content/_content.gotmpl (either in module or project)
{{ if site.Params.Adapter.Enable }}
    {{ range site.Params.Adapter.Data}}
        {{ partial "addPages" (slice $ .) }}
    {{ end }}
{{ else }}
    {{ warnidf "ADAPTER" "Content adapter is disabled" }}
{{ end }}
partial addPages
{{ $adapter := index . 0 }}
{{ $datafile := index . 1 }}
{{ warnf "addPages: %d : %s  -> %s" math.Counter templates.Current.Name $datafile}}
{{ $data := index site.Data  $datafile }}
{{ $base := $data.path }}
{{ range $data.pages }}
    {{ $path := add $base "/" . }}
    {{ $content := dict
        "mediaType" "text/markdown"
        "value" (add "Page was generated at " $path)
    }}
    {{ $page := dict
        "content" $content
        "kind" "page"
        "path" $path
        "title" .
    }}
    {{ $adapter.AddPage $page }}
{{ end }}

my datafiles matching the addPages code looks like these:

# FILE A.yaml
path: A
pages:
  - A_one
  - A_two

# FILE: B.yaml
path: B
pages:
  - B_one
  - B_two

p.s.

  • move all data detection completely to the partial or to the adapter just as it fits for you.

  • you could also place the adapter outside content and use mounts to bring it in

   [[module.mounts]]
      source = "content"
      target = "content"
   [[module.mounts]]
      source = "themes/hugo/adapter/_content.gotmpl"
      target = "content/_content.gotmpl"
1 Like

Ah this is brilliant! I love the idea of the target site config being the central source of determining where my adapter goes (through module mount pathing) in addition to being the source of dataset options.

I will move the data collection into something more segmented for maintenance, looking forward to giving this a try tomorrow!

I appreciate you taking the time, this is quite fun for me so thanks for the help :smiley:

I have followed your approach as discussed, and it works as expected! I define a list of data options in params.contentAdapter & pull from these in my _content.gotmpl

[params.ContentAdapter]
  enable = true
  data = ["datasetFileName"]

I can then pass this ā€œdatasetFileNameā€ into my partial & build pages with some logic to determine if I want to use a remote file or one in the site/data folder.

My follow-up question is this:

My idea is to reuse this same go template multiple times by mounting it in different spots on the site but for each instance to use different datasets. I need some sort of method to map a data file to the instance of the adapter I am mounting.

  • My initial thinking is to just put the name of the dataset or remote URL in the _index.md file’s frontmatter where the content.gotmpl is being mounted- and as you probably expected, I would not have access to this page’s params in the go template (they wouldn’t exist yet), ha!

I am sure I could try some stuff to create this sort of mapping in the config or just make multiple content adapter wrappers which call the addPages partial with different data, but wondering what is the most viable option.

Again thanks for the help!

I expected that question :wink: and tried that before.
Unfortunately the new templates.Current function reports the filesystem path – which is the one from the theme – always when mounted. Namemethod. and Filename returns nil :sad_but_relieved_face: . Found no other method for solving that problem

If a content adapter could provide a method to return the path relative to the contentDir this would help you out. To support generic adapters - But I think that’s was the intended use when it starts.

dunno if this maybe worth a feature request - @bep what do you think.

additional info:
when a content adapter is mounted to two different target folders in the contentDir.
and you modify it. Hugo server reports two changes on for each content folder

1 Like