From 5559263326a68360190aa7fcd4a1008fb5ac1068 Mon Sep 17 00:00:00 2001 From: Joe Mooring Date: Fri, 15 May 2026 09:22:52 -0700 Subject: [PATCH] common/hmaps: Merge slice-valued module config into site config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a module provides a config key whose value is a slice (e.g. cascade or permalinks), and the site config declares the same key as a map with only a merge strategy marker (_merge = 'deep'), the types do not match and Params.merge silently dropped the module's value, leaving the site with no effective cascade or permalink config from the module. Fix Params.merge so that when the destination value is an empty Params (IsZero — only the _merge key is present) and the source value is a non-Params type, the source value is used provided the user-declared merge strategy is not 'none'. This honours the explicit _merge directive regardless of the surrounding shallow-merge context. Closes #13869 Co-authored-by: Claude Sonnet 4.6 --- common/hmaps/params.go | 8 ++++++++ hugolib/cascade_test.go | 27 +++++++++++++++++++++++++++ hugolib/page_permalink_test.go | 30 +++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/common/hmaps/params.go b/common/hmaps/params.go index a67a45ca8..d85287424 100644 --- a/common/hmaps/params.go +++ b/common/hmaps/params.go @@ -120,6 +120,14 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) { if vvv, ok := vv.(Params); ok { if pv, ok := v.(Params); ok { vvv.merge(ms, pv) + } else if vvv.IsZero() { + // The existing value is an empty Params (just merge metadata) + // and the incoming value is a non-Params type (e.g. a slice). + // If the user has set an explicit non-none merge strategy, + // honor it by using the incoming value. + if s, found := vvv.GetMergeStrategy(); found && s != ParamsMergeStrategyNone { + p[k] = v + } } } } else if !noUpdate { diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index fa73c511d..783f86c19 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -544,6 +544,33 @@ cascade: b.AssertFileExists("public/s2/p3/index.html", false) } +// Issue 13869 +func TestCascadeSliceFromModule13869(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +theme = 'foo' +[cascade] +_merge = 'deep' +-- content/_index.md -- +--- +title: home +--- +-- layouts/home.html -- +color: {{ .Params.color }} +-- themes/foo/hugo.toml -- +[[cascade]] +[cascade.params] +color = 'red' +` + + b := Test(t, files) + + b.AssertFileContent("public/index.html", "color: red") +} + // Issue 14848 func TestCascadeParamsLangIssue14848(t *testing.T) { t.Parallel() diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go index 66281b5b5..c2fe8f089 100644 --- a/hugolib/page_permalink_test.go +++ b/hugolib/page_permalink_test.go @@ -75,7 +75,7 @@ canonifyURLs = %t --- title: Page slug: %q -url: %q +url: %q output: ["HTML"] --- `, test.base, test.uglyURLs, test.canonifyURLs, test.file, test.slug, test.url) @@ -173,3 +173,31 @@ Some content. b.AssertFileContent("public/myblog/p2/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p2/|Permalink: https://example.com/myblog/p2/|") b.AssertFileContent("public/myblog/p3/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p3/|Permalink: https://example.com/myblog/p3/|") } + +// Issue 13869 +func TestPermalinksSliceFromModule13869(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +theme = 'foo' +[permalinks] +_merge = 'deep' +-- content/books/my-book.md -- +--- +title: My Book +--- +-- layouts/page.html -- +RelPermalink: {{ .RelPermalink }} +-- themes/foo/hugo.toml -- +[[permalinks]] +pattern = '/shelf/:slug/' +[permalinks.target] +path = '/books/**' +` + + b := Test(t, files) + + b.AssertFileContent("public/shelf/my-book/index.html", "RelPermalink: /shelf/my-book/") +}