Repository: gohugoio/hugo (language: go)

Issue:
`union`, `complement`, `symdiff`, and `intersect` not working with resource collections
### Description

As per the title, the set methods provided for collections do not seem to work with global resources. For example:
```html
{{- $styles := resources.Match "css/**.css" }}
{{- $katex := resources.Match "css/**katex*" }}
{{- $complement := complement $katex $styles }}
```
Here, `$complement` contains all resources from `$styles`, not excluding those also found in `$katex`. The functions `union`, `symdiff`, and `intersect` malfunction in similar ways. `intersect` seems to return an empty collection, irrespective of the resource collections provided.

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.140.0+extended linux/amd64 BuildDate=unknown
</pre>


Discussion / hints:
To clarify, this works fine:

```text
{{ $a := resources.Get "a.jpg" }}
{{ $b := resources.Get "b.jpg" }}
{{ $c := resources.Get "c.jpg" }}
{{ $d := resources.Get "d.jpg" }}

{{ $abcd := slice $a $b $c $d }}
{{ $cd := slice $c $d}}

{{ $abcd | complement $cd }}
```

But these do not:

```text
{{ $match := resources.Match "*" }}
{{ $match | complement $cd }}

{{ $byType := resources.ByType "image"}}
{{ $byType | complement $cd }}
```

The problem occurs when working with the collections created by `resources.Match` and `resources.ByType`.

