Repository: gohugoio/hugo (language: go)

Issue:
Unable to intersect two term page collections
Reference: <https://discourse.gohugo.io/t/intersect-2-term-lists-broken-in-0-123-0-onwards/48765/>

I'd give this a low priority because (a) the use case is uncommon, and (b) there's a cleaner construct (see [this comment](https://discourse.gohugo.io/t/intersect-2-term-lists-broken-in-0-123-0-onwards/48765/2)) that works fine with v0.123.0 and later.

The test case below fails with v0.123.0 and later.

```go
func TestFoo(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
capitalizeListTitles = false
disableKinds = ['rss','sitemap']
-- content/p1.md --
---
title: p1
categories: [cat-a]
tags: ['tag-b','tag-a','tag-c']
---
-- content/p2.md --
---
title: p2
categories: [cat-a]
tags: ['tag-b','tag-a']
---
-- content/p3.md --
---
title: p3
categories: [cat-a]
tags: ['tag-b']
---
-- layouts/_default/term.html --
{{ $s := slice }}
{{ range site.Taxonomies.tags.ByCount }}
  {{ if intersect $.Pages .Pages }}
    {{ $s = $s | append . }}
  {{ end }}
{{ end }}
{{ $s = uniq $s }}

<ul>
  {{ range $s }}
    <li><a href="{{ .Page.RelPermalink }}">{{ .Page.Title }}</a> ({{ .Count }})</li>
  {{ end }}
</ul>
`
	b := Test(t, files)

	b.AssertFileContent("public/categories/cat-a/index.html",
		`<li><a href="/tags/tag-b/">tag-b</a> (3)</li>`,
		`<li><a href="/tags/tag-a/">tag-a</a> (2)</li>`,
		`<li><a href="/tags/tag-c/">tag-c</a> (1)</li>`,
	)
}
```


Discussion / hints:
I would be surprised if this was a general "page collections" issue. I see what's happening on the technical side here (we create these terms on demand now so you end up with different objects). This is fixable, of course.
Yeah, I wasn't quite sure how to zero-in on the description in 15 words or less.
Thank you, @jmooring and @bep !

Fix patch:
diff --git a/common/types/types.go b/common/types/types.go
index 801c511a0fd..e3850ddac88 100644
--- a/common/types/types.go
+++ b/common/types/types.go
@@ -99,6 +99,14 @@ type Unwrapper interface {
 	Unwrapv() any
 }
 
+// Unwrap returns the underlying value of v if it implements Unwrapper, otherwise v is returned.
+func Unwrapv(v any) any {
+	if u, ok := v.(Unwrapper); ok {
+		return u.Unwrapv()
+	}
+	return v
+}
+
 // LowHigh is typically used to represent a slice boundary.
 type LowHigh struct {
 	Low  int
diff --git a/hugolib/page.go b/hugolib/page.go
index c743d42823d..0622a15fe5e 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -36,6 +36,7 @@ import (
 
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/common/types"
 
 	"github.com/gohugoio/hugo/source"
 
@@ -731,3 +732,9 @@ func (p pageWithWeight0) Weight0() int {
 func (p pageWithWeight0) page() page.Page {
 	return p.pageState
 }
+
+var _ types.Unwrapper = (*pageWithWeight0)(nil)
+
+func (p pageWithWeight0) Unwrapv() any {
+	return p.pageState
+}
diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go
index 2107cf5a0eb..edec536ef40 100644
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -617,10 +617,10 @@ type intersector struct {
 }
 
 func (i *intersector) appendIfNotSeen(v reflect.Value) {
-	vi := v.Interface()
-	if !i.seen[vi] {
+	k := normalize(v)
+	if !i.seen[k] {
 		i.r = reflect.Append(i.r, v)
-		i.seen[vi] = true
+		i.seen[k] = true
 	}
 }
 
@@ -638,7 +638,7 @@ func (i *intersector) handleValuePair(l1vv, l2vv reflect.Value) {
 			i.appendIfNotSeen(l1vv)
 		}
 	case kind == reflect.Ptr, kind == reflect.Struct:
-		if l1vv.Interface() == l2vv.Interface() {
+		if types.Unwrapv(l1vv.Interface()) == types.Unwrapv(l2vv.Interface()) {
 			i.appendIfNotSeen(l1vv)
 		}
 	case kind == reflect.Interface:
diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go
index 56909d30e20..4687acdde57 100644
--- a/tpl/collections/reflect_helpers.go
+++ b/tpl/collections/reflect_helpers.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"reflect"
 
+	"github.com/gohugoio/hugo/common/types"
 	"github.com/mitchellh/hashstructure"
 )
 
@@ -60,7 +61,7 @@ func normalize(v reflect.Value) any {
 			return f
 		}
 	}
-	return v.Interface()
+	return types.Unwrapv(v.Interface())
 }
 
 // collects identities from the slices in seqs into a set. Numeric values are normalized,


Test patch:
diff --git a/tpl/collections/collections_integration_test.go b/tpl/collections/collections_integration_test.go
index 1bcabb447ab..3bcd4effbd0 100644
--- a/tpl/collections/collections_integration_test.go
+++ b/tpl/collections/collections_integration_test.go
@@ -230,3 +230,51 @@ boolf = false
 		"false",
 	)
 }
+
+func TestTermEntriesCollectionsIssue12254(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+capitalizeListTitles = false
+disableKinds = ['rss','sitemap']
+-- content/p1.md --
+---
+title: p1
+categories: [cat-a]
+tags: ['tag-b','tag-a','tag-c']
+---
+-- content/p2.md --
+---
+title: p2
+categories: [cat-a]
+tags: ['tag-b','tag-a']
+---
+-- content/p3.md --
+---
+title: p3
+categories: [cat-a]
+tags: ['tag-b']
+---
+-- layouts/_default/term.html --
+{{ $list1 := .Pages }}
+{{ range $i, $e := site.Taxonomies.tags.ByCount }}
+{{ $list2 := .Pages }}
+{{ $i }}: List1: {{ len $list1 }}|
+{{ $i }}: List2: {{ len $list2 }}|
+{{ $i }}: Intersect: {{ intersect $.Pages .Pages | len }}|
+{{ $i }}: Union: {{ union $.Pages .Pages | len }}|
+{{ $i }}: SymDiff: {{ symdiff $.Pages .Pages | len }}|
+{{ $i }}: Uniq: {{ append $.Pages .Pages | uniq | len }}|
+{{ end }}
+
+
+`
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/categories/cat-a/index.html",
+		"0: List1: 3|\n0: List2: 3|\n0: Intersect: 3|\n0: Union: 3|\n0: SymDiff: 0|\n0: Uniq: 3|\n\n\n1: List1: 3|",
+		"1: List2: 2|\n1: Intersect: 2|\n1: Union: 3|\n1: SymDiff: 1|\n1: Uniq: 3|\n\n\n2: List1: 3|\n2: List2: 1|",
+		"2: Intersect: 1|\n2: Union: 3|\n2: SymDiff: 2|\n2: Uniq: 3|",
+	)
+}