Fix/implement cascade for content adapters

Fixes #13692
This commit is contained in:
Bjørn Erik Pedersen
2025-05-07 10:40:39 +02:00
parent 9d1d8c8899
commit c745a3e108
11 changed files with 273 additions and 79 deletions

View File

@@ -356,7 +356,7 @@ func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *Buil
Watching: s.Conf.Watching(),
HandlePage: func(pt *pagesfromdata.PagesFromTemplate, pc *pagemeta.PageConfig) error {
s := pt.Site.(*Site)
if err := pc.Compile(pt.GoTmplFi.Meta().PathInfo.Base(), true, "", s.Log, s.conf.OutputFormats.Config, s.conf.MediaTypes.Config); err != nil {
if err := pc.CompileForPagesFromDataPre(pt.GoTmplFi.Meta().PathInfo.Base(), m.s.Log, s.conf.MediaTypes.Config); err != nil {
return err
}

View File

@@ -1695,7 +1695,9 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error {
pathInfo: pi,
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTerm,
PageConfigEarly: pagemeta.PageConfigEarly{
Kind: kinds.KindTerm,
},
},
},
}
@@ -1956,7 +1958,9 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kind,
PageConfigEarly: pagemeta.PageConfigEarly{
Kind: kind,
},
},
},
standaloneOutputFormat: f,
@@ -2082,7 +2086,9 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
pathInfo: p,
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindHome,
PageConfigEarly: pagemeta.PageConfigEarly{
Kind: kinds.KindHome,
},
},
},
}
@@ -2115,7 +2121,9 @@ func (sa *sitePagesAssembler) addMissingTaxonomies() error {
pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"),
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTaxonomy,
PageConfigEarly: pagemeta.PageConfigEarly{
Kind: kinds.KindTaxonomy,
},
},
},
singular: viewName.singular,

View File

