Repository: gohugoio/hugo (language: go)

Issue:
Support nested params when using Pages.GroupByParam
### What version of Hugo are you using (`hugo version`)?
<pre>
$ hugo version
hugo v0.113.0+extended linux/amd64 BuildDate=2024-01-23T04:15:35Z VendorInfo=ubuntu:0.113.0-3ubuntu0.1
</pre>

### Does this issue reproduce with the latest release?
I am not sure

### Description
When using Pages.GroupByParam hugo will fail to build if the param used is a nested like the following example.

Frontmatter:
<pre>
{
   "author": {
       "email": "jsmith@example.org",
       "name": "John Smith"
   },
}
</pre>

Grouping fails
<pre>
{{ range .Pages.GroupByParam "author.name" }}
</pre>

But sorting works
<pre>
{{ range .Pages.ByParam "author.name" }}
</pre>

### Error
<pre>
execute of template failed at <.Pages.GroupByParam>: error calling GroupByParam: there is no such a param
</pre>

I reported this issue on discourse.gohugo.io and was asked to submit this ticket.
https://discourse.gohugo.io/t/how-to-use-groupbyparam-with-param-subkeys/48929



Discussion / hints:
This behavior is inconsistent with [`.Pages.ByParam`](https://gohugo.io/methods/pages/byparam/) where you can sort by "foo.bar". If we can sort by a dot-chained parameter we should be able to group by it too.

Failing test:

```go
func TestFoo(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
disableKinds = ['section','rss','sitemap','taxonomy','term']
-- layouts/_default/home.html --
{{- range site.RegularPages.GroupByParam "author.name" -}}
  {{- .Key -}}|
  {{- range .Pages -}}
    {{ .Title -}}|
  {{- end -}}
{{- end }}
-- layouts/_default/single.html --
{{ .Title }}|
-- content/p1.md --
---
title: p1
params:
  author:
    name: John
---
-- content/p2.md --
---
title: p2
params:
  author:
    name: John
---
-- content/p3.md --
---
title: p3
params:
  author:
    name: Will
---
-- content/p4.md --
---
title: p4
---

`

	b := hugolib.Test(t, files)

	b.AssertFileContent("public/index.html", "John|p1|p2|Will|p3|")
}
```

Fix patch:
diff --git a/resources/resource/resource_helpers.go b/resources/resource/resource_helpers.go
index 90075f983ab..c2bb463c82e 100644
--- a/resources/resource/resource_helpers.go
+++ b/resources/resource/resource_helpers.go
@@ -14,6 +14,7 @@
 package resource
 
 import (
+	"github.com/gohugoio/hugo/common/maps"
 	"strings"
 	"time"
 
@@ -36,9 +37,9 @@ func GetParamToLower(r Resource, key string) any {
 }
 
 func getParam(r Resource, key string, stringToLower bool) any {
-	v := r.Params()[strings.ToLower(key)]
+	v, err := maps.GetNestedParam(key, ".", r.Params())
 
-	if v == nil {
+	if v == nil || err != nil {
 		return nil
 	}
 


Test patch:
diff --git a/resources/page/pagegroup_test.go b/resources/page/pagegroup_test.go
index 5008aa72016..69243ac4836 100644
--- a/resources/page/pagegroup_test.go
+++ b/resources/page/pagegroup_test.go
@@ -15,7 +15,7 @@ package page
 
 import (
 	"context"
-	"reflect"
+	"github.com/google/go-cmp/cmp"
 	"strings"
 	"testing"
 
@@ -55,12 +55,23 @@ func preparePageGroupTestPages(t *testing.T) Pages {
 		p.params["custom_param"] = src.param
 		p.params["custom_date"] = cast.ToTime(src.date)
 		p.params["custom_string_date"] = src.date
+		p.params["custom_object"] = map[string]any{
+			"param":       src.param,
+			"date":        cast.ToTime(src.date),
+			"string_date": src.date,
+		}
 		pages = append(pages, p)
 	}
 	return pages
 }
 
+var comparePageGroup = qt.CmpEquals(cmp.Comparer(func(a, b Page) bool {
+	return a == b
+}))
+
 func TestGroupByWithFieldNameArg(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -70,15 +81,13 @@ func TestGroupByWithFieldNameArg(t *testing.T) {
 	}
 
 	groups, err := pages.GroupBy(context.Background(), "Weight")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByWithMethodNameArg(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -87,15 +96,13 @@ func TestGroupByWithMethodNameArg(t *testing.T) {
 	}
 
 	groups, err := pages.GroupBy(context.Background(), "Type")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByWithSectionArg(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -104,15 +111,13 @@ func TestGroupByWithSectionArg(t *testing.T) {
 	}
 
 	groups, err := pages.GroupBy(context.Background(), "Section")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be\n%#v, got\n%#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByInReverseOrder(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -122,47 +127,39 @@ func TestGroupByInReverseOrder(t *testing.T) {
 	}
 
 	groups, err := pages.GroupBy(context.Background(), "Weight", "desc")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByCalledWithEmptyPages(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	var pages Pages
 	groups, err := pages.GroupBy(context.Background(), "Weight")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if groups != nil {
-		t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, qt.IsNil)
 }
 
 func TestReverse(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 
 	groups1, err := pages.GroupBy(context.Background(), "Weight", "desc")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
+	c.Assert(err, qt.IsNil)
 
 	groups2, err := pages.GroupBy(context.Background(), "Weight")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	groups2 = groups2.Reverse()
+	c.Assert(err, qt.IsNil)
 
-	if !reflect.DeepEqual(groups2, groups1) {
-		t.Errorf("PagesGroup is sorted in unexpected order. It should be %#v, got %#v", groups2, groups1)
-	}
+	groups2 = groups2.Reverse()
+	c.Assert(groups2, comparePageGroup, groups1)
 }
 
 func TestGroupByParam(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -172,15 +169,13 @@ func TestGroupByParam(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByParam("custom_param")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByParamInReverseOrder(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -190,12 +185,8 @@ func TestGroupByParamInReverseOrder(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByParam("custom_param", "desc")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByParamCalledWithCapitalLetterString(t *testing.T) {
@@ -208,10 +199,12 @@ func TestGroupByParamCalledWithCapitalLetterString(t *testing.T) {
 	groups, err := pages.GroupByParam("custom_param")
 
 	c.Assert(err, qt.IsNil)
-	c.Assert(groups[0].Key, qt.Equals, testStr)
+	c.Assert(groups[0].Key, qt.DeepEquals, testStr)
 }
 
 func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	delete(pages[1].Params(), "custom_param")
@@ -223,36 +216,49 @@ func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByParam("custom_param")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByParamCalledWithEmptyPages(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	var pages Pages
 	groups, err := pages.GroupByParam("custom_param")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if groups != nil {
-		t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, qt.IsNil)
 }
 
 func TestGroupByParamCalledWithUnavailableParam(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	_, err := pages.GroupByParam("unavailable_param")
-	if err != nil {
-		t.Errorf("GroupByParam returned an error when it shouldn't")
+	c.Assert(err, qt.IsNil)
+}
+
+func TestGroupByParamNested(t *testing.T) {
+	c := qt.New(t)
+
+	t.Parallel()
+	pages := preparePageGroupTestPages(t)
+
+	expect := PagesGroup{
+		{Key: "bar", Pages: Pages{pages[1], pages[3]}},
+		{Key: "baz", Pages: Pages{pages[4]}},
+		{Key: "foo", Pages: Pages{pages[0], pages[2]}},
 	}
+
+	groups, err := pages.GroupByParam("custom_object.param")
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByDate(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -262,15 +268,13 @@ func TestGroupByDate(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByDate("2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByDateInReverseOrder(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -280,15 +284,13 @@ func TestGroupByDateInReverseOrder(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByDate("2006-01", "asc")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByPublishDate(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -298,15 +300,13 @@ func TestGroupByPublishDate(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByPublishDate("2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByPublishDateInReverseOrder(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -316,27 +316,23 @@ func TestGroupByPublishDateInReverseOrder(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByDate("2006-01", "asc")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByPublishDateWithEmptyPages(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	var pages Pages
 	groups, err := pages.GroupByPublishDate("2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if groups != nil {
-		t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, qt.IsNil)
 }
 
 func TestGroupByExpiryDate(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -346,15 +342,13 @@ func TestGroupByExpiryDate(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByExpiryDate("2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByParamDate(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -364,16 +358,30 @@ func TestGroupByParamDate(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByParamDate("custom_date", "2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
+}
+
+func TestGroupByParamDateNested(t *testing.T) {
+	c := qt.New(t)
+
+	t.Parallel()
+	pages := preparePageGroupTestPages(t)
+	expect := PagesGroup{
+		{Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
+		{Key: "2012-03", Pages: Pages{pages[3]}},
+		{Key: "2012-01", Pages: Pages{pages[1]}},
 	}
+
+	groups, err := pages.GroupByParamDate("custom_object.date", "2006-01")
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 // https://github.com/gohugoio/hugo/issues/3983
 func TestGroupByParamDateWithStringParams(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -383,15 +391,29 @@ func TestGroupByParamDateWithStringParams(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByParamDate("custom_string_date", "2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
+}
+
+func TestGroupByParamDateNestedWithStringParams(t *testing.T) {
+	c := qt.New(t)
+
+	t.Parallel()
+	pages := preparePageGroupTestPages(t)
+	expect := PagesGroup{
+		{Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
+		{Key: "2012-03", Pages: Pages{pages[3]}},
+		{Key: "2012-01", Pages: Pages{pages[1]}},
 	}
+
+	groups, err := pages.GroupByParamDate("custom_object.string_date", "2006-01")
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByLastmod(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -401,15 +423,13 @@ func TestGroupByLastmod(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByLastmod("2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByLastmodInReverseOrder(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -419,15 +439,13 @@ func TestGroupByLastmodInReverseOrder(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByLastmod("2006-01", "asc")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be\n%#v, got\n%#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByParamDateInReverseOrder(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
 	expect := PagesGroup{
@@ -437,22 +455,16 @@ func TestGroupByParamDateInReverseOrder(t *testing.T) {
 	}
 
 	groups, err := pages.GroupByParamDate("custom_date", "2006-01", "asc")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if !reflect.DeepEqual(groups, expect) {
-		t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, comparePageGroup, expect)
 }
 
 func TestGroupByParamDateWithEmptyPages(t *testing.T) {
+	c := qt.New(t)
+
 	t.Parallel()
 	var pages Pages
 	groups, err := pages.GroupByParamDate("custom_date", "2006-01")
-	if err != nil {
-		t.Fatalf("Unable to make PagesGroup array: %s", err)
-	}
-	if groups != nil {
-		t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
-	}
+	c.Assert(err, qt.IsNil)
+	c.Assert(groups, qt.IsNil)
 }