Filtering pages based on a page param in Hugo

I asked this question on SO, but I think this is a better forum for it.

I have a metadata fields in some of my pages like so

---
dateStartOfEvent: "2022-06-26T00:00:00+02:00"
type: event
---

I would like to:

  1. collect all the pages where type: event
    {{- $cond1 := where .Site.RegularPages "Type" "event" }}
    {{- $events := $cond1 }}

  2. from the result of 1 above, I would like to find the pages that have dateStartOfEvent set
    {{- $cond2 := where $events ".Params.datestartofevent" "!=" "nil" }}

  3. from the result of 2 above, I would like to find the pages where dateStartOfEvent is after today
    {{- $cond3 := where $events (time .Params.dateStartOfEvent).After now }}

  4. finally, iterate over the first 10 events and list them
    {{- $events_num := (.Site.Params.widgets.events_num | default 10) }}
    {{- range first $events_num $events }}
    ...

I am facing a problem in step 3 where I get the following error: <time .Params.dateStartOfEvent>: error calling time: unable to cast <nil> of type <nil> to Time. Why? In the previous filter $cond2 I had already (in theory) removed all the pages where dateStartOfEvent did not exist, so why is this error occuring?

Since I am super new to Hugo and golang, I would also love to know if there is a better and shorter (more idiomatic) way for doing the above? For example, in JS I would just chain a bunch of filters.

There are syntax errors in the above construct.

There is no .After in Hugo there is the function after that:

slices an array to only the items after the Nth item.

Also now is not a variable, instead it is another function that returns:

the current local time

There is an example in the Documentation about showing future events in a list:

thanks for response. I am embarrassed about .After. I assumed that time.After would be an analog of time.Before (in the other direction), but I was wrong.

I looked at the example you linked, and that is pretty much exactly what I want to do. However, (in order to understand this better), I am still confused as to why my logic is not working. I now have the following

{{- $cond1  := where .Site.RegularPages "Type" "event" }}
{{- $events := $cond1 }}

{{- $cond2  := where $events ".Params.datestartofevent" "ne" "nil" }}
{{- $events := $cond2 }}

{{- $cond3  := where $events (time .Params.dateStartOfEvent) "gt" now }}
{{- $events := $cond3 }}

I still get the error <time .Params.dateStartOfEvent>: error calling time: unable to cast of type to Time

Shouldn’t $cond2weed out any pages where dateStartOfEvent doesn’t exist or is not set? (an aside: all my events pages do have a dateStartOfEvent, so actually $cond2is not even needed, but I am inserting it there just to future-proof the project just in case someone else creates and event page and forgets to add that param).

Thanks in advance for any guidance

Don’t quote nil.

Don’t quote nil.

That is ZEN. Just saying.

thanks for the hint. I followed your advice however the outcome is still the same

{{- $cond2  := where $events ".Params.datestartofevent" "ne" nil }}
{{- $events := $cond2 }}

{{- $cond3  := where $events (time .Params.dateStartOfEvent) "gt" now }}
{{- $events := $cond3 }}

the above croaks with <time .Params.dateStartOfEvent>: error calling time: unable to cast of type to Time. I also printed out the value of dateStartOfEvent, the param exists in all the pages in the range

<ul>
{{- range  $events }}
    <li>{{ printf "%#v" .Params.dateStartOfEvent }}]</li>
{{- end }}
</ul>

why is time .Params.dateStartOfEvent not casting the string to time in the where filter? Seems I am doing something fundamentally wrong and I have no idea what

Try formatting the date with something like:

{{ dateFormat "2006-01" .Params.dateStartOfEvent }}

OR

{{ time.Format "2006-01" .Params.dateStartOfEvent }}

just tried that but sorry, no luck. I get the same error as before

{{- $events := where .Site.RegularPages "Type" "event" }}
{{- $events := ($events.ByParam "datestartofevent") }}
{{- $events := where (time.Format "2006-01" .Params.dateStartOfEvent) "gt" now }}

execute of template failed: template: partials/widgets/events.html:5:26: executing "partials/widgets/events.html" at <time.Format>: error calling Format: unable to cast <nil> of type <nil> to Time

per the docs, the signature of the where function is

