Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9747724

Browse files
authored
Fix it so we never auto-fallback to page resources in other roles/versions
This is some logic that's left behind from when we had only one dimension (language) where the common case would be to have one resource set (e.g. an image) and many content translation. After this commit: * For sites matrix defined in the content filename (e.g. data.en.js) or in its mount definition, we may use that as a fallback for e.g. German languages if we don't find a better match. * For content adapters, this is not relevant: Here you must be explicit about this. * We never auto-fallback on resources from a role/version to another. * When a page bundle spans multiple roles (e.g. via roles = "*"), we clone its resources to all roles so each gets role-specific paths. Fixes #14749 Fixes #14752
1 parent a17bdbc commit 9747724

7 files changed

Lines changed: 253 additions & 13 deletions

File tree

hugofs/fileinfo.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,30 @@ func (m *FileMeta) ModulePath() string {
156156
return m.Module.Path()
157157
}
158158

159+
func (m *FileMeta) MatchSiteVectorCoarse(v sitesmatrix.Vector) bool {
160+
language := v.Language()
161+
if !(m.SitesMatrix.HasLanguage(language) || m.SitesComplements.HasLanguage(language)) {
162+
return false
163+
}
164+
165+
return m.MatchSiteVectorCoarseExcludeLanguage(v)
166+
}
167+
168+
func (m *FileMeta) MatchSiteVectorCoarseExcludeLanguage(v sitesmatrix.Vector) bool {
169+
version := v.Version()
170+
if !(m.SitesMatrix.HasVersion(version) || m.SitesComplements.HasVersion(version)) {
171+
return false
172+
}
173+
174+
role := v.Role()
175+
// lint:ignore S1008 preserve the symmetry from above.
176+
if !(m.SitesMatrix.HasRole(role) || m.SitesComplements.HasRole(role)) {
177+
return false
178+
}
179+
180+
return true
181+
}
182+
159183
type FileMetaInfo interface {
160184
fs.DirEntry
161185
MetaProvider

hugolib/content_map.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ func (p *resourceSource) lookupContentNode(v sitesmatrix.Vector) contentNode {
168168

169169
func (p *resourceSource) lookupContentNodes(siteVector sitesmatrix.Vector, fallback bool) iter.Seq[contentNodeForSite] {
170170
if siteVector == p.sv {
171+
found := p.rc == nil
172+
if !found {
173+
// For content adapter resources with explicit sites.matrix config,
174+
// verify the vector is allowed even on exact match,
175+
// since sv is the creating site's vector, not necessarily the target.
176+
// In the exact-match path (siteVector == p.sv), always use strict matching.
177+
// The coarse language-excluding fallback only applies for cross-site matching.
178+
found = p.rc.Sites.Matrix.IsZero() || p.rc.MatchSiteVector(siteVector)
179+
}
180+
if !found {
181+
return nil
182+
}
183+
171184
return func(yield func(n contentNodeForSite) bool) {
172185
yield(p)
173186
}
@@ -184,19 +197,19 @@ func (p *resourceSource) lookupContentNodes(siteVector sitesmatrix.Vector, fallb
184197
}
185198
}
186199

187-
if !found && pc != nil {
188-
if !pc.MatchLanguageCoarse(siteVector) {
189-
return nil
190-
}
191-
if !pc.MatchVersionCoarse(siteVector) {
192-
return nil
193-
}
194-
if !pc.MatchRoleCoarse(siteVector) {
195-
return nil
200+
if !found {
201+
if pc != nil {
202+
// Content adapter resources have explicit site matrix config; respect all dimensions.
203+
found = pc.MatchSiteVectorCoarse(siteVector)
204+
} else if p.fi != nil {
205+
// File-based resources: exclude the language dimension in the coarse match.
206+
// The typical setup is to provide translated content files but one resource (e.g. an image) that is shared between the translations.
207+
// To give all the languages a full set, we ignore that dimension for this last fallback check.
208+
found = p.fi.Meta().MatchSiteVectorCoarseExcludeLanguage(siteVector)
196209
}
197210
}
198211

199-
if !found && !fallback {
212+
if !found {
200213
return nil
201214
}
202215

hugolib/content_map_page.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,14 +561,15 @@ func (m *pageMap) forEachResourceInPage(
561561

562562
func (m *pageMap) getResourcesForPage(ps *pageState) (resource.Resources, error) {
563563
var res resource.Resources
564+
564565
m.forEachResourceInPage(ps, doctree.LockTypeNone, true, nil, func(resourceKey string, n contentNode) (bool, error) {
565566
switch n := n.(type) {
566567
case *resourceSource:
567568
r := n.r
569+
568570
if r == nil {
569571
panic(fmt.Sprintf("getResourcesForPage: resource %q for page %q has no resource, sites matrix %v/%v", resourceKey, ps.Path(), ps.siteVector(), n.sv))
570572
}
571-
572573
res = append(res, r)
573574
case *pageState:
574575
res = append(res, n)

hugolib/content_map_page_assembler.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,38 @@ func (a *allPagesAssembler) doCreatePages(prefix string, depth int) error {
791791
},
792792
)
793793

794+
// Duplicate resources across roles when owner pages share the same source.
795+
// This ensures resources in a page bundle are available to all roles
796+
// that the page spans, avoiding 404 errors in the rendered output.
797+
if len(nodes) > 0 {
798+
forEeachResourceOwnerPage(func(p *pageState) bool {
799+
if _, found := nodes[p.s.siteVector]; found {
800+
return true
801+
}
802+
// Find a resource from another role whose page shares
803+
// the same pageMetaSource (i.e. same mount/content file).
804+
forEeachResourceOwnerPage(func(donor *pageState) bool {
805+
if donor.s.siteVector == p.s.siteVector {
806+
return true
807+
}
808+
if donor.m.pageMetaSource != p.m.pageMetaSource {
809+
return true
810+
}
811+
// Only clone across role differences, not language or version.
812+
if donor.s.siteVector[sitesmatrix.Language] != p.s.siteVector[sitesmatrix.Language] ||
813+
donor.s.siteVector[sitesmatrix.Version] != p.s.siteVector[sitesmatrix.Version] {
814+
return true
815+
}
816+
if rs, ok := nodes[donor.s.siteVector]; ok {
817+
nodes[p.s.siteVector] = rs.(*resourceSource).clone().assignSiteVector(p.s.siteVector)
818+
return false
819+
}
820+
return true
821+
})
822+
return true
823+
})
824+
}
825+
794826
return
795827
}
796828

hugolib/sitesmatrix/dimensions.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ type ToVectorStoreProvider interface {
229229
ToVectorStore() VectorStore
230230
}
231231

232+
// CoarseSiteVectorMatcher is implemented by types that can match a site vector in a coarse way, i.e. it first checks for a
233+
// sites matrix match and then falls back to checking the sites complements.
234+
type CoarseSiteVectorMatcher interface {
235+
MatchSiteVectorCoarse(Vector) bool
236+
MatchSiteVectorCoarseExcludeLanguage(Vector) bool
237+
}
238+
232239
func VectorIteratorToStore(vi VectorIterator) VectorStore {
233240
switch v := vi.(type) {
234241
case VectorStore:

hugolib/sitesmatrix/sitematrix_integration_test.go

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,163 @@ sites:
399399
b.AssertFileContent("public/guest/v1.4.0/en/p2/index.html", "title: EN p2|")
400400
}
401401

402+
func TestContentFilesMountSitesMatrixResourcesVersionsAndLanguages(t *testing.T) {
403+
// The assertions below seems reasonable, but it's also a constructed and very rare corner case
404+
// that's hard to support without adding too much complexity.
405+
// Keep the test for now in case I can come up with something simple.
406+
t.Skip("TODO")
407+
t.Parallel()
408+
409+
filesTemplate := `
410+
-- hugo.toml --
411+
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
412+
defaultContentLanguage = "en"
413+
defaultContentLanguageInSubDir = true
414+
defaultContentVersion = "v1.2.3"
415+
defaultContentVersionInSubDir = true
416+
[languages]
417+
[languages.en]
418+
weight = 1
419+
[languages.nn]
420+
weight = 2
421+
[versions]
422+
[versions."v1.2.3"]
423+
[versions."v2.0.0"]
424+
425+
[[module.mounts]]
426+
source = 'content/v1'
427+
target = 'content'
428+
[module.mounts.sites.matrix]
429+
versions = ["v1.2.*"]
430+
languages = ["*"]
431+
[[module.mounts]]
432+
source = 'content/v2'
433+
target = 'content'
434+
[module.mounts.sites.matrix]
435+
versions = ["v2.0.*"]
436+
languages = ["*"]
437+
-- content/v1/p1/index.en.md --
438+
---
439+
title: "Title English v1"
440+
---
441+
-- content/v2/p1/index.en.md --
442+
---
443+
title: "Title English v2"
444+
---
445+
-- content/v1/p1/index.nn.md --
446+
---
447+
title: "Tittel Nynorsk"
448+
---
449+
-- content/v2/p1/mytext.nn.txt --
450+
Tekst Nynorsk
451+
-- layouts/all.html --
452+
Resources: {{ range .Resources }}{{ .RelPermalink }}|{{ end }}$
453+
`
454+
455+
b := hugolib.Test(t, filesTemplate)
456+
b.AssertFileContent("public/v1.2.3/nn/p1/index.html", "Resources: $")
457+
b.AssertFileContent("public/v1.2.3/en/p1/index.html", "Resources: $")
458+
b.AssertFileContent("public/v2.0.0/en/p1/index.html", "Resources: /v2.0.0/en/p1/mytext.nn.txt|$")
459+
}
460+
461+
func TestFileMountSitesMatrixResourcesRoles(t *testing.T) {
462+
filesTemplate := `
463+
-- hugo.toml --
464+
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
465+
defaultContentRole = "guest"
466+
defaultContentRoleInSubDir = true
467+
[roles]
468+
[roles.guest]
469+
weight = 300
470+
[roles.member]
471+
weight = 200
472+
[mounts]
473+
[[module.mounts]]
474+
source = 'content/guest'
475+
target = 'content'
476+
[module.mounts.sites.matrix]
477+
roles = "guest"
478+
[[module.mounts]]
479+
source = 'content/member'
480+
target = 'content'
481+
[module.mounts.sites.matrix]
482+
roles = "member"
483+
-- layouts/all.html --
484+
{{ .Title }}|{{ .RelPermalink }}|Resources: {{ range .Resources }}{{ .RelPermalink }}|{{ end }}$
485+
-- content/guest/p1/index.md --
486+
---
487+
title: "Guest Gallery"
488+
---
489+
-- content/member/p1/index.md --
490+
---
491+
title: "Member Gallery"
492+
---
493+
-- content/guest/p1/mytext.txt --
494+
Text Guest
495+
-- content/member/p1/mytext2.txt --
496+
Text Member
497+
498+
`
499+
500+
t.Run("Issue 1", func(t *testing.T) {
501+
t.Parallel()
502+
files := strings.Replace(filesTemplate, "content/member/p1/index.md", "content/member/p1_removed/index.md", 1)
503+
files = strings.Replace(files, `roles = "guest"`, `roles = "*"`, 1)
504+
b := hugolib.Test(t, files)
505+
506+
// The current behavior is well intended: We avoid copying the same resources to multiple places.
507+
// But for the typical role use case, this typically leads to 404 errors for shared resources in the member section.
508+
b.AssertFileContent("public/guest/p1/index.html", "Guest Gallery|/guest/p1/|Resources: /guest/p1/mytext.txt|/guest/p1/mytext2.txt|$")
509+
b.AssertFileContent("public/member/p1/index.html", "Guest Gallery|/member/p1/|Resources: /member/p1/mytext.txt|/member/p1/mytext2.txt|$")
510+
})
511+
512+
t.Run("Issue 2", func(t *testing.T) {
513+
t.Parallel()
514+
files := filesTemplate
515+
b := hugolib.Test(t, files)
516+
517+
// This comes from how we handled languages before we added version and role:
518+
// You would typically add 1 image resources and then translate the markdown files to multiple languages.
519+
// To make sure that all languages got a complete set when doing Page.Resources, we pull in missing resources from, in this case, the member section.
520+
// This obviously doesn't work for the role dimension, but it works for the language dimension, and we need to make sure that we don't break that.
521+
b.AssertFileContent("public/guest/p1/index.html", "Guest Gallery|/guest/p1/|Resources: /guest/p1/mytext.txt|$")
522+
b.AssertFileContent("public/member/p1/index.html", "Member Gallery|/member/p1/|Resources: /member/p1/mytext2.txt|$")
523+
})
524+
}
525+
526+
func TestFileMountSitesMatrixResourcesRolesContentAdapter(t *testing.T) {
527+
files := `
528+
-- hugo.toml --
529+
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
530+
defaultContentRole = "guest"
531+
defaultContentRoleInSubDir = true
532+
[roles]
533+
[roles.guest]
534+
weight = 300
535+
[roles.member]
536+
weight = 200
537+
-- layouts/all.html --
538+
{{ .Title }}|{{ .RelPermalink }}|Resources: {{ range .Resources }}{{ .RelPermalink }}|{{ end }}$
539+
-- content/_content.gotmpl --
540+
{{ $guest := dict "roles" "guest" }}
541+
{{ $member := dict "roles" "member" }}
542+
{{ $contentMarkdownGuest := dict "value" "**Guest**" "mediaType" "text/markdown" }}
543+
{{ $contentMarkdownMember := dict "value" "**Member**" "mediaType" "text/markdown" }}
544+
{{ $contentTextGuest:= dict "value" "Guest" "mediaType" "text/plain" }}
545+
{{ $contentTextMember:= dict "value" "Member" "mediaType" "text/plain" }}
546+
{{ .AddPage (dict "path" "p1" "title" "P1 guest" "content" $contentMarkdownGuest "sites" (dict "matrix" $guest )) }}
547+
{{ .AddPage (dict "path" "p1" "title" "P1 member" "content" $contentMarkdownMember "sites" (dict "matrix" $member )) }}
548+
549+
{{ .AddResource (dict "path" "p1/hello1.txt" "title" "Hello guest" "content" $contentTextGuest "sites" (dict "matrix" $guest )) }}
550+
{{ .AddResource (dict "path" "p1/hello2.txt" "title" "Hello member" "content" $contentTextMember "sites" (dict "matrix" $member )) }}
551+
{{ .AddResource (dict "path" "p1/hello3.txt" "title" "Hello member 2" "content" $contentTextMember "sites" (dict "matrix" $member )) }}
552+
553+
`
554+
b := hugolib.Test(t, files)
555+
b.AssertFileContent("public/member/p1/index.html", "P1 member|/member/p1/|Resources: /member/p1/hello2.txt|/member/p1/hello3.txt|$")
556+
b.AssertFileContent("public/guest/p1/index.html", "P1 guest|/guest/p1/|Resources: /guest/p1/hello1.txt|$")
557+
}
558+
402559
func TestGetPageAndRef(t *testing.T) {
403560
t.Parallel()
404561

@@ -1605,8 +1762,6 @@ defaultContentVersionInSubDir = true
16051762
[versions]
16061763
[versions."v1"]
16071764
[versions."v2"]
1608-
1609-
[module]
16101765
[[module.mounts]]
16111766
source = 'content'
16121767
target = 'content'

resources/page/pagemeta/page_frontmatter.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ func (p *SitesMatrixAndComplements) MatchVersionCoarse(siteVector sitesmatrix.Ve
215215
return p.SitesMatrix.HasVersion(i) || p.SitesComplements.HasVersion(i)
216216
}
217217

218+
func (p *SitesMatrixAndComplements) MatchSiteVectorCoarse(siteVector sitesmatrix.Vector) bool {
219+
return p.MatchLanguageCoarse(siteVector) && p.MatchSiteVectorCoarseExcludeLanguage(siteVector)
220+
}
221+
222+
func (p *SitesMatrixAndComplements) MatchSiteVectorCoarseExcludeLanguage(siteVector sitesmatrix.Vector) bool {
223+
return p.MatchRoleCoarse(siteVector) && p.MatchVersionCoarse(siteVector)
224+
}
225+
218226
func DefaultPageConfig() *PageConfigLate {
219227
return &PageConfigLate{
220228
Build: DefaultBuildConfig,

0 commit comments

Comments
 (0)