Repository: gohugoio/hugo (2026-04-15T17:23:44Z)

Issue:
Language/role/version set in filename, e.g. mypage._role_member.md should replace any mount configuration for that dimension
This relates to #14750 -- of solving that issue has made this issue more visible.

If I have this `roles` and `mounts` config:

```toml
[roles]
    [roles.guest]
        weight = 100
    [roles.member]
        weight = 200

[module]
    [[module.mounts]]
        source = 'content/member'
        target = 'content'
        [module.mounts.sites.matrix]
            roles = "member"
    [[module.mounts]]
        source = 'content/guest'
        target = 'content'
        [module.mounts.sites.matrix]
            # This allows the member role to also see the guest content.
            # But note the order of the mounts here, this will be fallback content for members.
            roles = "*"
```

And this in `content/member`:

```
_index.md           20220330_094321.jpg 20220330_094357.jpg 20220330_095331.jpg
```

And this in `content/guest`:

```
_index.md                        20220330_094115._role_member_.jpg 20221020_114303.jpg
```

The current behavior for the bundled resource `20220330_094115._role_guest.jpg` in `content/member` is that:

* It gets assigned all roles from the mount config (`roles = "*"`)
* It gets assigned the `member` role from the filename.

The expected and obviously most useful behaviour would be if any dimension value(s) (language, role or version) in the filename would replace any existing file mount config for that dimension. In the example above `20220330_094115._role_member.jpg` would be assigned the `member` role only.

A note mostly to myself re the above: Any sites matrix definition in front matter will work how we want the filename to work above, to the order of precedence after fixing this would be front matter => filename => file mount, which I think makes the most sense.  





Discussion:
Looks like this was just an omission from the implementation of #14750 -- this was how we have handled language in filenames all the time, which means this isn't fixing a released issue, just what's recently merged to master.