where COLLECTION KEY [optional OPERATOR] MATCH

maybe it is not possible to transform the key in line. I can’t figure out what is going on.

  1. Please look at this example again. In particular:

    If you restrict front matter to the TOML format, and omit quotation marks surrounding date fields, you can perform date comparisons without casting.

    There is a subtle but very important difference between this:

    # TOML
    my_custom_date = 2022-01-30T11:15:30-08:00
    

    and these:

    # TOML
    my_custom_date = "2022-01-30T11:15:30-08:00"
    
    # YAML
    my_custom_date: 2022-01-30T11:15:30-08:00
    
    # JSON
    {
      "my_custom_date": "2022-01-30T11:15:30-08:00"
    }
    
  2. Please share your project repository if you need additional assistance.

I did see that, thanks. Converting the frontmatter to TOML would involve going back to all the existing events pages and redoing the data. I will do that if I determine there is actually no way forward the way I am approaching the problem (that is, if inline transformation of keys is not possible in a where filter).

Unfortunately I can’t share the project repo as the project happens to be private, but really, it is a sum total of the following

pages with frontmatter like so

---
dateStartOfEvent: "2022-06-26T00:00:00+02:00"
type: event
---

and the partial template like so

{{/* 1. collect all the pages where type is event */}}
{{- $events := where .Site.RegularPages "Type" "event" }}

{{/* 2. from #1, find pages with "dateStartOfEvent" */}}
{{- $events := where $events ".Params.datestartofevent" "ne" nil }}

{{/* 3. from #2, find pages where "dateStartOfEvent" is after today */}}
{{- $events := where $events (time .Params.dateStartOfEvent) "gt" now }}

{{/* 4. finally, iterate over the first 10 events and list them */}}
{{- $events_num := (.Site.Params.widgets.events_num | default 10) }}  
{{- range first $events_num $events }}
...

I always get the error on step 3. Interestingly, if I remove step 3 and put the same logic inside a range loop, I don’t get an error

{{- if $events }}
<ul>
    {{- range $events }}

        {{ if (isset .Params "datestartofevent") }}
            {{ $t := (time .Params.dateStartOfEvent) }}

            {{ if gt $t now }}
                <li>{{ .Title }}</li>
            {{- end }}
        {{- end }}
        
    {{- end }}
</ul>
{{- end }}

The above, however, produces a slightly different result from what I want to accomplish. Plus, I am really interested in understanding why I can’t get my initial approach to work

This is correct. That is why the example shows two implementations; one for TOML dates, and one for TOML/YAML/JSON strings.

many thanks… this is exactly what I wanted|needed to know. Now I can look for alternative approaches. It might be worth adding this tidbit to the docs so others don’t go down the path I did

We tend to document what you can do instead of the many things you can’t do.

If you feel strongly about this, please submit a detailed suggestion for improving the docs:
https://github.com/gohugoio/hugoDocs/issues

Thanks.

Another approach, if your custom dates are strings instead of TOML dates, is to perform a string comparison in the where clause, but this could get a little wobbly if your date formats are inconsistent or contain different offsets.

The comparison operators (eq, gt, ge, etc.) can be used with strings as well.

{{ range where (where site.RegularPages "Type" "event") "Params.datestartofevent" "ge" (now.Format "2006-01-02T15:04:05-07:00") }}

Note that there’s no need to check for nil.

Finally, to clarify an earlier comment, the .After and .Before methods can be used on time.Time values.

{{ $d := time.AsTime "2022-01-30T08:00:00-08:00" }}
{{ $d.After (time.AsTime "2022-02-01") }} --> false
{{ $d.Before (time.AsTime "2022-02-01") }} --> true

But these are not useful in date comparisons (string or date values) in where clauses.

ok, thanks for the detailed advice. I might just bite the bullet and convert all my existing fm metadata to toml from the current yaml since I’d rather not do string comparisons of dates

Have a look at hugo convert toTOML . By default this is considered (correctly) unsafe, and will tell you it won’t do it without passing the --unsafe flag. Don’t do that.

Instead:

hugo convert toTOML -o /tmp/junk

Then examine the results. Dates will be quoted, but you can easily fix those after conversion in your IDE with a regex replace.