From eaf5ace30dafc40a8771ef97af0367e655ab0881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 26 May 2025 18:12:32 +0200 Subject: [PATCH] Fix recent regression with cascading of params to content adapters Fixes #13743 --- config/allconfig/allconfig.go | 4 +- hugolib/cascade_test.go | 21 ++++++ hugolib/content_map_page.go | 12 ++-- hugolib/page__meta.go | 29 ++++++--- resources/page/page_matcher.go | 72 ++++++++------------- resources/page/page_matcher_test.go | 8 +-- resources/page/pagemeta/page_frontmatter.go | 8 +-- 7 files changed, 82 insertions(+), 72 deletions(-) diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index d3ee28490..0db0be1d8 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -146,7 +146,7 @@ type Config struct { // The cascade configuration section contains the top level front matter cascade configuration options, // a slice of page matcher and params to apply to those pages. - Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, maps.Params]] `mapstructure:"-"` + Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]] `mapstructure:"-"` // The segments defines segments for the site. Used for partial/segmented builds. Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"` @@ -776,7 +776,7 @@ type Configs struct { } func (c *Configs) Validate(logger loggers.Logger) error { - c.Base.Cascade.Config.Range(func(p page.PageMatcher, params maps.Params) bool { + c.Base.Cascade.Config.Range(func(p page.PageMatcher, cfg page.PageMatcherParamsConfig) bool { page.CheckCascadePattern(logger, p) return true }) diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index d0a6730db..10b47764f 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -917,3 +917,24 @@ title: p2 b.AssertFileExists("public/sx/index.html", true) // failing b.AssertFileExists("public/sx/p2/index.html", true) // failing } + +func TestCascadeGotmplIssue13743(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +[cascade.params] +foo = 'bar' +[cascade.target] +path = '/p1' +-- content/_content.gotmpl -- +{{ .AddPage (dict "title" "p1" "path" "p1") }} +-- layouts/all.html -- +{{ .Title }}|{{ .Params.foo }} +` + + b := Test(t, files) + + b.AssertFileContent("public/p1/index.html", "p1|bar") // actual content is "p1|" +} diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 9e67fbb1b..ef2daca74 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -1412,7 +1412,7 @@ func (sa *sitePagesAssembler) applyAggregates() error { } // Handle cascades first to get any default dates set. - var cascade *maps.Ordered[page.PageMatcher, maps.Params] + var cascade *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] if keyPage == "" { // Home page gets it's cascade from the site config. cascade = sa.conf.Cascade.Config @@ -1424,7 +1424,7 @@ func (sa *sitePagesAssembler) applyAggregates() error { } else { _, data := pw.WalkContext.Data().LongestPrefix(paths.Dir(keyPage)) if data != nil { - cascade = data.(*maps.Ordered[page.PageMatcher, maps.Params]) + cascade = data.(*maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]) } } @@ -1506,11 +1506,11 @@ func (sa *sitePagesAssembler) applyAggregates() error { pageResource := rs.r.(*pageState) relPath := pageResource.m.pathInfo.BaseRel(pageBundle.m.pathInfo) pageResource.m.resourcePath = relPath - var cascade *maps.Ordered[page.PageMatcher, maps.Params] + var cascade *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] // Apply cascade (if set) to the page. _, data := pw.WalkContext.Data().LongestPrefix(resourceKey) if data != nil { - cascade = data.(*maps.Ordered[page.PageMatcher, maps.Params]) + cascade = data.(*maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]) } if err := pageResource.setMetaPost(cascade); err != nil { return false, err @@ -1574,10 +1574,10 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error { const eventName = "dates" if p.Kind() == kinds.KindTerm { - var cascade *maps.Ordered[page.PageMatcher, maps.Params] + var cascade *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] _, data := pw.WalkContext.Data().LongestPrefix(s) if data != nil { - cascade = data.(*maps.Ordered[page.PageMatcher, maps.Params]) + cascade = data.(*maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]) } if err := p.setMetaPost(cascade); err != nil { return false, err diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index e8bce20d1..0d6d22e9a 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -83,8 +83,8 @@ type pageMetaParams struct { // These are only set in watch mode. datesOriginal pagemeta.Dates - paramsOriginal map[string]any // contains the original params as defined in the front matter. - cascadeOriginal *maps.Ordered[page.PageMatcher, maps.Params] // contains the original cascade as defined in the front matter. + paramsOriginal map[string]any // contains the original params as defined in the front matter. + cascadeOriginal *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] // contains the original cascade as defined in the front matter. } func (m *pageMetaParams) init(preserveOriginal bool) { @@ -291,7 +291,7 @@ func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf return nil } -func (ps *pageState) setMetaPost(cascade *maps.Ordered[page.PageMatcher, maps.Params]) error { +func (ps *pageState) setMetaPost(cascade *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]) error { ps.m.setMetaPostCount++ var cascadeHashPre uint64 if ps.m.setMetaPostCount > 1 { @@ -303,15 +303,20 @@ func (ps *pageState) setMetaPost(cascade *maps.Ordered[page.PageMatcher, maps.Pa // Apply cascades first so they can be overridden later. if cascade != nil { if ps.m.pageConfig.CascadeCompiled != nil { - cascade.Range(func(k page.PageMatcher, v maps.Params) bool { + cascade.Range(func(k page.PageMatcher, v page.PageMatcherParamsConfig) bool { vv, found := ps.m.pageConfig.CascadeCompiled.Get(k) if !found { ps.m.pageConfig.CascadeCompiled.Set(k, v) } else { // Merge - for ck, cv := range v { - if _, found := vv[ck]; !found { - vv[ck] = cv + for ck, cv := range v.Params { + if _, found := vv.Params[ck]; !found { + vv.Params[ck] = cv + } + } + for ck, cv := range v.Fields { + if _, found := vv.Fields[ck]; !found { + vv.Fields[ck] = cv } } } @@ -341,11 +346,17 @@ func (ps *pageState) setMetaPost(cascade *maps.Ordered[page.PageMatcher, maps.Pa // Cascade is also applied to itself. var err error - cascade.Range(func(k page.PageMatcher, v maps.Params) bool { + cascade.Range(func(k page.PageMatcher, v page.PageMatcherParamsConfig) bool { if !k.Matches(ps) { return true } - for kk, vv := range v { + for kk, vv := range v.Params { + if _, found := ps.m.pageConfig.Params[kk]; !found { + ps.m.pageConfig.Params[kk] = vv + } + } + + for kk, vv := range v.Fields { if ps.m.pageConfig.IsFromContentAdapter { if _, found := ps.m.pageConfig.ContentAdapterData[kk]; !found { ps.m.pageConfig.ContentAdapterData[kk] = vv diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go index 27a7c7e9e..306c2d03c 100644 --- a/resources/page/page_matcher.go +++ b/resources/page/page_matcher.go @@ -105,9 +105,9 @@ func CheckCascadePattern(logger loggers.Logger, m PageMatcher) { } } -func DecodeCascadeConfig(logger loggers.Logger, handleLegacyFormat bool, in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, *maps.Ordered[PageMatcher, maps.Params]], error) { - buildConfig := func(in any) (*maps.Ordered[PageMatcher, maps.Params], any, error) { - cascade := maps.NewOrdered[PageMatcher, maps.Params]() +func DecodeCascadeConfig(logger loggers.Logger, handleLegacyFormat bool, in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, *maps.Ordered[PageMatcher, PageMatcherParamsConfig]], error) { + buildConfig := func(in any) (*maps.Ordered[PageMatcher, PageMatcherParamsConfig], any, error) { + cascade := maps.NewOrdered[PageMatcher, PageMatcherParamsConfig]() if in == nil { return cascade, []map[string]any{}, nil } @@ -124,11 +124,7 @@ func DecodeCascadeConfig(logger loggers.Logger, handleLegacyFormat bool, in any) c PageMatcherParamsConfig err error ) - if handleLegacyFormat { - c, err = mapToPageMatcherParamsConfigLegacy(m) - } else { - c, err = mapToPageMatcherParamsConfig(m) - } + c, err = mapToPageMatcherParamsConfig(m) if err != nil { return nil, nil, err } @@ -147,23 +143,28 @@ func DecodeCascadeConfig(logger loggers.Logger, handleLegacyFormat bool, in any) if found { // Merge for k, v := range cfg.Params { - if _, found := c[k]; !found { - c[k] = v + if _, found := c.Params[k]; !found { + c.Params[k] = v + } + } + for k, v := range cfg.Fields { + if _, found := c.Fields[k]; !found { + c.Fields[k] = v } } } else { - cascade.Set(m, cfg.Params) + cascade.Set(m, cfg) } } return cascade, cfgs, nil } - return config.DecodeNamespace[[]PageMatcherParamsConfig, *maps.Ordered[PageMatcher, maps.Params]](in, buildConfig) + return config.DecodeNamespace[[]PageMatcherParamsConfig, *maps.Ordered[PageMatcher, PageMatcherParamsConfig]](in, buildConfig) } // DecodeCascade decodes in which could be either a map or a slice of maps. -func DecodeCascade(logger loggers.Logger, handleLegacyFormat bool, in any) (*maps.Ordered[PageMatcher, maps.Params], error) { +func DecodeCascade(logger loggers.Logger, handleLegacyFormat bool, in any) (*maps.Ordered[PageMatcher, PageMatcherParamsConfig], error) { conf, err := DecodeCascadeConfig(logger, handleLegacyFormat, in) if err != nil { return nil, err @@ -181,47 +182,22 @@ func mapToPageMatcherParamsConfig(m map[string]any) (PageMatcherParamsConfig, er return pcfg, err } pcfg.Target = target - default: + case "params": if pcfg.Params == nil { pcfg.Params = make(maps.Params) } - pcfg.Params[k] = v - } - } - return pcfg, pcfg.init() -} - -func mapToPageMatcherParamsConfigLegacy(m map[string]any) (PageMatcherParamsConfig, error) { - var pcfg PageMatcherParamsConfig - for k, v := range m { - switch strings.ToLower(k) { - case "params": - // We simplified the structure of the cascade config in Hugo 0.111.0. - // There is a small chance that someone has used the old structure with the params keyword, - // those values will now be moved to the top level. - // This should be very unlikely as it would lead to constructs like .Params.params.foo, - // and most people see params as an Hugo internal keyword. params := maps.ToStringMap(v) - if pcfg.Params == nil { - pcfg.Params = params - } else { - for k, v := range params { - if _, found := pcfg.Params[k]; !found { - pcfg.Params[k] = v - } + for k, v := range params { + if _, found := pcfg.Params[k]; !found { + pcfg.Params[k] = v } } - case "_target", "target": - var target PageMatcher - if err := decodePageMatcher(v, &target); err != nil { - return pcfg, err - } - pcfg.Target = target default: - if pcfg.Params == nil { - pcfg.Params = make(maps.Params) + if pcfg.Fields == nil { + pcfg.Fields = make(maps.Params) } - pcfg.Params[k] = v + + pcfg.Fields[k] = v } } return pcfg, pcfg.init() @@ -250,10 +226,14 @@ func decodePageMatcher(m any, v *PageMatcher) error { type PageMatcherParamsConfig struct { // Apply Params to all Pages matching Target. Params maps.Params + // Fields holds all fields but Params. + Fields maps.Params + // Target is the PageMatcher that this config applies to. Target PageMatcher } func (p *PageMatcherParamsConfig) init() error { maps.PrepareParams(p.Params) + maps.PrepareParams(p.Fields) return nil } diff --git a/resources/page/page_matcher_test.go b/resources/page/page_matcher_test.go index 2fe6ccc89..ad35da43c 100644 --- a/resources/page/page_matcher_test.go +++ b/resources/page/page_matcher_test.go @@ -84,19 +84,17 @@ func TestPageMatcher(t *testing.T) { c.Run("mapToPageMatcherParamsConfig", func(c *qt.C) { fn := func(m map[string]any) PageMatcherParamsConfig { - v, err := mapToPageMatcherParamsConfigLegacy(m) + v, err := mapToPageMatcherParamsConfig(m) c.Assert(err, qt.IsNil) return v } - // Legacy. c.Assert(fn(map[string]any{"_target": map[string]any{"kind": "page"}, "foo": "bar"}), qt.DeepEquals, PageMatcherParamsConfig{ - Params: maps.Params{ + Fields: maps.Params{ "foo": "bar", }, Target: PageMatcher{Path: "", Kind: "page", Lang: "", Environment: ""}, }) - // Current format. c.Assert(fn(map[string]any{"target": map[string]any{"kind": "page"}, "params": map[string]any{"foo": "bar"}}), qt.DeepEquals, PageMatcherParamsConfig{ Params: maps.Params{ "foo": "bar", @@ -134,7 +132,7 @@ func TestDecodeCascadeConfig(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(got, qt.IsNotNil) c.Assert(got.Config.Keys(), qt.DeepEquals, []PageMatcher{{Kind: "page", Environment: "production"}, {Kind: "page"}}) - c.Assert(got.Config.Values(), qt.DeepEquals, []maps.Params{{"a": string("av")}, {"b": string("bv")}}) + c.Assert(got.SourceStructure, qt.DeepEquals, []PageMatcherParamsConfig{ { Params: maps.Params{"a": string("av")}, diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index 7dea7ca6b..fd4f7759b 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -125,10 +125,10 @@ type PageConfig struct { ContentAdapterData map[string]any `mapstructure:"-" json:"-"` // Compiled values. - CascadeCompiled *maps.Ordered[page.PageMatcher, maps.Params] `mapstructure:"-" json:"-"` - ContentMediaType media.Type `mapstructure:"-" json:"-"` - ConfiguredOutputFormats output.Formats `mapstructure:"-" json:"-"` - IsFromContentAdapter bool `mapstructure:"-" json:"-"` + CascadeCompiled *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] `mapstructure:"-" json:"-"` + ContentMediaType media.Type `mapstructure:"-" json:"-"` + ConfiguredOutputFormats output.Formats `mapstructure:"-" json:"-"` + IsFromContentAdapter bool `mapstructure:"-" json:"-"` } func ClonePageConfigForRebuild(p *PageConfig, params map[string]any) *PageConfig {