Patch:
diff --git a/hugofs/component_fs.go b/hugofs/component_fs.go
index ec7f06a075a..e59b595c788 100644
--- a/hugofs/component_fs.go
+++ b/hugofs/component_fs.go
@@ -320,8 +320,9 @@ func (fs *componentFs) applyMeta(fi FileNameIsDir, name string) (FileMetaInfo, b
 
 	meta.PathInfo = pi
 	if !fim.IsDir() {
+		pp := fs.opts.Cfg.PathParser()
 		if fileLang := meta.PathInfo.Lang(); fileLang != "" {
-			if idx, ok := fs.opts.Cfg.PathParser().LanguageIndex[fileLang]; ok {
+			if idx, ok := pp.LanguageIndex[fileLang]; ok {
 				// A valid lang set in filename.
 				// Give priority to myfile.sv.txt inside the sv filesystem.
 				meta.Weight++
@@ -330,7 +331,29 @@ func (fs *componentFs) applyMeta(fi FileNameIsDir, name string) (FileMetaInfo, b
 					// Not the default language, add some weight.
 					meta.SitesMatrix = sitesmatrix.NewWeightedVectorStore(meta.SitesMatrix, 10)
 				}
-
+			}
+		}
+		// Filename identifiers for roles/versions replace any mount configuration for that dimension.
+		if roles := pi.Roles(); len(roles) > 0 {
+			var indices []int
+			for _, role := range roles {
+				if idx := pp.ConfiguredDimensions.ConfiguredRoles.ResolveIndex(role); idx >= 0 {
+					indices = append(indices, idx)
+				}
+			}
+			if len(indices) > 0 {
+				meta.SitesMatrix = meta.SitesMatrix.WithRoleIndices(indices...)
+			}
+		}
+		if versions := pi.Versions(); len(versions) > 0 {
+			var indices []int
+			for _, version := range versions {
+				if idx := pp.ConfiguredDimensions.ConfiguredVersions.ResolveIndex(version); idx >= 0 {
+					indices = append(indices, idx)
+				}
+			}
+			if len(indices) > 0 {
+				meta.SitesMatrix = meta.SitesMatrix.WithVersionIndices(indices...)
 			}
 		}
 		switch meta.Component {
@@ -341,7 +364,6 @@ func (fs *componentFs) applyMeta(fi FileNameIsDir, name string) (FileMetaInfo, b
 				meta.Weight--
 			}
 		}
-
 	}
 
 	if fi.IsDir() {
diff --git a/hugolib/sitesmatrix/dimensions.go b/hugolib/sitesmatrix/dimensions.go
index d7a96a03816..aefa6f5e06f 100644
--- a/hugolib/sitesmatrix/dimensions.go
+++ b/hugolib/sitesmatrix/dimensions.go
@@ -214,7 +214,9 @@ type VectorProvider interface {
 type VectorStore interface {
 	VectorProvider
 	Complement(...VectorProvider) VectorStore
-	WithLanguageIndices(i int) VectorStore
+	WithLanguageIndices(i ...int) VectorStore
+	WithVersionIndices(i ...int) VectorStore
+	WithRoleIndices(i ...int) VectorStore
 	HasLanguage(lang int) bool
 	HasVersion(version int) bool
 	HasRole(role int) bool
diff --git a/hugolib/sitesmatrix/vectorstores.go b/hugolib/sitesmatrix/vectorstores.go
index 174dd6ea942..b865bea65bf 100644
--- a/hugolib/sitesmatrix/vectorstores.go
+++ b/hugolib/sitesmatrix/vectorstores.go
@@ -209,17 +209,46 @@ func (s *vectorStoreMap) Vectors() []Vector {
 	return vectors
 }
 
-func (s *vectorStoreMap) WithLanguageIndices(i int) VectorStore {
+func (s *vectorStoreMap) WithLanguageIndices(i ...int) VectorStore {
 	c := s.clone()
 
 	for v := range c.sets {
-		v[Language] = i
-		c.sets[v] = struct{}{}
+		for _, i := range i {
+			nv := v
+			nv[Language] = i
+			c.sets[nv] = struct{}{}
+		}
 	}
 
 	return c
 }
 
+func (s *vectorStoreMap) WithVersionIndices(indices ...int) VectorStore {
+	old := s.clone()
+	c := newVectorStoreMap(len(old.sets) * len(indices))
+	for v := range old.sets {
+		for _, i := range indices {
+			nv := v
+			nv[Version] = i
+			c.sets[nv] = struct{}{}
+		}
+	}
+	return c
+}
+
+func (s *vectorStoreMap) WithRoleIndices(indices ...int) VectorStore {
+	old := s.clone()
+	c := newVectorStoreMap(len(old.sets) * len(indices))
+	for v := range old.sets {
+		for _, i := range indices {
+			nv := v
+			nv[Role] = i
+			c.sets[nv] = struct{}{}
+		}
+	}
+	return c
+}
+
 func (s *vectorStoreMap) HasLanguage(i int) bool {
 	for v := range s.sets {
 		if v.Language() == i {
@@ -589,10 +618,24 @@ func (s IntSets) shallowClone() *IntSets {
 	return &s
 }
 
-// WithLanguageIndices replaces the current language set with a single language index.
-func (s *IntSets) WithLanguageIndices(i int) VectorStore {
+// WithLanguageIndices replaces the current language set with the given language indices.
+func (s *IntSets) WithLanguageIndices(indices ...int) VectorStore {
+	c := s.shallowClone()
+	c.languages = hmaps.NewOrderedIntSet(indices...)
+	return c.init()
+}
+
+// WithVersionIndices replaces the current version set with the given version indices.
+func (s *IntSets) WithVersionIndices(indices ...int) VectorStore {
+	c := s.shallowClone()
+	c.versions = hmaps.NewOrderedIntSet(indices...)
+	return c.init()
+}
+
+// WithRoleIndices replaces the current role set with the given role indices.
+func (s *IntSets) WithRoleIndices(indices ...int) VectorStore {
 	c := s.shallowClone()
-	c.languages = hmaps.NewOrderedIntSet(i)
+	c.roles = hmaps.NewOrderedIntSet(indices...)
 	return c.init()
 }
 


Test patch:
diff --git a/hugolib/sitesmatrix/sitematrix_integration_test.go b/hugolib/sitesmatrix/sitematrix_integration_test.go
index 84908a19048..e76e2b7aa00 100644
--- a/hugolib/sitesmatrix/sitematrix_integration_test.go
+++ b/hugolib/sitesmatrix/sitematrix_integration_test.go
@@ -1901,3 +1901,55 @@ title: p1 de
 </ul>
 	`)
 }
+
+// Issue 14756.
+func TestFilenameIdentifierShouldReplaceMountDimension(t *testing.T) {
+	t.Parallel()
+	files := `
+-- hugo.toml --
+baseURL = "https://example.org/"
+disableKinds = ["taxonomy", "term", "rss", "sitemap"]
+defaultContentLanguage = "en"
+defaultContentRole = "guest"
+defaultContentRoleInSubdir = true
+[roles]
+[roles.guest]
+weight = 100
+[roles.member]
+weight = 200
+[module]
+[[module.mounts]]
+source = 'content/member'
+target = 'content'
+[module.mounts.sites.matrix]
+roles = "member"
+[[module.mounts]]
+source = 'content/guest'
+target = 'content'
+[module.mounts.sites.matrix]
+roles = "*"
+-- content/member/bundle/index.md --
+---
+title: "Bundle"
+---
+-- content/guest/bundle/index.md --
+---
+title: "Bundle"
+---
+-- content/guest/bundle/img._role_member_.txt --
+memberimg
+-- content/guest/bundle/guestimg.txt --
+guestimg
+-- layouts/page.html --
+Role: {{ .Site.Role.Name }}|{{ range .Resources }}{{ .Name }}|{{ end }}$
+-- layouts/list.html --
+List.
+`
+
+	b := hugolib.Test(t, files)
+
+	// The guest role should only see guestimg.txt (not img._role_member_.txt).
+	b.AssertFileContent("public/guest/bundle/index.html", "Role: guest|guestimg.txt|$")
+	// The member role should see both.
+	b.AssertFileContent("public/member/bundle/index.html", "Role: member|guestimg.txt|img._role_member_.txt|$")
+}