From: Bjørn Erik Pedersen Date: Fri, 20 Feb 2026 17:47:25 +0000 (+0100) Subject: hugolib: Fix automatic section pages not replaced by sites.complements X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=a18bec1123e582da818dc8a841285d1a8eb27de4;p=brevno-suite%2Fhugo hugolib: Fix automatic section pages not replaced by sites.complements When a home page or section page in language A is backed by a file and language B has no content file for that section, the automatic page for language B was not replaced by the complement from language A. This happened because: 1. The content node shifter returned auto pages (not backed by files) as exact matches without trying complement fallback. 2. findContentNodeForSiteVector immediately returned auto pages as exact matches without considering file-backed complements. 3. getPagesInSection used Get (no fallback) for IncludeSelf, preventing complement resolution for home/section pages. Fix by preferring file-backed complement pages over auto pages in the shift and lookup mechanisms, and using fallback for self-inclusion. Fixes #14540 Co-Authored-By: Joe Mooring Co-Authored-By: Claude Opus 4.6 --- diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 15b2c58a9..bfab1c8ff 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -388,7 +388,7 @@ func (m *pageMap) getPagesInSection(q pageMapQueryPagesInSection) page.Pages { if err == nil { if q.IncludeSelf { - if n := m.treePages.Get(q.Path); n != nil { + if n := m.treePages.GetWithFallback(q.Path); n != nil { if p, ok := n.(*pageState); ok && include(p) { pas = append(pas, p) } diff --git a/hugolib/content_map_page_contentnode.go b/hugolib/content_map_page_contentnode.go index 220ba630e..4df311d3e 100644 --- a/hugolib/content_map_page_contentnode.go +++ b/hugolib/content_map_page_contentnode.go @@ -223,6 +223,7 @@ func (h helperContentNode) findContentNodeForSiteVector(q sitesmatrix.Vector, fa var ( best contentNodeForSite = nil bestDistance int + bestWeight int ) for n := range candidates { @@ -233,27 +234,41 @@ func (h helperContentNode) findContentNodeForSiteVector(q sitesmatrix.Vector, fa if m := n.(contentNodeLookupContentNodes).lookupContentNodes(q, fallback); m != nil { for nn := range m { vec := nn.siteVector() - if q == vec { - // Exact match. + var w int + if wp, ok := nn.(contentNodeContentWeightProvider); ok { + w = wp.contentWeight() + } + + if q == vec && w > 0 { + // Exact match backed by a file. return nn } distance := q.Distance(vec) + distanceAbs := absint(distance) if best == nil { best = nn bestDistance = distance + bestWeight = w } else { - distanceAbs := absint(distance) bestDistanceAbs := absint(bestDistance) - if distanceAbs < bestDistanceAbs { - // Closer is better. - best = nn - bestDistance = distance - } else if distanceAbs == bestDistanceAbs && distance > 0 { - // Positive distance is better than negative. + if w > 0 && bestWeight == 0 { + // Prefer file-backed pages over auto pages. best = nn bestDistance = distance + bestWeight = w + } else if (w > 0) == (bestWeight > 0) { + // Both file-backed or both auto: use distance. + if distanceAbs < bestDistanceAbs { + best = nn + bestDistance = distance + bestWeight = w + } else if distanceAbs == bestDistanceAbs && distance > 0 { + best = nn + bestDistance = distance + bestWeight = w + } } } } diff --git a/hugolib/content_map_page_contentnodeshifter.go b/hugolib/content_map_page_contentnodeshifter.go index 1f26cddcd..1025e1153 100644 --- a/hugolib/content_map_page_contentnodeshifter.go +++ b/hugolib/content_map_page_contentnodeshifter.go @@ -152,17 +152,31 @@ func (s *contentNodeShifter) Insert(old, new contentNode) (contentNode, contentN } func (s *contentNodeShifter) Shift(n contentNode, siteVector sitesmatrix.Vector, fallback bool) (contentNode, bool) { + var exact contentNode switch v := n.(type) { case contentNodeLookupContentNode: - if vv := v.lookupContentNode(siteVector); vv != nil { - return vv, true - } + exact = v.lookupContentNode(siteVector) default: panic(fmt.Sprintf("Shift: unknown type %T for %q", n, n.Path())) } + if exact != nil { + if !fallback { + return exact, true + } + // If the exact match is backed by a file, return it directly. + if wp, ok := exact.(contentNodeContentWeightProvider); ok && wp.contentWeight() > 0 { + return exact, true + } + // The exact match is an auto page (not backed by a file). + // Check if there's a file-backed complement that should take precedence. + if vvv := cnh.findContentNodeForSiteVector(siteVector, fallback, contentNodeToSeq(n)); vvv != nil { + return vvv, true + } + return exact, true + } + if !fallback { - // Done return nil, false } diff --git a/hugolib/doctree/nodeshifttree.go b/hugolib/doctree/nodeshifttree.go index 8963a610a..0ccf3eba3 100644 --- a/hugolib/doctree/nodeshifttree.go +++ b/hugolib/doctree/nodeshifttree.go @@ -338,6 +338,22 @@ func (r *NodeShiftTree[T]) Get(s string) T { return t } +// GetWithFallback is like Get but uses the fallback/complement mechanism +// when finding a node for the current site vector. +func (r *NodeShiftTree[T]) GetWithFallback(s string) T { + s = cleanKey(s) + v, ok := r.tree.Get(s) + if !ok { + var t T + return t + } + if v, ok := r.shift(v, true); ok { + return v + } + var t T + return t +} + func (r *NodeShiftTree[T]) ForEeachInDimension(s string, dims sitesmatrix.Vector, d int, f func(T) bool) { s = cleanKey(s) v, ok := r.tree.Get(s) diff --git a/hugolib/sitesmatrix/sitematrix_integration_test.go b/hugolib/sitesmatrix/sitematrix_integration_test.go index 674a33950..8706a0a43 100644 --- a/hugolib/sitesmatrix/sitematrix_integration_test.go +++ b/hugolib/sitesmatrix/sitematrix_integration_test.go @@ -1658,3 +1658,90 @@ Version: {{ .Site.Version.Name }}| b.AssertFileContent("public/v1/index.html", "Edited version: v1|") } + +// Issue 14540 +func TestSiteComplementsOverrideAutomaticSectionPages(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['rss', 'sitemap', 'taxonomy', 'term'] +defaultContentLanguageInSubdir = true + +[languages.en] + weight = 1 + +[languages.de] + weight = 2 + +[[module.mounts]] + source = 'content/en' + target = 'content' + [module.mounts.sites.matrix] + languages = ['en'] + [module.mounts.sites.complements] + languages = ['de'] + +[[module.mounts]] + source = 'content/de' + target = 'content' + [module.mounts.sites.matrix] + languages = ['de'] +-- layouts/home.html -- +
    +{{ range $k, $_ := site.Pages -}} +
  • [{{ $k }}] {{ .Title }}
  • +{{ end -}} +
+-- layouts/page.html -- +{{ .Title }}| +-- layouts/section.html -- +{{ .Title }}| +-- content/en/_index.md -- +--- +title: home en +--- +-- content/en/s1/_index.md -- +--- +title: s1 en +--- +-- content/en/s1/p1.md -- +--- +title: p1 en +--- +-- content/en/s2/_index.md -- +--- +title: s2 en +--- +-- content/en/s2/p2.md -- +--- +title: p2 en +--- +-- content/de/s1/p1.md -- +--- +title: p1 de +--- +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/en/index.html", ` + + `) + + b.AssertFileContent("public/de/index.html", ` + + `) +}