Fix patch:
diff --git a/resources/resource.go b/resources/resource.go
index 4b81a478a42..7ab10b0ae34 100644
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -47,6 +47,7 @@ var (
 	_ resource.Cloner                    = (*genericResource)(nil)
 	_ resource.ResourcesLanguageMerger   = (*resource.Resources)(nil)
 	_ resource.Identifier                = (*genericResource)(nil)
+	_ resource.TransientIdentifier       = (*genericResource)(nil)
 	_ targetPathProvider                 = (*genericResource)(nil)
 	_ sourcePathProvider                 = (*genericResource)(nil)
 	_ identity.IdentityGroupProvider     = (*genericResource)(nil)
@@ -359,6 +360,9 @@ func GetTestInfoForResource(r resource.Resource) GenericResourceTestInfo {
 type genericResource struct {
 	publishInit *sync.Once
 
+	key     string
+	keyInit *sync.Once
+
 	sd    ResourceSourceDescriptor
 	paths internal.ResourcePaths
 
@@ -444,19 +448,24 @@ func (l *genericResource) Data() any {
 }
 
 func (l *genericResource) Key() string {
-	basePath := l.spec.Cfg.BaseURL().BasePathNoTrailingSlash
-	var key string
-	if basePath == "" {
-		key = l.RelPermalink()
-	} else {
-		key = strings.TrimPrefix(l.RelPermalink(), basePath)
-	}
+	l.keyInit.Do(func() {
+		basePath := l.spec.Cfg.BaseURL().BasePathNoTrailingSlash
+		if basePath == "" {
+			l.key = l.RelPermalink()
+		} else {
+			l.key = strings.TrimPrefix(l.RelPermalink(), basePath)
+		}
 
-	if l.spec.Cfg.IsMultihost() {
-		key = l.spec.Lang() + key
-	}
+		if l.spec.Cfg.IsMultihost() {
+			l.key = l.spec.Lang() + l.key
+		}
+	})
+
+	return l.key
+}
 
-	return key
+func (l *genericResource) TransientKey() string {
+	return l.Key()
 }
 
 func (l *genericResource) targetPath() string {
@@ -623,6 +632,7 @@ func (rc *genericResource) cloneWithUpdates(u *transformationUpdate) (baseResour
 
 func (l genericResource) clone() *genericResource {
 	l.publishInit = &sync.Once{}
+	l.keyInit = &sync.Once{}
 	return &l
 }
 
diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go
index 0fb87f37137..b33750e8033 100644
--- a/resources/resource/resourcetypes.go
+++ b/resources/resource/resourcetypes.go
@@ -170,11 +170,19 @@ type ResourcesLanguageMerger interface {
 
 // Identifier identifies a resource.
 type Identifier interface {
-	// Key is is mostly for internal use and should be considered opaque.
+	// Key is mostly for internal use and should be considered opaque.
 	// This value may change between Hugo versions.
 	Key() string
 }
 
+// TransientIdentifier identifies a transient resource.
+type TransientIdentifier interface {
+	// TransientKey is mostly for internal use and should be considered opaque.
+	// This value is implemented by transient resources where pointers may be short lived and
+	// not suitable for use as a map keys.
+	TransientKey() string
+}
+
 // WeightProvider provides a weight.
 type WeightProvider interface {
 	Weight() int
diff --git a/resources/resource_spec.go b/resources/resource_spec.go
index d50edeb73ce..912a0d786f0 100644
--- a/resources/resource_spec.go
+++ b/resources/resource_spec.go
@@ -187,6 +187,7 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro
 		Staler:      &AtomicStaler{},
 		h:           &resourceHash{},
 		publishInit: &sync.Once{},
+		keyInit:     &sync.Once{},
 		paths:       rp,
 		spec:        r,
 		sd:          rd,
diff --git a/resources/transform.go b/resources/transform.go
index 4214067bddc..c5d24066937 100644
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -52,8 +52,10 @@ var (
 	_ identity.IdentityGroupProvider     = (*resourceAdapterInner)(nil)
 	_ resource.Source                    = (*resourceAdapter)(nil)
 	_ resource.Identifier                = (*resourceAdapter)(nil)
+	_ resource.TransientIdentifier       = (*resourceAdapter)(nil)
 	_ targetPathProvider                 = (*resourceAdapter)(nil)
 	_ sourcePathProvider                 = (*resourceAdapter)(nil)
+	_ resource.Identifier                = (*resourceAdapter)(nil)
 	_ resource.ResourceNameTitleProvider = (*resourceAdapter)(nil)
 	_ resource.WithResourceMetaProvider  = (*resourceAdapter)(nil)
 	_ identity.DependencyManagerProvider = (*resourceAdapter)(nil)
@@ -279,6 +281,10 @@ func (r *resourceAdapter) Key() string {
 	return r.target.(resource.Identifier).Key()
 }
 
+func (r *resourceAdapter) TransientKey() string {
+	return r.Key()
+}
+
 func (r *resourceAdapter) targetPath() string {
 	r.init(false, false)
 	return r.target.(targetPathProvider).targetPath()
diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go
index 4b222be15ca..6b986cbc4ce 100644
--- a/tpl/collections/reflect_helpers.go
+++ b/tpl/collections/reflect_helpers.go
@@ -20,11 +20,12 @@ import (
 
 	"github.com/gohugoio/hugo/common/hashing"
 	"github.com/gohugoio/hugo/common/types"
+	"github.com/gohugoio/hugo/resources/resource"
 )
 
 var (
 	zero      reflect.Value
-	errorType = reflect.TypeOf((*error)(nil)).Elem()
+	errorType = reflect.TypeFor[error]()
 )
 
 func numberToFloat(v reflect.Value) (float64, error) {
@@ -56,7 +57,13 @@ func normalize(v reflect.Value) any {
 			return f
 		}
 	}
-	return types.Unwrapv(v.Interface())
+
+	vv := types.Unwrapv(v.Interface())
+	if ip, ok := vv.(resource.TransientIdentifier); ok {
+		return ip.TransientKey()
+	}
+
+	return vv
 }
 
 // collects identities from the slices in seqs into a set. Numeric values are normalized,
@@ -151,7 +158,6 @@ func convertNumber(v reflect.Value, to reflect.Kind) (reflect.Value, error) {
 		case reflect.Uint64:
 			n = reflect.ValueOf(uint64(i))
 		}
-
 	}
 
 	if !n.IsValid() {


Test patch:
diff --git a/tpl/collections/collections_integration_test.go b/tpl/collections/collections_integration_test.go
index e39493b529b..2aabee03e5c 100644
--- a/tpl/collections/collections_integration_test.go
+++ b/tpl/collections/collections_integration_test.go
@@ -249,3 +249,32 @@ tags: ['tag-b']
 		"2: Intersect: 1|\n2: Union: 3|\n2: SymDiff: 2|\n2: Uniq: 3|",
 	)
 }
+
+// Issue #13181
+func TestUnionResourcesMatch(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+disableKinds = ['rss','sitemap', 'taxonomy', 'term', 'page']
+-- layouts/index.html --
+{{ $a := resources.Match "*a*" }}
+{{ $b := resources.Match "*b*" }}
+{{ $union := $a | union $b }}
+{{ range $i, $e := $union }}
+{{ $i }}: {{ .Name }}
+{{ end }}$
+-- assets/a1.html --
+<div>file1</div>
+-- assets/a2.html --
+<div>file2</div>
+-- assets/a3_b1.html --
+<div>file3</div>
+-- assets/b2.html --
+<div>file4</div>
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContentExact("public/index.html", "0: /a3_b1.html\n\n1: /b2.html\n\n2: /a1.html\n\n3: /a2.html\n$")
+}