]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
hugolib: Fix automatic section pages not replaced by sites.complements
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 20 Feb 2026 17:47:25 +0000 (18:47 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 20 Feb 2026 20:23:20 +0000 (21:23 +0100)
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>
hugolib/content_map_page.go
hugolib/content_map_page_contentnode.go
hugolib/content_map_page_contentnodeshifter.go
hugolib/doctree/nodeshifttree.go
hugolib/sitesmatrix/sitematrix_integration_test.go

index 15b2c58a9e9e6a475d632754f40df041556e0955..bfab1c8ffdc34ad23481fc728b962e77163da9fd 100644 (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)
                                        }
index 220ba630e8ddfd6012a7917f91a302dd7e0029b9..4df311d3e6316a6afe434adbe13bc39632000775 100644 (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
+                                               }
                                        }
                                }
                        }
index 1f26cddcd1e181b3f929f8d89e408ae8920f71b4..1025e11537a6e9c2fcef5b709057b43442742669 100644 (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
        }
 
index 8963a610ad2f2ef0df39ec5c3a271437e04b872b..0ccf3eba3de749df3b4485c1db69da7764387040 100644 (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)
index 674a339509e1e4d30af646308f9bdb4e1cf71ede..8706a0a434f26e79bf2efac83492c5a6e1b61d20 100644 (file)
@@ -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>
+       `)
+}