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 <joe.mooring@veriphor.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bjørn Erik Pedersen
2026-02-20 18:47:25 +01:00
parent 3c9b41f9ec
commit a18bec1123
5 changed files with 146 additions and 14 deletions
+1 -1
View File
@@ -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)
}
+24 -9
View File
@@ -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
}
}
}
}
+18 -4
View File
@@ -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
}
+16
View File
@@ -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)
@@ -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 --
<ul>
{{ range $k, $_ := site.Pages -}}
<li>[{{ $k }}] <a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ end -}}
</ul>
-- 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", `
<ul>
<li>[0] <a href="/en/">home en</a></li>
<li>[1] <a href="/en/s1/p1/">p1 en</a></li>
<li>[2] <a href="/en/s2/p2/">p2 en</a></li>
<li>[3] <a href="/en/s1/">s1 en</a></li>
<li>[4] <a href="/en/s2/">s2 en</a></li>
</ul>
`)
b.AssertFileContent("public/de/index.html", `
<ul>
<li>[0] <a href="/en/">home en</a></li>
<li>[1] <a href="/de/s1/p1/">p1 de</a></li>
<li>[2] <a href="/en/s2/p2/">p2 en</a></li>
<li>[3] <a href="/en/s1/">s1 en</a></li>
<li>[4] <a href="/en/s2/">s2 en</a></li>
</ul>
`)
}