@@ -72,12 +72,7 @@ type pageMeta struct {
// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() {
params := xmaps.Clone(m.paramsOriginal)
m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
Kind: m.pageConfig.Kind,
Lang: m.pageConfig.Lang,
Path: m.pageConfig.Path,
Params: params,
}
m.pageMetaParams.pageConfig = pagemeta.ClonePageConfigForRebuild(m.pageMetaParams.pageConfig, params)
}
type pageMetaParams struct {
@@ -94,7 +89,11 @@ type pageMetaParams struct {
func (m *pageMetaParams) init(preserveOriginal bool) {
if preserveOriginal {
m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params)
if m.pageConfig.IsFromContentAdapter {
m.paramsOriginal = xmaps.Clone(m.pageConfig.ContentAdapterData)
} else {
m.paramsOriginal = xmaps.Clone(m.pageConfig.Params)
}
m.cascadeOriginal = m.pageConfig.CascadeCompiled.Clone()
}
}
@@ -254,7 +253,7 @@ func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf
// Check for any cascade define on itself.
if cv, found := frontmatter["cascade"]; found {
var err error
cascade, err := page.DecodeCascade(logger, cv)
cascade, err := page.DecodeCascade(logger, true, cv)
if err != nil {
return err
}
@@ -341,18 +340,29 @@ 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 {
if !k.Matches(ps) {
return true
}
for kk, vv := range v {
if _, found := ps.m.pageConfig.Params[kk]; !found {
ps.m.pageConfig.Params[kk] = vv
if ps.m.pageConfig.IsFromContentAdapter {
if _, found := ps.m.pageConfig.ContentAdapterData[kk]; !found {
ps.m.pageConfig.ContentAdapterData[kk] = vv
}
} else {
if _, found := ps.m.pageConfig.Params[kk]; !found {
ps.m.pageConfig.Params[kk] = vv
}
}
}
return true
})
if err != nil {
return err
}
if err := ps.setMetaPostParams(); err != nil {
return err
}
@@ -398,6 +408,12 @@ func (p *pageState) setMetaPostParams() error {
PathOrTitle: p.pathOrTitle(),
}
if isContentAdapter {
if err := pm.pageConfig.Compile(ext, p.s.Log, p.s.conf.OutputFormats.Config, p.s.conf.MediaTypes.Config); err != nil {
return err
}
}
// Handle the date separately
// TODO(bep) we need to "do more" in this area so this can be split up and
// more easily tested without the Page, but the coupling is strong.
@@ -656,7 +672,7 @@ params:
return err
}
if err := pcfg.Compile("", false, ext, p.s.Log, p.s.conf.OutputFormats.Config, p.s.conf.MediaTypes.Config); err != nil {
if err := pcfg.Compile(ext, p.s.Log, p.s.conf.OutputFormats.Config, p.s.conf.MediaTypes.Config); err != nil {
return err
}

View File

@@ -91,17 +91,20 @@ func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) {
pd := pagemeta.DefaultPageConfig
pd.IsFromContentAdapter = true
pd.ContentAdapterData = m
if err := mapstructure.WeakDecode(m, &pd); err != nil {
return "", fmt.Errorf("failed to decode page map: %w", err)
// The rest will be handled after the cascade is calculated and applied.
if err := mapstructure.WeakDecode(pd.ContentAdapterData, &pd.PageConfigEarly); err != nil {
err = fmt.Errorf("failed to decode page map: %w", err)
return "", err
}
p.p.buildState.NumPagesAdded++
if err := pd.Validate(true); err != nil {
return "", err
}
p.p.buildState.NumPagesAdded++
return "", p.p.HandlePage(p.p, &pd)
}

View File

@@ -821,3 +821,97 @@ outputs:
b.AssertFileExists("public/p4/index.html", true)
b.AssertFileExists("public/p4/index.json", false) // currently returns true
}
func TestContentAdapterOutputsIssue13692(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','home','sitemap','taxonomy','term']
[[cascade]]
outputs = ['html','json']
[cascade.target]
path = '{/s2,/s4}'
-- layouts/section.html --
html: {{ .Title }}
-- layouts/section.json --
json: {{ .Title }}
-- content/s1/_index.md --
---
title: s1
---
-- content/s2/_index.md --
---
title: s2
---
-- content/_content.gotmpl --
{{ $page := dict "path" "s3" "title" "s3" "kind" "section" }}
{{ $.AddPage $page }}
{{ $page := dict "path" "s4" "title" "s4" "kind" "section" }}
{{ $.AddPage $page }}
{{ $page := dict "path" "s5" "title" "s5" "kind" "section" "outputs" (slice "html") }}
{{ $.AddPage $page }}
`
b := hugolib.Test(t, files)
b.AssertFileExists("public/s1/index.html", true)
b.AssertFileExists("public/s1/index.json", false)
b.AssertFileExists("public/s1/index.xml", true)
b.AssertFileExists("public/s2/index.html", true)
b.AssertFileExists("public/s2/index.json", true)
b.AssertFileExists("public/s2/index.xml", false)
b.AssertFileExists("public/s3/index.html", true)
b.AssertFileExists("public/s3/index.json", false)
b.AssertFileExists("public/s3/index.xml", true)
b.AssertFileExists("public/s4/index.html", true)
b.AssertFileExists("public/s4/index.json", true)
b.AssertFileExists("public/s4/index.xml", false)
b.AssertFileExists("public/s5/index.html", true)
b.AssertFileExists("public/s5/index.json", false)
b.AssertFileExists("public/s5/index.xml", false)
}
func TestContentAdapterCascadeBasic(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableLiveReload = true
-- content/_index.md --
---
cascade:
- title: foo
target:
path: "**"
---
-- layouts/all.html --
Title: {{ .Title }}|Content: {{ .Content }}|
-- content/_content.gotmpl --
{{ $content := dict
"mediaType" "text/markdown"
"value" "The _Hunchback of Notre Dame_ was written by Victor Hugo."
}}
{{ $page := dict "path" "s1" "kind" "page" }}
{{ $.AddPage $page }}
{{ $page := dict "path" "s2" "kind" "page" "title" "bar" "content" $content }}
{{ $.AddPage $page }}
`
b := hugolib.TestRunning(t, files)
b.AssertFileContent("public/s1/index.html", "Title: foo|")
b.AssertFileContent("public/s2/index.html", "Title: bar|", "Content: <p>The <em>Hunchback of Notre Dame</em> was written by Victor Hugo.</p>")
b.EditFileReplaceAll("content/_index.md", "foo", "baz").Build()
b.AssertFileContent("public/s1/index.html", "Title: baz|")
}