Using dynamic paths with the range function

Hello Hugo users,

I am building my own blog with my own template using hugo. All of this has been a very steep learning curve for me, but I was able to resolve all challenges so far and I’m happy with the results and liking Hugo very much. I am almost done, but got stuck on the following.

I am trying to build a commenting system where I will have a YAML file for each comment, storing the comment files for each blog in their own directory. The relevant part of my project structure looks like this:

.
├── /content/blogs/article-a.md
├── /content/blogs/article-y.md
├── /content/blogs/article-z.md
├── /data/comments/article_a/
├── /data/comments/article_y/comment_a.yml
├── /data/comments/article_z/comment_a.yml
└── /data/comments/article_z/comment_b.yml

This is my environment:

C:\site\test1>hugo env
Hugo Static Site Generator v0.50 windows/amd64 BuildDate: 2018-10-29T09:53:09Z
GOOS="windows"
GOARCH="amd64"
GOVERSION="go1.11"

Right now, I have the following piece of code in the partial that builds the HTML body for my blog pages. I need to create every empty comment directory for every blog when using this. Doing so allows me to use readDir: if the comments folder for a blog is empty, there are no comments yet so I don’t have to display them on the page. I use the with conditional to check for this.
Then, I’m trying to put a variable into the range command. Each blog has a variable called page_id in the front matter. The contents of these variables correspond with the comment directory names for the blogs.

{{ $comment_prefix := "/data/comments/" }}
{{ $post_comment_path := (print $comment_prefix $.Params.page_id) }}
{{ $test_comment_path := readDir $post_comment_path }}
{{ with $test_comment_path }}              
  {{ $data_prefix := "$.Site.Data.comments." }}
  {{ $blog_comment_path := (print $data_prefix $.Params.page_id) }}
  {{ range $blog_comment_path }}
    <b>Name: </b>{{ .name }}<br>
  {{ end }}
{{ else }}
  Be the first to leave a comment!
{{ end }}

Running this results in the following error:

C:\site\test1>hugo server -D
e[?25lBuilding sites … e[1;31mERRORe[0m 2019/03/06 17:32:19 [en] page "C:\\site\\test1\\content\\blogs\\20180917-using-docker-securely-on-management-servers.md": render of "page" failed: execute of template failed: template: blogs/single.html:5:3: executing "blogs/single.html" at <partial "body-conten...>: error calling partial: e[1;36m"C:\site\test1\themes\labtime\layouts\partials\body-content-blog.html:38:23"e[0m: execute of template failed: template: partials/body-content-blog.html:38:23: executing "partials/body-content-blog.html" at <$blog_comment_path>: range can't iterate over $.Site.Data.comments.y
e[KTotal in 196 ms
Error: Error building site: failed to render pages: [en] page "C:\\site\\test1\\content\\blogs\\20181103-announcing-some-lab-time-changes.md": render of "page" failed: execute of template failed: template: blogs/single.html:5:3: executing "blogs/single.html" at <partial "body-conten...>: error calling partial: e[1;36m"C:\site\test1\themes\labtime\layouts\partials\body-content-blog.html:38:23"e[0m: execute of template failed: template: partials/body-content-blog.html:38:23: executing "partials/body-content-blog.html" at <$blog_comment_path>: range can't iterate over $.Site.Data.comments.z

The stupid thing is that putting a static data reference into the range function with the same code works just fine, but obviously renders each page that has content in its comments directory with the same comments from the one blog I statically referenced:

{{ $comment_prefix := "/data/comments/" }}
{{ $post_comment_path := (print $comment_prefix $.Params.page_id) }}
{{ $test_comment_path := readDir $post_comment_path }}
{{ with $test_comment_path }}              
  {{ $data_prefix := "$.Site.Data.comments." }}
  {{ $blog_comment_path := (print $data_prefix $.Params.page_id) }}
  {{ range $.Site.Data.comments.z }}
    <b>Name: </b>{{ .name }}<br>
  {{ end }}
{{ else }}
  Be the first to leave a comment!
{{ end }}

I don’t understand why the range function won’t accept a variable that holds the exact same string as I put into it with the static reference. To clarify this further, I get two errors saying this: range can’t iterate over $.Site.Data.comments.y|z

However, my working test literally contains code saying this: range $.Site.Data.comments.z. What’s the difference? Is there any way to resolve this behavior and achieve what I’m trying to do here?

I’ve got my project in this repo:

Only the comment code that I’m working on isn’t pushed to this repo yet, as the repo code is already live automatically at https://test.lab-time.it. I don’t want to put broken code into it.

So typically after I posted this, I found another thread with a new search query and it contained a working solution. The following piece of code is now working for me:

{{ $comment_prefix := "/data/comments/" }}
{{ $post_comment_path := (print $comment_prefix $.Params.page_id) }}
{{ $test_comment_path := readDir $post_comment_path }}
{{ with $test_comment_path }}
  {{ range (index $.Site.Data.comments $.Params.page_id) }}
    <b>Name: </b>{{ .name }}<br>
  {{ end }}
{{ else }}
  Be the first to leave a comment!
{{ end }}

I know the behavior is now correct, but I don’t understand what the index keyword does for the range function.

It grabs the data file named whatever the value of $.Params.page_id is on that iteration.

1 Like

What @zwbetz said. Basically you have those keys in an interface (most likely a map) and the index function does the lookup for you. See: https://gohugo.io/functions/index-function/#readout

By the way I am very interested to read more about the static commenting system you’re trying to build. I got the Hugo part. As you said above, basically you are fetching comments as data files and then call each one under its respective post via the .Params.page_id.

But what does the other -non Hugo- half of your commenting system look like? How do you capture the comments and how are you outputting them as data file?

If you care to share please do.

Am curious about this as well

I would love to share the working system, but at this moment it is only an idea. I found no other commenting solution that fits my needs, so I have to roll my own. It’s not the most fun thing to have to build, and I’m not an experienced developer, but this is what I have in mind:

My blog content is placed in a Github repo. That repo has a webhook that calls an API on AWS API Gateway. That API Gateway thing triggers a “serverless” AWS Lambda function that has a Golang build binary. This binary clones the repo from GitHub, executes the hugo build command, then spits the result in an AWS S3 bucket. The S3 bucket is enabled for HTTP hosting and connected to CloudFront CDN for caching and HTTPS.

Now that I have the front-end part of a commenting solution done, I can add YAML files for new comments per blog. To get a full solution, I need to build two more things:
1: A client-side piece of JavaScript that pulls the comment from a HTML form and POSTs it to another API on AWS. I’ve never written anything in JavaScript before but I’m hoping to do this quick but safe, so maybe there’s some library for input validation, and I’m hoping to include one of those “I’m not a robot” solutions.
A nice side-effect of the way I’m reading my comment files is that the comment-data itself gets run through the “| markdownify” pipeline, so users get to leave markdown comments which is nice.
2: The second part is another Lambda function on AWS, that gets called by the second API with the POSTed comment. This function will run a piece of Python code (because I can read Python the easiest and it seems that all libraries I imagine I’d need are available). The Python code will create a YAML file with the correct structure for my Hugo solution(any required data transformation will be done at this moment, because Python makes it pretty easy), then create a new branch in the GitHub repo to add the file, and package this in a pull request. When all of this stuff finally works, I can review comments by merging the branches from the pull requests.

It’s funny how I was thinking to migrate my Wordpress stuff to Hugo over a weekend last November. It’s been four months on and off, I ended up doing everything from scratch including frontend bootstrap crap that I have no experience with, but I’m almost done :grinning:

1 Like

My comment files include these fields:

  • comment_date
  • pagename
  • name
  • site
  • comment

I still need to include a sort-statement in my range function, to sort by comment_date or by the filenames of the comment files. If anybody knows how to include a sort function into the range statement that I have, that would be awesome.

Untested, but try

{{ range (sort (index $.Site.Data.comments $.Params.page_id) ".comment_date" "desc") }}

That would sort by comment_date descending.

I’d keep comments in a separate repo, and use merges to the comment repo to trigger rebuilds. Because I like to keep things tidy. :slight_smile:

Thanks! I’ve tried this exact syntax but it fails with the following error:
error calling sort: sequence must be provided
I think this is because “.comment_date” is not available in this context, it is a value that only gets loaded when the actual range “starts”.

I will be storing comment files starting with a timestamp in the filename. Would it be possible to sort on the filenames that the range function iterates through?

It may do that by default. Test it out and report back

You are correct, using my syntax defaults to sorting on filename in ascending order (oldest time stamp gets rendered first). This behavior is fine with me.