key: "cascade",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
- p.c.Cascade, err = page.DecodeCascadeConfig(nil, p.p.Get(d.key))
+ p.c.Cascade, err = page.DecodeCascadeConfig(nil, true, p.p.Get(d.key))
return err
},
},
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
}
pathInfo: pi,
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
- Kind: kinds.KindTerm,
+ PageConfigEarly: pagemeta.PageConfigEarly{
+ Kind: kinds.KindTerm,
+ },
},
},
}
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,
pathInfo: p,
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
- Kind: kinds.KindHome,
+ PageConfigEarly: pagemeta.PageConfigEarly{
+ Kind: kinds.KindHome,
+ },
},
},
}
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,
// 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 {
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()
}
}
// 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
}
}
// 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
}
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.
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
}
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)
}
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|")
+}
import (
"fmt"
"path/filepath"
+ "slices"
"strings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/mitchellh/mapstructure"
- "slices"
)
// A PageMatcher can be used to match a Page with Glob patterns.
}
}
-func DecodeCascadeConfig(logger loggers.Logger, in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, *maps.Ordered[PageMatcher, maps.Params]], error) {
+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]()
if in == nil {
for _, m := range ms {
m = maps.CleanConfigStringMap(m)
- c, err := mapToPageMatcherParamsConfig(m)
+ var (
+ c PageMatcherParamsConfig
+ err error
+ )
+ if handleLegacyFormat {
+ c, err = mapToPageMatcherParamsConfigLegacy(m)
+ } else {
+ c, err = mapToPageMatcherParamsConfig(m)
+ }
if err != nil {
return nil, nil, err
}
}
// DecodeCascade decodes in which could be either a map or a slice of maps.
-func DecodeCascade(logger loggers.Logger, in any) (*maps.Ordered[PageMatcher, maps.Params], error) {
- conf, err := DecodeCascadeConfig(logger, in)
+func DecodeCascade(logger loggers.Logger, handleLegacyFormat bool, in any) (*maps.Ordered[PageMatcher, maps.Params], error) {
+ conf, err := DecodeCascadeConfig(logger, handleLegacyFormat, in)
if err != nil {
return nil, err
}
}
func mapToPageMatcherParamsConfig(m map[string]any) (PageMatcherParamsConfig, error) {
+ var pcfg PageMatcherParamsConfig
+ for k, v := range m {
+ switch strings.ToLower(k) {
+ 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)
+ }
+ 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) {
}
pcfg.Target = target
default:
- // Legacy config.
if pcfg.Params == nil {
pcfg.Params = make(maps.Params)
}
c.Run("mapToPageMatcherParamsConfig", func(c *qt.C) {
fn := func(m map[string]any) PageMatcherParamsConfig {
- v, err := mapToPageMatcherParamsConfig(m)
+ v, err := mapToPageMatcherParamsConfigLegacy(m)
c.Assert(err, qt.IsNil)
return v
}
},
}
- got, err := DecodeCascadeConfig(loggers.NewDefault(), in)
+ got, err := DecodeCascadeConfig(loggers.NewDefault(), true, in)
c.Assert(err, qt.IsNil)
c.Assert(got, qt.IsNotNil)
{Params: maps.Params{"b": string("bv")}, Target: PageMatcher{Kind: "page"}},
})
- got, err = DecodeCascadeConfig(loggers.NewDefault(), nil)
+ got, err = DecodeCascadeConfig(loggers.NewDefault(), true, nil)
c.Assert(err, qt.IsNil)
c.Assert(got, qt.IsNotNil)
}
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
+ "github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/helpers"
return d.Date.IsZero() && d.Lastmod.IsZero() && d.PublishDate.IsZero() && d.ExpiryDate.IsZero()
}
+// Page config that needs to be set early. These cannot be modified by cascade.
+type PageConfigEarly struct {
+ Kind string // The kind of page, e.g. "page", "section", "home" etc. This is usually derived from the content path.
+ Path string // The canonical path to the page, e.g. /sect/mypage. Note: Leading slash, no trailing slash, no extensions or language identifiers.
+ Lang string // The language code for this page. This is usually derived from the module mount or filename.
+ Cascade []map[string]any
+
+ // Content holds the content for this page.
+ Content Source
+}
+
// PageConfig configures a Page, typically from front matter.
// Note that all the top level fields are reserved Hugo keywords.
// Any custom configuration needs to be set in the Params map.
type PageConfig struct {
Dates Dates `json:"-"` // Dates holds the four core dates for this page.
DatesStrings
- Title string // The title of the page.
- LinkTitle string // The link title of the page.
- Type string // The content type of the page.
- Layout string // The layout to use for to render this page.
- Weight int // The weight of the page, used in sorting if set to a non-zero value.
- Kind string // The kind of page, e.g. "page", "section", "home" etc. This is usually derived from the content path.
- Path string // The canonical path to the page, e.g. /sect/mypage. Note: Leading slash, no trailing slash, no extensions or language identifiers.
- Lang string // The language code for this page. This is usually derived from the module mount or filename.
- URL string // The URL to the rendered page, e.g. /sect/mypage.html.
- Slug string // The slug for this page.
- Description string // The description for this page.
- Summary string // The summary for this page.
- Draft bool // Whether or not the content is a draft.
- Headless bool `json:"-"` // Whether or not the page should be rendered.
- IsCJKLanguage bool // Whether or not the content is in a CJK language.
- TranslationKey string // The translation key for this page.
- Keywords []string // The keywords for this page.
- Aliases []string // The aliases for this page.
- Outputs []string // The output formats to render this page in. If not set, the site's configured output formats for this page kind will be used.
+ PageConfigEarly `mapstructure:",squash"`
+ Title string // The title of the page.
+ LinkTitle string // The link title of the page.
+ Type string // The content type of the page.
+ Layout string // The layout to use for to render this page.
+ Weight int // The weight of the page, used in sorting if set to a non-zero value.
+ URL string // The URL to the rendered page, e.g. /sect/mypage.html.
+ Slug string // The slug for this page.
+ Description string // The description for this page.
+ Summary string // The summary for this page.
+ Draft bool // Whether or not the content is a draft.
+ Headless bool `json:"-"` // Whether or not the page should be rendered.
+ IsCJKLanguage bool // Whether or not the content is in a CJK language.
+ TranslationKey string // The translation key for this page.
+ Keywords []string // The keywords for this page.
+ Aliases []string // The aliases for this page.
+ Outputs []string // The output formats to render this page in. If not set, the site's configured output formats for this page kind will be used.
FrontMatterOnlyValues `mapstructure:"-" json:"-"`
- Cascade []map[string]any
Sitemap config.SitemapConfig
Build BuildConfig
Menus any // Can be a string, []string or map[string]any.
// User defined params.
Params maps.Params
- // Content holds the content for this page.
- Content Source
+ // The raw data from the content adapter.
+ // TODO(bep) clean up the ContentAdapterData vs Params.
+ ContentAdapterData map[string]any `mapstructure:"-" json:"-"`
// Compiled values.
CascadeCompiled *maps.Ordered[page.PageMatcher, maps.Params] `mapstructure:"-" json:"-"`
IsFromContentAdapter bool `mapstructure:"-" json:"-"`
}
+func ClonePageConfigForRebuild(p *PageConfig, params map[string]any) *PageConfig {
+ pp := &PageConfig{
+ PageConfigEarly: p.PageConfigEarly,
+ IsFromContentAdapter: p.IsFromContentAdapter,
+ }
+ if pp.IsFromContentAdapter {
+ pp.ContentAdapterData = params
+ } else {
+ pp.Params = params
+ }
+
+ return pp
+}
+
var DefaultPageConfig = PageConfig{
Build: DefaultBuildConfig,
}
return nil
}
-// Compile sets up the page configuration after all fields have been set.
-func (p *PageConfig) Compile(basePath string, pagesFromData bool, ext string, logger loggers.Logger, outputFormats output.Formats, mediaTypes media.Types) error {
+func (p *PageConfig) CompileForPagesFromDataPre(basePath string, logger loggers.Logger, mediaTypes media.Types) error {
// In content adapters, we always get relative paths.
if basePath != "" {
p.Path = path.Join(basePath, p.Path)
if p.Params == nil {
p.Params = make(maps.Params)
- } else if pagesFromData {
- p.Params = maps.PrepareParamsClone(p.Params)
} else {
- maps.PrepareParams(p.Params)
+ p.Params = maps.PrepareParamsClone(p.Params)
}
+ if p.Kind == "" {
+ p.Kind = kinds.KindPage
+ }
+
+ if p.Cascade != nil {
+ cascade, err := page.DecodeCascade(logger, false, p.Cascade)
+ if err != nil {
+ return fmt.Errorf("failed to decode cascade: %w", err)
+ }
+ p.CascadeCompiled = cascade
+ }
+
+ // Note that NormalizePathStringBasic will make sure that we don't preserve the unnormalized path.
+ // We do that when we create pages from the file system; mostly for backward compatibility,
+ // but also because people tend to use use the filename to name their resources (with spaces and all),
+ // and this isn't relevant when creating resources from an API where it's easy to add textual meta data.
+ p.Path = paths.NormalizePathStringBasic(p.Path)
+
+ return p.compilePrePost("", mediaTypes)
+}
+
+func (p *PageConfig) compilePrePost(ext string, mediaTypes media.Types) error {
if p.Content.Markup == "" && p.Content.MediaType == "" {
if ext == "" {
ext = "md"
if p.Content.Markup == "" {
p.Content.Markup = p.ContentMediaType.SubType
}
+ return nil
+}
- if len(p.Outputs) > 0 {
- outFormats, err := outputFormats.GetByNames(p.Outputs...)
- if err != nil {
- return fmt.Errorf("failed to resolve output formats %v: %w", p.Outputs, err)
- } else {
- p.ConfiguredOutputFormats = outFormats
+// Compile sets up the page configuration after all fields have been set.
+func (p *PageConfig) Compile(ext string, logger loggers.Logger, outputFormats output.Formats, mediaTypes media.Types) error {
+ if p.IsFromContentAdapter {
+ if err := mapstructure.WeakDecode(p.ContentAdapterData, p); err != nil {
+ err = fmt.Errorf("failed to decode page map: %w", err)
+ return err
}
+ // Not needed anymore.
+ p.ContentAdapterData = nil
}
- if pagesFromData {
- if p.Kind == "" {
- p.Kind = kinds.KindPage
- }
-
- // Note that NormalizePathStringBasic will make sure that we don't preserve the unnormalized path.
- // We do that when we create pages from the file system; mostly for backward compatibility,
- // but also because people tend to use use the filename to name their resources (with spaces and all),
- // and this isn't relevant when creating resources from an API where it's easy to add textual meta data.
- p.Path = paths.NormalizePathStringBasic(p.Path)
+ if p.Params == nil {
+ p.Params = make(maps.Params)
+ } else {
+ maps.PrepareParams(p.Params)
+ }
+ if err := p.compilePrePost(ext, mediaTypes); err != nil {
+ return err
}
- if p.Cascade != nil {
- cascade, err := page.DecodeCascade(logger, p.Cascade)
+ if len(p.Outputs) > 0 {
+ outFormats, err := outputFormats.GetByNames(p.Outputs...)
if err != nil {
- return fmt.Errorf("failed to decode cascade: %w", err)
+ return fmt.Errorf("failed to resolve output formats %v: %w", p.Outputs, err)
+ } else {
+ p.ConfiguredOutputFormats = outFormats
}
- p.CascadeCompiled = cascade
}
return nil
} {
var pc pagemeta.PageConfig
pc.Content.Markup = test.in
- c.Assert(pc.Compile("", true, "", logger, output.DefaultFormats, media.DefaultTypes), qt.IsNil)
+ c.Assert(pc.Compile("", logger, output.DefaultFormats, media.DefaultTypes), qt.IsNil)
c.Assert(pc.ContentMediaType.Type, qt.Equals, test.expected)
}
}
}
func TestVimeoShortcode(t *testing.T) {
+ t.Skip("Fix me: Upstream API changes")
t.Parallel()
files := `