Skip to content

Commit a18bec1

Browse files
bepjmooringclaude
committed
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 gohugoio#14540 Co-Authored-By: Joe Mooring <[email protected]> Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 3c9b41f commit a18bec1

File tree

5 files changed

+146
-14
lines changed

5 files changed

+146
-14
lines changed

hugolib/content_map_page.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ func (m *pageMap) getPagesInSection(q pageMapQueryPagesInSection) page.Pages {
388388

389389
if err == nil {
390390
if q.IncludeSelf {
391-
if n := m.treePages.Get(q.Path); n != nil {
391+
if n := m.treePages.GetWithFallback(q.Path); n != nil {
392392
if p, ok := n.(*pageState); ok && include(p) {
393393
pas = append(pas, p)
394394
}

hugolib/content_map_page_contentnode.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ func (h helperContentNode) findContentNodeForSiteVector(q sitesmatrix.Vector, fa
223223
var (
224224
best contentNodeForSite = nil
225225
bestDistance int
226+
bestWeight int
226227
)
227228

228229
for n := range candidates {
@@ -233,27 +234,41 @@ func (h helperContentNode) findContentNodeForSiteVector(q sitesmatrix.Vector, fa
233234
if m := n.(contentNodeLookupContentNodes).lookupContentNodes(q, fallback); m != nil {
234235
for nn := range m {
235236
vec := nn.siteVector()
236-
if q == vec {
237-
// Exact match.
237+
var w int
238+
if wp, ok := nn.(contentNodeContentWeightProvider); ok {
239+
w = wp.contentWeight()
240+
}
241+
242+
if q == vec && w > 0 {
243+
// Exact match backed by a file.
238244
return nn
239245
}
240246

241247
distance := q.Distance(vec)
248+
distanceAbs := absint(distance)
242249

243250
if best == nil {
244251
best = nn
245252
bestDistance = distance
253+
bestWeight = w
246254
} else {
247-
distanceAbs := absint(distance)
248255
bestDistanceAbs := absint(bestDistance)
249-
if distanceAbs < bestDistanceAbs {
250-
// Closer is better.
251-
best = nn
252-
bestDistance = distance
253-
} else if distanceAbs == bestDistanceAbs && distance > 0 {
254-
// Positive distance is better than negative.
256+
if w > 0 && bestWeight == 0 {
257+
// Prefer file-backed pages over auto pages.
255258
best = nn
256259
bestDistance = distance
260+
bestWeight = w
261+
} else if (w > 0) == (bestWeight > 0) {
262+
// Both file-backed or both auto: use distance.
263+
if distanceAbs < bestDistanceAbs {
264+
best = nn
265+
bestDistance = distance
266+
bestWeight = w
267+
} else if distanceAbs == bestDistanceAbs && distance > 0 {
268+
best = nn
269+
bestDistance = distance
270+
bestWeight = w
271+
}
257272
}
258273
}
259274
}

hugolib/content_map_page_contentnodeshifter.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,31 @@ func (s *contentNodeShifter) Insert(old, new contentNode) (contentNode, contentN
152152
}
153153

154154
func (s *contentNodeShifter) Shift(n contentNode, siteVector sitesmatrix.Vector, fallback bool) (contentNode, bool) {
155+
var exact contentNode
155156
switch v := n.(type) {
156157
case contentNodeLookupContentNode:
157-
if vv := v.lookupContentNode(siteVector); vv != nil {
158-
return vv, true
159-
}
158+
exact = v.lookupContentNode(siteVector)
160159
default:
161160
panic(fmt.Sprintf("Shift: unknown type %T for %q", n, n.Path()))
162161
}
163162

163+
if exact != nil {
164+
if !fallback {
165+
return exact, true
166+
}
167+
// If the exact match is backed by a file, return it directly.
168+
if wp, ok := exact.(contentNodeContentWeightProvider); ok && wp.contentWeight() > 0 {
169+
return exact, true
170+
}
171+
// The exact match is an auto page (not backed by a file).
172+
// Check if there's a file-backed complement that should take precedence.
173+
if vvv := cnh.findContentNodeForSiteVector(siteVector, fallback, contentNodeToSeq(n)); vvv != nil {
174+
return vvv, true
175+
}
176+
return exact, true
177+
}
178+
164179
if !fallback {
165-
// Done
166180
return nil, false
167181
}
168182

hugolib/doctree/nodeshifttree.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,22 @@ func (r *NodeShiftTree[T]) Get(s string) T {
338338
return t
339339
}
340340

341+
// GetWithFallback is like Get but uses the fallback/complement mechanism
342+
// when finding a node for the current site vector.
343+
func (r *NodeShiftTree[T]) GetWithFallback(s string) T {
344+
s = cleanKey(s)
345+
v, ok := r.tree.Get(s)
346+
if !ok {
347+
var t T
348+
return t
349+
}
350+
if v, ok := r.shift(v, true); ok {
351+
return v
352+
}
353+
var t T
354+
return t
355+
}
356+
341357
func (r *NodeShiftTree[T]) ForEeachInDimension(s string, dims sitesmatrix.Vector, d int, f func(T) bool) {
342358
s = cleanKey(s)
343359
v, ok := r.tree.Get(s)

hugolib/sitesmatrix/sitematrix_integration_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,3 +1658,90 @@ Version: {{ .Site.Version.Name }}|
16581658

16591659
b.AssertFileContent("public/v1/index.html", "Edited version: v1|")
16601660
}
1661+
1662+
// Issue 14540
1663+
func TestSiteComplementsOverrideAutomaticSectionPages(t *testing.T) {
1664+
t.Parallel()
1665+
1666+
files := `
1667+
-- hugo.toml --
1668+
disableKinds = ['rss', 'sitemap', 'taxonomy', 'term']
1669+
defaultContentLanguageInSubdir = true
1670+
1671+
[languages.en]
1672+
weight = 1
1673+
1674+
[languages.de]
1675+
weight = 2
1676+
1677+
[[module.mounts]]
1678+
source = 'content/en'
1679+
target = 'content'
1680+
[module.mounts.sites.matrix]
1681+
languages = ['en']
1682+
[module.mounts.sites.complements]
1683+
languages = ['de']
1684+
1685+
[[module.mounts]]
1686+
source = 'content/de'
1687+
target = 'content'
1688+
[module.mounts.sites.matrix]
1689+
languages = ['de']
1690+
-- layouts/home.html --
1691+
<ul>
1692+
{{ range $k, $_ := site.Pages -}}
1693+
<li>[{{ $k }}] <a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
1694+
{{ end -}}
1695+
</ul>
1696+
-- layouts/page.html --
1697+
{{ .Title }}|
1698+
-- layouts/section.html --
1699+
{{ .Title }}|
1700+
-- content/en/_index.md --
1701+
---
1702+
title: home en
1703+
---
1704+
-- content/en/s1/_index.md --
1705+
---
1706+
title: s1 en
1707+
---
1708+
-- content/en/s1/p1.md --
1709+
---
1710+
title: p1 en
1711+
---
1712+
-- content/en/s2/_index.md --
1713+
---
1714+
title: s2 en
1715+
---
1716+
-- content/en/s2/p2.md --
1717+
---
1718+
title: p2 en
1719+
---
1720+
-- content/de/s1/p1.md --
1721+
---
1722+
title: p1 de
1723+
---
1724+
`
1725+
1726+
b := hugolib.Test(t, files)
1727+
1728+
b.AssertFileContent("public/en/index.html", `
1729+
<ul>
1730+
<li>[0] <a href="/en/">home en</a></li>
1731+
<li>[1] <a href="/en/s1/p1/">p1 en</a></li>
1732+
<li>[2] <a href="/en/s2/p2/">p2 en</a></li>
1733+
<li>[3] <a href="/en/s1/">s1 en</a></li>
1734+
<li>[4] <a href="/en/s2/">s2 en</a></li>
1735+
</ul>
1736+
`)
1737+
1738+
b.AssertFileContent("public/de/index.html", `
1739+
<ul>
1740+
<li>[0] <a href="/en/">home en</a></li>
1741+
<li>[1] <a href="/de/s1/p1/">p1 de</a></li>
1742+
<li>[2] <a href="/en/s2/p2/">p2 en</a></li>
1743+
<li>[3] <a href="/en/s1/">s1 en</a></li>
1744+
<li>[4] <a href="/en/s2/">s2 en</a></li>
1745+
</ul>
1746+
`)
1747+
}

0 commit comments

Comments
 (0)