Add path, kind and lang to content front matter

Note that none of these can be set via cascade (you will get an error)

Fixes #11544
This commit is contained in:
Bjørn Erik Pedersen 2024-01-29 10:02:24 +01:00
parent ec22bb31a8
commit f31a6db797
22 changed files with 707 additions and 429 deletions

2
go.mod
View file

@ -48,7 +48,7 @@ require (
github.com/marekm4/color-extractor v1.2.1 github.com/marekm4/color-extractor v1.2.1
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mitchellh/hashstructure v1.1.0 github.com/mitchellh/hashstructure v1.1.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c
github.com/muesli/smartcrop v0.3.0 github.com/muesli/smartcrop v0.3.0
github.com/niklasfasching/go-org v1.7.0 github.com/niklasfasching/go-org v1.7.0
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5

2
go.sum
View file

@ -359,6 +359,8 @@ github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9km
github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=

View file

@ -187,7 +187,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
if pi.IsContent() { if pi.IsContent() {
// Create the page now as we need it at assemembly time. // Create the page now as we need it at assemembly time.
// The other resources are created if needed. // The other resources are created if needed.
pageResource, err := m.s.h.newPage( pageResource, pi, err := m.s.h.newPage(
&pageMeta{ &pageMeta{
f: source.NewFileInfo(fim), f: source.NewFileInfo(fim),
pathInfo: pi, pathInfo: pi,
@ -197,6 +197,8 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
if err != nil { if err != nil {
return err return err
} }
key = pi.Base()
rs = &resourceSource{r: pageResource} rs = &resourceSource{r: pageResource}
} else { } else {
rs = &resourceSource{path: pi, opener: r, fi: fim} rs = &resourceSource{path: pi, opener: r, fi: fim}
@ -226,7 +228,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
}, },
)) ))
// A content file. // A content file.
p, err := m.s.h.newPage( p, pi, err := m.s.h.newPage(
&pageMeta{ &pageMeta{
f: source.NewFileInfo(fi), f: source.NewFileInfo(fi),
pathInfo: pi, pathInfo: pi,

View file

@ -43,6 +43,7 @@ import (
"github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
) )
@ -97,7 +98,6 @@ type pageMap struct {
cacheContentRendered *dynacache.Partition[string, *resources.StaleValue[contentSummary]] cacheContentRendered *dynacache.Partition[string, *resources.StaleValue[contentSummary]]
cacheContentPlain *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]] cacheContentPlain *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]]
contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]] contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]]
cacheContentSource *dynacache.Partition[string, *resources.StaleValue[[]byte]]
cfg contentMapConfig cfg contentMapConfig
} }
@ -147,7 +147,6 @@ func (t *pageTrees) collectIdentities(key string) []identity.Identity {
// collectIdentitiesSurrounding collects all identities surrounding the given key. // collectIdentitiesSurrounding collects all identities surrounding the given key.
func (t *pageTrees) collectIdentitiesSurrounding(key string, maxSamplesPerTree int) []identity.Identity { func (t *pageTrees) collectIdentitiesSurrounding(key string, maxSamplesPerTree int) []identity.Identity {
// TODO1 test language coverage from this.
ids := t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treePages) ids := t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treePages)
ids = append(ids, t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treeResources)...) ids = append(ids, t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treeResources)...)
return ids return ids
@ -483,7 +482,7 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources
return nil, err return nil, err
} }
if translationKey := ps.m.translationKey; translationKey != "" { if translationKey := ps.m.pageConfig.TranslationKey; translationKey != "" {
// This this should not be a very common case. // This this should not be a very common case.
// Merge in resources from the other languages. // Merge in resources from the other languages.
translatedPages, _ := m.s.h.translationKeyPages.Get(translationKey) translatedPages, _ := m.s.h.translationKeyPages.Get(translationKey)
@ -539,9 +538,9 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources
sort.SliceStable(res, lessFunc) sort.SliceStable(res, lessFunc)
if len(ps.m.resourcesMetadata) > 0 { if len(ps.m.pageConfig.Resources) > 0 {
for i, r := range res { for i, r := range res {
res[i] = resources.CloneWithMetadataIfNeeded(ps.m.resourcesMetadata, r) res[i] = resources.CloneWithMetadataIfNeeded(ps.m.pageConfig.Resources, r)
} }
sort.SliceStable(res, lessFunc) sort.SliceStable(res, lessFunc)
} }
@ -819,7 +818,6 @@ func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *
cacheContentRendered: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentSummary]](mcache, fmt.Sprintf("/cont/ren/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), cacheContentRendered: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentSummary]](mcache, fmt.Sprintf("/cont/ren/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](mcache, fmt.Sprintf("/cont/src/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
cfg: contentMapConfig{ cfg: contentMapConfig{
lang: s.Lang(), lang: s.Lang(),
@ -1215,7 +1213,7 @@ func (sa *sitePagesAssembler) applyAggregates() error {
// Home page gets it's cascade from the site config. // Home page gets it's cascade from the site config.
cascade = sa.conf.Cascade.Config cascade = sa.conf.Cascade.Config
if pageBundle.m.cascade == nil { if pageBundle.m.pageConfig.Cascade == nil {
// Pass the site cascade downwards. // Pass the site cascade downwards.
pw.WalkContext.Data().Insert(keyPage, cascade) pw.WalkContext.Data().Insert(keyPage, cascade)
} }
@ -1227,12 +1225,12 @@ func (sa *sitePagesAssembler) applyAggregates() error {
} }
if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 { if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 {
oldDates := pageBundle.m.dates oldDates := pageBundle.m.pageConfig.Dates
// We need to wait until after the walk to determine if any of the dates have changed. // We need to wait until after the walk to determine if any of the dates have changed.
pw.WalkContext.AddPostHook( pw.WalkContext.AddPostHook(
func() error { func() error {
if oldDates != pageBundle.m.dates { if oldDates != pageBundle.m.pageConfig.Dates {
sa.assembleChanges.Add(pageBundle) sa.assembleChanges.Add(pageBundle)
} }
return nil return nil
@ -1251,11 +1249,12 @@ func (sa *sitePagesAssembler) applyAggregates() error {
const eventName = "dates" const eventName = "dates"
if n.isContentNodeBranch() { if n.isContentNodeBranch() {
if pageBundle.m.cascade != nil { if pageBundle.m.pageConfig.Cascade != nil {
// Pass it down. // Pass it down.
pw.WalkContext.Data().Insert(keyPage, pageBundle.m.cascade) pw.WalkContext.Data().Insert(keyPage, pageBundle.m.pageConfig.Cascade)
} }
wasZeroDates := resource.IsZeroDates(pageBundle.m.dates)
wasZeroDates := pageBundle.m.pageConfig.Dates.IsAllDatesZero()
if wasZeroDates || pageBundle.IsHome() { if wasZeroDates || pageBundle.IsHome() {
pw.WalkContext.AddEventListener(eventName, keyPage, func(e *doctree.Event[contentNodeI]) { pw.WalkContext.AddEventListener(eventName, keyPage, func(e *doctree.Event[contentNodeI]) {
sp, ok := e.Source.(*pageState) sp, ok := e.Source.(*pageState)
@ -1264,15 +1263,15 @@ func (sa *sitePagesAssembler) applyAggregates() error {
} }
if wasZeroDates { if wasZeroDates {
pageBundle.m.dates.UpdateDateAndLastmodIfAfter(sp.m.dates) pageBundle.m.pageConfig.Dates.UpdateDateAndLastmodIfAfter(sp.m.pageConfig.Dates)
} }
if pageBundle.IsHome() { if pageBundle.IsHome() {
if pageBundle.m.dates.Lastmod().After(pageBundle.s.lastmod) { if pageBundle.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) {
pageBundle.s.lastmod = pageBundle.m.dates.Lastmod() pageBundle.s.lastmod = pageBundle.m.pageConfig.Dates.Lastmod
} }
if sp.m.dates.Lastmod().After(pageBundle.s.lastmod) { if sp.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) {
pageBundle.s.lastmod = sp.m.dates.Lastmod() pageBundle.s.lastmod = sp.m.pageConfig.Dates.Lastmod
} }
} }
}) })
@ -1351,9 +1350,9 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error {
p := n.(*pageState) p := n.(*pageState)
if p.Kind() != kinds.KindTerm { if p.Kind() != kinds.KindTerm {
// The other kinds were handled in applyAggregates. // The other kinds were handled in applyAggregates.
if p.m.cascade != nil { if p.m.pageConfig.Cascade != nil {
// Pass it down. // Pass it down.
pw.WalkContext.Data().Insert(s, p.m.cascade) pw.WalkContext.Data().Insert(s, p.m.pageConfig.Cascade)
} }
} }
@ -1388,14 +1387,14 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error {
// Send the date info up the tree. // Send the date info up the tree.
pw.WalkContext.SendEvent(&doctree.Event[contentNodeI]{Source: n, Path: s, Name: eventName}) pw.WalkContext.SendEvent(&doctree.Event[contentNodeI]{Source: n, Path: s, Name: eventName})
if resource.IsZeroDates(p.m.dates) { if p.m.pageConfig.Dates.IsAllDatesZero() {
pw.WalkContext.AddEventListener(eventName, s, func(e *doctree.Event[contentNodeI]) { pw.WalkContext.AddEventListener(eventName, s, func(e *doctree.Event[contentNodeI]) {
sp, ok := e.Source.(*pageState) sp, ok := e.Source.(*pageState)
if !ok { if !ok {
return return
} }
p.m.dates.UpdateDateAndLastmodIfAfter(sp.m.dates) p.m.pageConfig.Dates.UpdateDateAndLastmodIfAfter(sp.m.pageConfig.Dates)
}) })
} }
@ -1443,8 +1442,8 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error {
// This is a little out of place, but is conveniently put here. // This is a little out of place, but is conveniently put here.
// Check if translationKey is set by user. // Check if translationKey is set by user.
// This is to support the manual way of setting the translationKey in front matter. // This is to support the manual way of setting the translationKey in front matter.
if ps.m.translationKey != "" { if ps.m.pageConfig.TranslationKey != "" {
sa.s.h.translationKeyPages.Append(ps.m.translationKey, ps) sa.s.h.translationKeyPages.Append(ps.m.pageConfig.TranslationKey, ps)
} }
if sa.pageMap.cfg.taxonomyTermDisabled { if sa.pageMap.cfg.taxonomyTermDisabled {
@ -1477,9 +1476,13 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error {
singular: viewName.singular, singular: viewName.singular,
s: sa.Site, s: sa.Site,
pathInfo: pi, pathInfo: pi,
kind: kinds.KindTerm, pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTerm,
},
},
} }
n, err := sa.h.newPage(m) n, pi, err := sa.h.newPage(m)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -1524,7 +1527,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
targetPaths := ps.targetPaths() targetPaths := ps.targetPaths()
baseTarget := targetPaths.SubResourceBaseTarget baseTarget := targetPaths.SubResourceBaseTarget
duplicateResourceFiles := true duplicateResourceFiles := true
if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.markup) { if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.pageConfig.Markup) {
duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
} }
@ -1566,7 +1569,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
BasePathTargetPath: baseTarget, BasePathTargetPath: baseTarget,
Name: relPath, Name: relPath,
NameOriginal: relPathOriginal, NameOriginal: relPathOriginal,
LazyPublish: !ps.m.buildConfig.PublishResources, LazyPublish: !ps.m.pageConfig.Build.PublishResources,
} }
r, err := ps.m.s.ResourceSpec.NewResource(rd) r, err := ps.m.s.ResourceSpec.NewResource(rd)
if err != nil { if err != nil {
@ -1631,7 +1634,7 @@ func (sa *sitePagesAssembler) removeShouldNotBuild() error {
case kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy: case kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy:
// We need to keep these for the structure, but disable // We need to keep these for the structure, but disable
// them so they don't get listed/rendered. // them so they don't get listed/rendered.
(&p.m.buildConfig).Disable() (&p.m.pageConfig.Build).Disable()
default: default:
keys = append(keys, key) keys = append(keys, key)
} }
@ -1673,13 +1676,17 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
} }
m := &pageMeta{ m := &pageMeta{
s: s, s: s,
pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix), pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
kind: kind, pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kind,
},
},
standaloneOutputFormat: f, standaloneOutputFormat: f,
} }
p, _ := s.h.newPage(m) p, _, _ := s.h.newPage(m)
tree.InsertIntoValuesDimension(key, p) tree.InsertIntoValuesDimension(key, p)
} }
@ -1756,7 +1763,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
pathInfo: pth, pathInfo: pth,
} }
ps, err := sa.h.newPage(m) ps, pth, err := sa.h.newPage(m)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -1781,9 +1788,13 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
m := &pageMeta{ m := &pageMeta{
s: sa.Site, s: sa.Site,
pathInfo: p, pathInfo: p,
kind: kinds.KindHome, pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindHome,
},
},
} }
n, err := sa.h.newPage(m) n, p, err := sa.h.newPage(m)
if err != nil { if err != nil {
return err return err
} }
@ -1810,10 +1821,14 @@ func (sa *sitePagesAssembler) addMissingTaxonomies() error {
m := &pageMeta{ m := &pageMeta{
s: sa.Site, s: sa.Site,
pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"), pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"),
kind: kinds.KindTaxonomy, pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTaxonomy,
},
},
singular: viewName.singular, singular: viewName.singular,
} }
p, _ := sa.h.newPage(m) p, _, _ := sa.h.newPage(m)
tree.InsertIntoValuesDimension(key, p) tree.InsertIntoValuesDimension(key, p)
} }
} }

View file

@ -26,6 +26,7 @@ import (
"github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/hugolib/doctree" "github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/resources"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
@ -72,6 +73,8 @@ type HugoSites struct {
// Cache for page listings. // Cache for page listings.
cachePages *dynacache.Partition[string, page.Pages] cachePages *dynacache.Partition[string, page.Pages]
// Cache for content sources.
cacheContentSource *dynacache.Partition[string, *resources.StaleValue[[]byte]]
// Before Hugo 0.122.0 we managed all translations in a map using a translationKey // Before Hugo 0.122.0 we managed all translations in a map using a translationKey
// that could be overridden in front matter. // that could be overridden in front matter.

View file

@ -80,6 +80,15 @@ func Test(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder {
return NewIntegrationTestBuilder(cfg).Build() return NewIntegrationTestBuilder(cfg).Build()
} }
// TestE is the same as Test, but returns an error instead of failing the test.
func TestE(t testing.TB, files string, opts ...TestOpt) (*IntegrationTestBuilder, error) {
cfg := IntegrationTestConfig{T: t, TxtarString: files}
for _, o := range opts {
o(&cfg)
}
return NewIntegrationTestBuilder(cfg).BuildE()
}
// TestRunning is a convenience method to create a new IntegrationTestBuilder from some files with Running set to true and run a build. // TestRunning is a convenience method to create a new IntegrationTestBuilder from some files with Running set to true and run a build.
// Deprecated: Use Test with TestOptRunning instead. // Deprecated: Use Test with TestOptRunning instead.
func TestRunning(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder { func TestRunning(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder {

View file

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved. // Copyright 2024 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -27,6 +27,7 @@ import (
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/output/layouts" "github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/related" "github.com/gohugoio/hugo/related"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/tableofcontents"
@ -197,7 +198,7 @@ func (p *pageHeadingsFiltered) page() page.Page {
// For internal use by the related content feature. // For internal use by the related content feature.
func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document { func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document {
r, err := p.content.contentToC(ctx, p.pageOutput.pco) r, err := p.m.content.contentToC(ctx, p.pageOutput.pco)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -313,14 +314,14 @@ func (p *pageState) Pages() page.Pages {
// RawContent returns the un-rendered source content without // RawContent returns the un-rendered source content without
// any leading front matter. // any leading front matter.
func (p *pageState) RawContent() string { func (p *pageState) RawContent() string {
if p.content.parseInfo.itemsStep2 == nil { if p.m.content.pi.itemsStep2 == nil {
return "" return ""
} }
start := p.content.parseInfo.posMainContent start := p.m.content.pi.posMainContent
if start == -1 { if start == -1 {
start = 0 start = 0
} }
source, err := p.content.contentSource() source, err := p.m.content.pi.contentSource(p.m.content)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -332,11 +333,11 @@ func (p *pageState) Resources() resource.Resources {
} }
func (p *pageState) HasShortcode(name string) bool { func (p *pageState) HasShortcode(name string) bool {
if p.content.shortcodeState == nil { if p.m.content.shortcodeState == nil {
return false return false
} }
return p.content.shortcodeState.hasName(name) return p.m.content.shortcodeState.hasName(name)
} }
func (p *pageState) Site() page.Site { func (p *pageState) Site() page.Site {
@ -355,8 +356,8 @@ func (p *pageState) IsTranslated() bool {
// TranslationKey returns the key used to identify a translation of this content. // TranslationKey returns the key used to identify a translation of this content.
func (p *pageState) TranslationKey() string { func (p *pageState) TranslationKey() string {
if p.m.translationKey != "" { if p.m.pageConfig.TranslationKey != "" {
return p.m.translationKey return p.m.pageConfig.TranslationKey
} }
return p.Path() return p.Path()
} }
@ -365,9 +366,9 @@ func (p *pageState) TranslationKey() string {
func (p *pageState) AllTranslations() page.Pages { func (p *pageState) AllTranslations() page.Pages {
key := p.Path() + "/" + "translations-all" key := p.Path() + "/" + "translations-all"
pages, err := p.s.pageMap.getOrCreatePagesFromCache(key, func(string) (page.Pages, error) { pages, err := p.s.pageMap.getOrCreatePagesFromCache(key, func(string) (page.Pages, error) {
if p.m.translationKey != "" { if p.m.pageConfig.TranslationKey != "" {
// translationKey set by user. // translationKey set by user.
pas, _ := p.s.h.translationKeyPages.Get(p.m.translationKey) pas, _ := p.s.h.translationKeyPages.Get(p.m.pageConfig.TranslationKey)
pasc := make(page.Pages, len(pas)) pasc := make(page.Pages, len(pas))
copy(pasc, pas) copy(pasc, pas)
page.SortByLanguage(pasc) page.SortByLanguage(pasc)
@ -534,7 +535,7 @@ var defaultRenderStringOpts = renderStringOpts{
Markup: "", // Will inherit the page's value when not set. Markup: "", // Will inherit the page's value when not set.
} }
func (p *pageMeta) wrapError(err error) error { func (p *pageMeta) wrapError(err error, sourceFs afero.Fs) error {
if err == nil { if err == nil {
panic("wrapError with nil") panic("wrapError with nil")
} }
@ -544,18 +545,18 @@ func (p *pageMeta) wrapError(err error) error {
return fmt.Errorf("%q: %w", p.Path(), err) return fmt.Errorf("%q: %w", p.Path(), err)
} }
return hugofs.AddFileInfoToError(err, p.File().FileInfo(), p.s.SourceSpec.Fs.Source) return hugofs.AddFileInfoToError(err, p.File().FileInfo(), sourceFs)
} }
// wrapError adds some more context to the given error if possible/needed // wrapError adds some more context to the given error if possible/needed
func (p *pageState) wrapError(err error) error { func (p *pageState) wrapError(err error) error {
return p.m.wrapError(err) return p.m.wrapError(err, p.s.h.SourceFs)
} }
func (p *pageState) getContentConverter() converter.Converter { func (p *pageState) getContentConverter() converter.Converter {
var err error var err error
p.contentConverterInit.Do(func() { p.contentConverterInit.Do(func() {
markup := p.m.markup markup := p.m.pageConfig.Markup
if markup == "html" { if markup == "html" {
// Only used for shortcode inner content. // Only used for shortcode inner content.
markup = "markdown" markup = "markdown"
@ -612,7 +613,7 @@ func (p *pageState) posFromInput(input []byte, offset int) text.Position {
} }
func (p *pageState) posOffset(offset int) text.Position { func (p *pageState) posOffset(offset int) text.Position {
return p.posFromInput(p.content.mustSource(), offset) return p.posFromInput(p.m.content.mustSource(), offset)
} }
// shiftToOutputFormat is serialized. The output format idx refers to the // shiftToOutputFormat is serialized. The output format idx refers to the

View file

@ -91,9 +91,6 @@ type pageCommon struct {
layoutDescriptor layouts.LayoutDescriptor layoutDescriptor layouts.LayoutDescriptor
layoutDescriptorInit sync.Once layoutDescriptorInit sync.Once
// The source and the parsed page content.
content *cachedContent
// Set if feature enabled and this is in a Git repo. // Set if feature enabled and this is in a Git repo.
gitInfo source.GitInfo gitInfo source.GitInfo
codeowners []string codeowners []string

View file

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -53,9 +54,8 @@ type pageContentReplacement struct {
source pageparser.Item source pageparser.Item
} }
func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) { func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string) (*contentParseInfo, error) {
var openSource hugio.OpenReadSeekCloser var openSource hugio.OpenReadSeekCloser
var filename string
if m.f != nil { if m.f != nil {
meta := m.f.FileInfo().Meta() meta := m.f.FileInfo().Meta()
openSource = func() (hugio.ReadSeekCloser, error) { openSource = func() (hugio.ReadSeekCloser, error) {
@ -65,6 +65,44 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) {
} }
return r, nil return r, nil
} }
}
if sourceKey == "" {
sourceKey = strconv.Itoa(int(pid))
}
pi := &contentParseInfo{
h: h,
pid: pid,
sourceKey: sourceKey,
openSource: openSource,
}
source, err := pi.contentSource(m)
if err != nil {
return nil, err
}
items, err := pageparser.ParseBytes(
source,
pageparser.Config{},
)
if err != nil {
return nil, err
}
pi.itemsStep1 = items
if err := pi.mapFrontMatter(source); err != nil {
return nil, err
}
return pi, nil
}
func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cachedContent, error) {
var filename string
if m.f != nil {
filename = m.f.Filename() filename = m.f.Filename()
} }
@ -72,15 +110,11 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) {
pm: m.s.pageMap, pm: m.s.pageMap,
StaleInfo: m, StaleInfo: m,
shortcodeState: newShortcodeHandler(filename, m.s), shortcodeState: newShortcodeHandler(filename, m.s),
parseInfo: &contentParseInfo{ pi: pi,
pid: pid, enableEmoji: m.s.conf.EnableEmoji,
},
cacheBaseKey: m.pathInfo.PathNoLang(),
openSource: openSource,
enableEmoji: m.s.conf.EnableEmoji,
} }
source, err := c.contentSource() source, err := c.pi.contentSource(m)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -95,23 +129,25 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) {
type cachedContent struct { type cachedContent struct {
pm *pageMap pm *pageMap
cacheBaseKey string
// The source bytes.
openSource hugio.OpenReadSeekCloser
resource.StaleInfo resource.StaleInfo
shortcodeState *shortcodeHandler shortcodeState *shortcodeHandler
// Parsed content. // Parsed content.
parseInfo *contentParseInfo pi *contentParseInfo
enableEmoji bool enableEmoji bool
} }
type contentParseInfo struct { type contentParseInfo struct {
pid uint64 h *HugoSites
pid uint64
sourceKey string
// The source bytes.
openSource hugio.OpenReadSeekCloser
frontMatter map[string]any frontMatter map[string]any
// Whether the parsed content contains a summary separator. // Whether the parsed content contains a summary separator.
@ -190,25 +226,15 @@ func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte,
} }
func (c *cachedContent) IsZero() bool { func (c *cachedContent) IsZero() bool {
return len(c.parseInfo.itemsStep2) == 0 return len(c.pi.itemsStep2) == 0
} }
func (c *cachedContent) parseContentFile(source []byte) error { func (c *cachedContent) parseContentFile(source []byte) error {
if source == nil || c.openSource == nil { if source == nil || c.pi.openSource == nil {
return nil return nil
} }
items, err := pageparser.ParseBytes( return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState)
source,
pageparser.Config{},
)
if err != nil {
return err
}
c.parseInfo.itemsStep1 = items
return c.parseInfo.mapItems(source, c.shortcodeState)
} }
func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error { func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error {
@ -242,7 +268,49 @@ func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser
return nil return nil
} }
func (rn *contentParseInfo) mapItems( func (rn *contentParseInfo) failMap(source []byte, err error, i pageparser.Item) error {
if fe, ok := err.(herrors.FileError); ok {
return fe
}
pos := posFromInput("", source, i.Pos())
return herrors.NewFileErrorFromPos(err, pos)
}
func (rn *contentParseInfo) mapFrontMatter(source []byte) error {
if len(rn.itemsStep1) == 0 {
return nil
}
iter := pageparser.NewIterator(rn.itemsStep1)
Loop:
for {
it := iter.Next()
switch {
case it.IsFrontMatter():
if err := rn.parseFrontMatter(it, iter, source); err != nil {
return err
}
next := iter.Peek()
if !next.IsDone() {
rn.posMainContent = next.Pos()
}
// Done.
break Loop
case it.IsEOF():
break Loop
case it.IsError():
return rn.failMap(source, it.Err, it)
default:
}
}
return nil
}
func (rn *contentParseInfo) mapItemsAfterFrontMatter(
source []byte, source []byte,
s *shortcodeHandler, s *shortcodeHandler,
) error { ) error {
@ -273,13 +341,7 @@ Loop:
switch { switch {
case it.Type == pageparser.TypeIgnore: case it.Type == pageparser.TypeIgnore:
case it.IsFrontMatter(): case it.IsFrontMatter():
if err := rn.parseFrontMatter(it, iter, source); err != nil { // Ignore.
return err
}
next := iter.Peek()
if !next.IsDone() {
rn.posMainContent = next.Pos()
}
case it.Type == pageparser.TypeLeadSummaryDivider: case it.Type == pageparser.TypeLeadSummaryDivider:
posBody := -1 posBody := -1
f := func(item pageparser.Item) bool { f := func(item pageparser.Item) bool {
@ -347,16 +409,16 @@ Loop:
} }
func (c *cachedContent) mustSource() []byte { func (c *cachedContent) mustSource() []byte {
source, err := c.contentSource() source, err := c.pi.contentSource(c)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return source return source
} }
func (c *cachedContent) contentSource() ([]byte, error) { func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
key := c.cacheBaseKey key := c.sourceKey
v, err := c.pm.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) { v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
b, err := c.readSourceAll() b, err := c.readSourceAll()
if err != nil { if err != nil {
return nil, err return nil, err
@ -365,7 +427,7 @@ func (c *cachedContent) contentSource() ([]byte, error) {
return &resources.StaleValue[[]byte]{ return &resources.StaleValue[[]byte]{
Value: b, Value: b,
IsStaleFunc: func() bool { IsStaleFunc: func() bool {
return c.IsStale() return s.IsStale()
}, },
}, nil }, nil
}) })
@ -376,7 +438,7 @@ func (c *cachedContent) contentSource() ([]byte, error) {
return v.Value, nil return v.Value, nil
} }
func (c *cachedContent) readSourceAll() ([]byte, error) { func (c *contentParseInfo) readSourceAll() ([]byte, error) {
if c.openSource == nil { if c.openSource == nil {
return []byte{}, nil return []byte{}, nil
} }
@ -424,7 +486,7 @@ type contentPlainPlainWords struct {
func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) { func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) {
ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal) ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal)
key := c.cacheBaseKey + "/" + cp.po.f.Name key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := cp.contentRenderedVersion versionv := cp.contentRenderedVersion
v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) { v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) {
@ -447,7 +509,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
}, },
} }
if len(c.parseInfo.itemsStep2) == 0 { if len(c.pi.itemsStep2) == 0 {
// Nothing to do. // Nothing to do.
return rs, nil return rs, nil
} }
@ -501,8 +563,8 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
var result contentSummary // hasVariants bool var result contentSummary // hasVariants bool
if c.parseInfo.hasSummaryDivider { if c.pi.hasSummaryDivider {
isHTML := cp.po.p.m.markup == "html" isHTML := cp.po.p.m.pageConfig.Markup == "html"
if isHTML { if isHTML {
// Use the summary sections as provided by the user. // Use the summary sections as provided by the user.
i := bytes.Index(b, internalSummaryDividerPre) i := bytes.Index(b, internalSummaryDividerPre)
@ -510,7 +572,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
b = b[i+len(internalSummaryDividerPre):] b = b[i+len(internalSummaryDividerPre):]
} else { } else {
summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.markup, b) summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Markup, b)
if err != nil { if err != nil {
cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err) cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err)
} else { } else {
@ -518,7 +580,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
result.summary = helpers.BytesToHTML(summary) result.summary = helpers.BytesToHTML(summary)
} }
} }
result.summaryTruncated = c.parseInfo.summaryTruncated result.summaryTruncated = c.pi.summaryTruncated
} }
result.content = helpers.BytesToHTML(b) result.content = helpers.BytesToHTML(b)
rs.Value = result rs.Value = result
@ -543,11 +605,11 @@ func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutpu
var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback") var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback")
func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) { func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) {
key := c.cacheBaseKey + "/" + cp.po.f.Name key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := cp.contentRenderedVersion versionv := cp.contentRenderedVersion
v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) { v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
source, err := c.contentSource() source, err := c.pi.contentSource(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -572,7 +634,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
} }
if p.s.conf.Internal.Watch { if p.s.conf.Internal.Watch {
for _, s := range cp2.po.p.content.shortcodeState.shortcodes { for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
for _, templ := range s.templs { for _, templ := range s.templs {
cp.trackDependency(templ.(identity.IdentityProvider)) cp.trackDependency(templ.(identity.IdentityProvider))
} }
@ -580,7 +642,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
} }
// Transfer shortcode names so HasShortcode works for shortcodes from included pages. // Transfer shortcode names so HasShortcode works for shortcodes from included pages.
cp.po.p.content.shortcodeState.transferNames(cp2.po.p.content.shortcodeState) cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 { if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
cp.po.p.pageOutputTemplateVariationsState.Add(1) cp.po.p.pageOutputTemplateVariationsState.Add(1)
} }
@ -589,7 +651,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback) ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
var hasVariants bool var hasVariants bool
ct.contentToRender, hasVariants, err = c.parseInfo.contentToRender(ctx, source, ct.contentPlaceholders) ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -598,7 +660,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
p.pageOutputTemplateVariationsState.Add(1) p.pageOutputTemplateVariationsState.Add(1)
} }
isHTML := cp.po.p.m.markup == "html" isHTML := cp.po.p.m.pageConfig.Markup == "html"
if !isHTML { if !isHTML {
createAndSetToC := func(tocProvider converter.TableOfContentsProvider) { createAndSetToC := func(tocProvider converter.TableOfContentsProvider) {
@ -661,7 +723,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
} }
func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) { func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) {
key := c.cacheBaseKey + "/" + cp.po.f.Name key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := cp.contentRenderedVersion versionv := cp.contentRenderedVersion
@ -681,7 +743,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
result.plain = tpl.StripHTML(string(rendered.content)) result.plain = tpl.StripHTML(string(rendered.content))
result.plainWords = strings.Fields(result.plain) result.plainWords = strings.Fields(result.plain)
isCJKLanguage := cp.po.p.m.isCJKLanguage isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
if isCJKLanguage { if isCJKLanguage {
result.wordCount = 0 result.wordCount = 0
@ -711,8 +773,8 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
if rendered.summary != "" { if rendered.summary != "" {
result.summary = rendered.summary result.summary = rendered.summary
result.summaryTruncated = rendered.summaryTruncated result.summaryTruncated = rendered.summaryTruncated
} else if cp.po.p.m.summary != "" { } else if cp.po.p.m.pageConfig.Summary != "" {
b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.summary), false) b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved. // Copyright 2024 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -48,13 +48,11 @@ import (
var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)
type pageMeta struct { type pageMeta struct {
kind string // Page kind.
term string // Set for kind == KindTerm. term string // Set for kind == KindTerm.
singular string // Set for kind == KindTerm and kind == KindTaxonomy. singular string // Set for kind == KindTerm and kind == KindTaxonomy.
resource.Staler resource.Staler
pageMetaParams pageMetaParams
pageMetaFrontMatter pageMetaFrontMatter
// Set for standalone pages, e.g. robotsTXT. // Set for standalone pages, e.g. robotsTXT.
@ -66,13 +64,15 @@ type pageMeta struct {
pathInfo *paths.Path // Always set. This the canonical path to the Page. pathInfo *paths.Path // Always set. This the canonical path to the Page.
f *source.File f *source.File
content *cachedContent // The source and the parsed page content.
s *Site // The site this page belongs to. s *Site // The site this page belongs to.
} }
// Prepare for a rebuild of the data passed in from front matter. // Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() { func (m *pageMeta) setMetaPostPrepareRebuild() {
params := xmaps.Clone[map[string]any](m.paramsOriginal) params := xmaps.Clone[map[string]any](m.paramsOriginal)
m.pageMetaParams.params = params m.pageMetaParams.pageConfig.Params = params
m.pageMetaFrontMatter = pageMetaFrontMatter{} m.pageMetaFrontMatter = pageMetaFrontMatter{}
} }
@ -80,48 +80,28 @@ type pageMetaParams struct {
setMetaPostCount int setMetaPostCount int
setMetaPostCascadeChanged bool setMetaPostCascadeChanged bool
params map[string]any // Params contains configuration defined in the params section of page frontmatter. pageConfig *pagemeta.PageConfig
cascade map[page.PageMatcher]maps.Params // cascade contains default configuration to be cascaded downwards.
// These are only set in watch mode. // These are only set in watch mode.
datesOriginal pageMetaDates datesOriginal pagemeta.Dates
paramsOriginal map[string]any // contains the original params as defined in the front matter. paramsOriginal map[string]any // contains the original params as defined in the front matter.
cascadeOriginal map[page.PageMatcher]maps.Params // contains the original cascade as defined in the front matter. cascadeOriginal map[page.PageMatcher]maps.Params // contains the original cascade as defined in the front matter.
} }
// From page front matter. // From page front matter.
type pageMetaFrontMatter struct { type pageMetaFrontMatter struct {
draft bool // Only published when running with -D flag configuredOutputFormats output.Formats // outputs defiend in front matter.
title string
linkTitle string
summary string
weight int
markup string
contentType string // type in front matter.
isCJKLanguage bool // whether the content is in a CJK language.
layout string
aliases []string
description string
keywords []string
translationKey string // maps to translation(s) of this page.
buildConfig pagemeta.BuildConfig
configuredOutputFormats output.Formats // outputs defiend in front matter.
pageMetaDates // The 4 front matter dates that Hugo cares about.
resourcesMetadata []map[string]any // Raw front matter metadata that is going to be assigned to the page resources.
sitemap config.SitemapConfig // Sitemap overrides from front matter.
urlPaths pagemeta.URLPath
} }
func (m *pageMetaParams) init(preserveOringal bool) { func (m *pageMetaParams) init(preserveOringal bool) {
if preserveOringal { if preserveOringal {
m.paramsOriginal = xmaps.Clone[maps.Params](m.params) m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params)
m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.cascade) m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.Cascade)
} }
} }
func (p *pageMeta) Aliases() []string { func (p *pageMeta) Aliases() []string {
return p.aliases return p.pageConfig.Aliases
} }
func (p *pageMeta) Author() page.Author { func (p *pageMeta) Author() page.Author {
@ -150,8 +130,24 @@ func (p *pageMeta) BundleType() string {
} }
} }
func (p *pageMeta) Date() time.Time {
return p.pageConfig.Date
}
func (p *pageMeta) PublishDate() time.Time {
return p.pageConfig.PublishDate
}
func (p *pageMeta) Lastmod() time.Time {
return p.pageConfig.Lastmod
}
func (p *pageMeta) ExpiryDate() time.Time {
return p.pageConfig.ExpiryDate
}
func (p *pageMeta) Description() string { func (p *pageMeta) Description() string {
return p.description return p.pageConfig.Description
} }
func (p *pageMeta) Lang() string { func (p *pageMeta) Lang() string {
@ -159,7 +155,7 @@ func (p *pageMeta) Lang() string {
} }
func (p *pageMeta) Draft() bool { func (p *pageMeta) Draft() bool {
return p.draft return p.pageConfig.Draft
} }
func (p *pageMeta) File() *source.File { func (p *pageMeta) File() *source.File {
@ -171,20 +167,20 @@ func (p *pageMeta) IsHome() bool {
} }
func (p *pageMeta) Keywords() []string { func (p *pageMeta) Keywords() []string {
return p.keywords return p.pageConfig.Keywords
} }
func (p *pageMeta) Kind() string { func (p *pageMeta) Kind() string {
return p.kind return p.pageConfig.Kind
} }
func (p *pageMeta) Layout() string { func (p *pageMeta) Layout() string {
return p.layout return p.pageConfig.Layout
} }
func (p *pageMeta) LinkTitle() string { func (p *pageMeta) LinkTitle() string {
if p.linkTitle != "" { if p.pageConfig.LinkTitle != "" {
return p.linkTitle return p.pageConfig.LinkTitle
} }
return p.Title() return p.Title()
@ -194,7 +190,7 @@ func (p *pageMeta) Name() string {
if p.resourcePath != "" { if p.resourcePath != "" {
return p.resourcePath return p.resourcePath
} }
if p.kind == kinds.KindTerm { if p.pageConfig.Kind == kinds.KindTerm {
return p.pathInfo.Unmormalized().BaseNameNoIdentifier() return p.pathInfo.Unmormalized().BaseNameNoIdentifier()
} }
return p.Title() return p.Title()
@ -218,7 +214,7 @@ func (p *pageMeta) Param(key any) (any, error) {
} }
func (p *pageMeta) Params() maps.Params { func (p *pageMeta) Params() maps.Params {
return p.params return p.pageConfig.Params
} }
func (p *pageMeta) Path() string { func (p *pageMeta) Path() string {
@ -248,18 +244,18 @@ func (p *pageMeta) Section() string {
} }
func (p *pageMeta) Sitemap() config.SitemapConfig { func (p *pageMeta) Sitemap() config.SitemapConfig {
return p.sitemap return p.pageConfig.Sitemap
} }
func (p *pageMeta) Title() string { func (p *pageMeta) Title() string {
return p.title return p.pageConfig.Title
} }
const defaultContentType = "page" const defaultContentType = "page"
func (p *pageMeta) Type() string { func (p *pageMeta) Type() string {
if p.contentType != "" { if p.pageConfig.Type != "" {
return p.contentType return p.pageConfig.Type
} }
if sect := p.Section(); sect != "" { if sect := p.Section(); sect != "" {
@ -270,36 +266,56 @@ func (p *pageMeta) Type() string {
} }
func (p *pageMeta) Weight() int { func (p *pageMeta) Weight() int {
return p.weight return p.pageConfig.Weight
} }
func (ps *pageState) setMetaPre() error { func (p *pageMeta) setMetaPre(pi *contentParseInfo, conf config.AllProvider) error {
pm := ps.m frontmatter := pi.frontMatter
p := ps
frontmatter := p.content.parseInfo.frontMatter
watching := p.s.watching()
if frontmatter != nil { if frontmatter != nil {
pcfg := p.pageConfig
if pcfg == nil {
panic("pageConfig not set")
}
// Needed for case insensitive fetching of params values // Needed for case insensitive fetching of params values
maps.PrepareParams(frontmatter) maps.PrepareParams(frontmatter)
pm.pageMetaParams.params = frontmatter pcfg.Params = frontmatter
if p.IsNode() { // Check for any cascade define on itself.
// Check for any cascade define on itself. if cv, found := frontmatter["cascade"]; found {
if cv, found := frontmatter["cascade"]; found { var err error
var err error cascade, err := page.DecodeCascade(cv)
cascade, err := page.DecodeCascade(cv) if err != nil {
if err != nil { return err
return err }
} pcfg.Cascade = cascade
pm.pageMetaParams.cascade = cascade }
// Look for path, lang and kind, all of which values we need early on.
if v, found := frontmatter["path"]; found {
pcfg.Path = paths.ToSlashPreserveLeading(cast.ToString(v))
pcfg.Params["path"] = pcfg.Path
}
if v, found := frontmatter["lang"]; found {
lang := strings.ToLower(cast.ToString(v))
if _, ok := conf.PathParser().LanguageIndex[lang]; ok {
pcfg.Lang = lang
pcfg.Params["lang"] = pcfg.Lang
} }
} }
} else if pm.pageMetaParams.params == nil { if v, found := frontmatter["kind"]; found {
pm.pageMetaParams.params = make(maps.Params) s := cast.ToString(v)
if s != "" {
pcfg.Kind = kinds.GetKindMain(s)
if pcfg.Kind == "" {
return fmt.Errorf("unknown kind %q in front matter", s)
}
pcfg.Params["kind"] = pcfg.Kind
}
}
} else if p.pageMetaParams.pageConfig.Params == nil {
p.pageConfig.Params = make(maps.Params)
} }
pm.pageMetaParams.init(watching) p.pageMetaParams.init(conf.Watching())
return nil return nil
} }
@ -308,18 +324,18 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
ps.m.setMetaPostCount++ ps.m.setMetaPostCount++
var cascadeHashPre uint64 var cascadeHashPre uint64
if ps.m.setMetaPostCount > 1 { if ps.m.setMetaPostCount > 1 {
cascadeHashPre = identity.HashUint64(ps.m.cascade) cascadeHashPre = identity.HashUint64(ps.m.pageConfig.Cascade)
ps.m.cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal) ps.m.pageConfig.Cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
} }
// Apply cascades first so they can be overriden later. // Apply cascades first so they can be overriden later.
if cascade != nil { if cascade != nil {
if ps.m.cascade != nil { if ps.m.pageConfig.Cascade != nil {
for k, v := range cascade { for k, v := range cascade {
vv, found := ps.m.cascade[k] vv, found := ps.m.pageConfig.Cascade[k]
if !found { if !found {
ps.m.cascade[k] = v ps.m.pageConfig.Cascade[k] = v
} else { } else {
// Merge // Merge
for ck, cv := range v { for ck, cv := range v {
@ -329,21 +345,21 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
} }
} }
} }
cascade = ps.m.cascade cascade = ps.m.pageConfig.Cascade
} else { } else {
ps.m.cascade = cascade ps.m.pageConfig.Cascade = cascade
} }
} }
if cascade == nil { if cascade == nil {
cascade = ps.m.cascade cascade = ps.m.pageConfig.Cascade
} }
if ps.m.setMetaPostCount > 1 { if ps.m.setMetaPostCount > 1 {
ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.cascade) ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.Cascade)
if !ps.m.setMetaPostCascadeChanged { if !ps.m.setMetaPostCascadeChanged {
// No changes, restore any value that may be changed by aggregation. // No changes, restore any value that may be changed by aggregation.
ps.m.dates = ps.m.datesOriginal.dates ps.m.pageConfig.Dates = ps.m.datesOriginal
return nil return nil
} }
ps.m.setMetaPostPrepareRebuild() ps.m.setMetaPostPrepareRebuild()
@ -356,8 +372,8 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
continue continue
} }
for kk, vv := range v { for kk, vv := range v {
if _, found := ps.m.params[kk]; !found { if _, found := ps.m.pageConfig.Params[kk]; !found {
ps.m.params[kk] = vv ps.m.pageConfig.Params[kk] = vv
} }
} }
} }
@ -371,7 +387,7 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
} }
// Store away any original values that may be changed from aggregation. // Store away any original values that may be changed from aggregation.
ps.m.datesOriginal = ps.m.pageMetaDates ps.m.datesOriginal = ps.m.pageConfig.Dates
return nil return nil
} }
@ -392,13 +408,8 @@ func (p *pageState) setMetaPostParams() error {
gitAuthorDate = p.gitInfo.AuthorDate gitAuthorDate = p.gitInfo.AuthorDate
} }
pm.pageMetaDates = pageMetaDates{}
pm.urlPaths = pagemeta.URLPath{}
descriptor := &pagemeta.FrontMatterDescriptor{ descriptor := &pagemeta.FrontMatterDescriptor{
Params: pm.params, PageConfig: pm.pageConfig,
Dates: &pm.pageMetaDates.dates,
PageURLs: &pm.urlPaths,
BaseFilename: contentBaseName, BaseFilename: contentBaseName,
ModTime: mtime, ModTime: mtime,
GitAuthorDate: gitAuthorDate, GitAuthorDate: gitAuthorDate,
@ -413,16 +424,27 @@ func (p *pageState) setMetaPostParams() error {
p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err) p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
} }
pm.buildConfig, err = pagemeta.DecodeBuildConfig(pm.params["_build"]) var buildConfig any
if v, ok := pm.pageConfig.Params["_build"]; ok {
buildConfig = v
} else {
buildConfig = pm.pageConfig.Params["build"]
}
pm.pageConfig.Build, err = pagemeta.DecodeBuildConfig(buildConfig)
if err != nil { if err != nil {
return err return err
} }
var sitemapSet bool var sitemapSet bool
pcfg := pm.pageConfig
params := pcfg.Params
var draft, published, isCJKLanguage *bool var draft, published, isCJKLanguage *bool
var userParams map[string]any var userParams map[string]any
for k, v := range pm.params { for k, v := range pcfg.Params {
loki := strings.ToLower(k) loki := strings.ToLower(k)
if loki == "params" { if loki == "params" {
@ -431,7 +453,7 @@ func (p *pageState) setMetaPostParams() error {
return err return err
} }
userParams = vv userParams = vv
delete(pm.params, k) delete(pcfg.Params, k)
continue continue
} }
@ -450,43 +472,43 @@ func (p *pageState) setMetaPostParams() error {
switch loki { switch loki {
case "title": case "title":
pm.title = cast.ToString(v) pcfg.Title = cast.ToString(v)
pm.params[loki] = pm.title params[loki] = pcfg.Title
case "linktitle": case "linktitle":
pm.linkTitle = cast.ToString(v) pcfg.LinkTitle = cast.ToString(v)
pm.params[loki] = pm.linkTitle params[loki] = pcfg.LinkTitle
case "summary": case "summary":
pm.summary = cast.ToString(v) pcfg.Summary = cast.ToString(v)
pm.params[loki] = pm.summary params[loki] = pcfg.Summary
case "description": case "description":
pm.description = cast.ToString(v) pcfg.Description = cast.ToString(v)
pm.params[loki] = pm.description params[loki] = pcfg.Description
case "slug": case "slug":
// Don't start or end with a - // Don't start or end with a -
pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-") pcfg.Slug = strings.Trim(cast.ToString(v), "-")
pm.params[loki] = pm.Slug() params[loki] = pm.Slug()
case "url": case "url":
url := cast.ToString(v) url := cast.ToString(v)
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle()) return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle())
} }
pm.urlPaths.URL = url pcfg.URL = url
pm.params[loki] = url params[loki] = url
case "type": case "type":
pm.contentType = cast.ToString(v) pcfg.Type = cast.ToString(v)
pm.params[loki] = pm.contentType params[loki] = pcfg.Type
case "keywords": case "keywords":
pm.keywords = cast.ToStringSlice(v) pcfg.Keywords = cast.ToStringSlice(v)
pm.params[loki] = pm.keywords params[loki] = pcfg.Keywords
case "headless": case "headless":
// Legacy setting for leaf bundles. // Legacy setting for leaf bundles.
// This is since Hugo 0.63 handled in a more general way for all // This is since Hugo 0.63 handled in a more general way for all
// pages. // pages.
isHeadless := cast.ToBool(v) isHeadless := cast.ToBool(v)
pm.params[loki] = isHeadless params[loki] = isHeadless
if p.File().TranslationBaseName() == "index" && isHeadless { if p.File().TranslationBaseName() == "index" && isHeadless {
pm.buildConfig.List = pagemeta.Never pm.pageConfig.Build.List = pagemeta.Never
pm.buildConfig.Render = pagemeta.Never pm.pageConfig.Build.Render = pagemeta.Never
} }
case "outputs": case "outputs":
o := cast.ToStringSlice(v) o := cast.ToStringSlice(v)
@ -501,43 +523,42 @@ func (p *pageState) setMetaPostParams() error {
p.s.Log.Errorf("Failed to resolve output formats: %s", err) p.s.Log.Errorf("Failed to resolve output formats: %s", err)
} else { } else {
pm.configuredOutputFormats = outFormats pm.configuredOutputFormats = outFormats
pm.params[loki] = outFormats params[loki] = outFormats
} }
} }
case "draft": case "draft":
draft = new(bool) draft = new(bool)
*draft = cast.ToBool(v) *draft = cast.ToBool(v)
case "layout": case "layout":
pm.layout = cast.ToString(v) pcfg.Layout = cast.ToString(v)
pm.params[loki] = pm.layout params[loki] = pcfg.Layout
case "markup": case "markup":
pm.markup = cast.ToString(v) pcfg.Markup = cast.ToString(v)
pm.params[loki] = pm.markup params[loki] = pcfg.Markup
case "weight": case "weight":
pm.weight = cast.ToInt(v) pcfg.Weight = cast.ToInt(v)
pm.params[loki] = pm.weight params[loki] = pcfg.Weight
case "aliases": case "aliases":
pm.aliases = cast.ToStringSlice(v) pcfg.Aliases = cast.ToStringSlice(v)
for i, alias := range pm.aliases { for i, alias := range pcfg.Aliases {
if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
return fmt.Errorf("http* aliases not supported: %q", alias) return fmt.Errorf("http* aliases not supported: %q", alias)
} }
pm.aliases[i] = filepath.ToSlash(alias) pcfg.Aliases[i] = filepath.ToSlash(alias)
} }
pm.params[loki] = pm.aliases params[loki] = pcfg.Aliases
case "sitemap": case "sitemap":
p.m.sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.ToStringMap(v)) pcfg.Sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.ToStringMap(v))
if err != nil { if err != nil {
return fmt.Errorf("failed to decode sitemap config in front matter: %s", err) return fmt.Errorf("failed to decode sitemap config in front matter: %s", err)
} }
pm.params[loki] = p.m.sitemap
sitemapSet = true sitemapSet = true
case "iscjklanguage": case "iscjklanguage":
isCJKLanguage = new(bool) isCJKLanguage = new(bool)
*isCJKLanguage = cast.ToBool(v) *isCJKLanguage = cast.ToBool(v)
case "translationkey": case "translationkey":
pm.translationKey = cast.ToString(v) pcfg.TranslationKey = cast.ToString(v)
pm.params[loki] = pm.translationKey params[loki] = pcfg.TranslationKey
case "resources": case "resources":
var resources []map[string]any var resources []map[string]any
handled := true handled := true
@ -563,8 +584,7 @@ func (p *pageState) setMetaPostParams() error {
} }
if handled { if handled {
pm.params[loki] = resources pcfg.Resources = resources
pm.resourcesMetadata = resources
break break
} }
fallthrough fallthrough
@ -586,51 +606,51 @@ func (p *pageState) setMetaPostParams() error {
for i, u := range vv { for i, u := range vv {
a[i] = cast.ToString(u) a[i] = cast.ToString(u)
} }
pm.params[loki] = a params[loki] = a
} else { } else {
pm.params[loki] = vv params[loki] = vv
} }
} else { } else {
pm.params[loki] = []string{} params[loki] = []string{}
} }
default: default:
pm.params[loki] = vv params[loki] = vv
} }
} }
} }
for k, v := range userParams { for k, v := range userParams {
pm.params[strings.ToLower(k)] = v params[strings.ToLower(k)] = v
} }
if !sitemapSet { if !sitemapSet {
pm.sitemap = p.s.conf.Sitemap pcfg.Sitemap = p.s.conf.Sitemap
} }
pm.markup = p.s.ContentSpec.ResolveMarkup(pm.markup) pcfg.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Markup)
if draft != nil && published != nil { if draft != nil && published != nil {
pm.draft = *draft pcfg.Draft = *draft
p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename()) p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
} else if draft != nil { } else if draft != nil {
pm.draft = *draft pcfg.Draft = *draft
} else if published != nil { } else if published != nil {
pm.draft = !*published pcfg.Draft = !*published
} }
pm.params["draft"] = pm.draft params["draft"] = pcfg.Draft
if isCJKLanguage != nil { if isCJKLanguage != nil {
pm.isCJKLanguage = *isCJKLanguage pcfg.IsCJKLanguage = *isCJKLanguage
} else if p.s.conf.HasCJKLanguage && p.content.openSource != nil { } else if p.s.conf.HasCJKLanguage && p.m.content.pi.openSource != nil {
if cjkRe.Match(p.content.mustSource()) { if cjkRe.Match(p.m.content.mustSource()) {
pm.isCJKLanguage = true pcfg.IsCJKLanguage = true
} else { } else {
pm.isCJKLanguage = false pcfg.IsCJKLanguage = false
} }
} }
pm.params["iscjklanguage"] = p.m.isCJKLanguage params["iscjklanguage"] = pcfg.IsCJKLanguage
return nil return nil
} }
@ -643,7 +663,7 @@ func (p *pageMeta) shouldList(global bool) bool {
return false return false
} }
switch p.buildConfig.List { switch p.pageConfig.Build.List {
case pagemeta.Always: case pagemeta.Always:
return true return true
case pagemeta.Never: case pagemeta.Never:
@ -667,56 +687,56 @@ func (p *pageMeta) shouldBeCheckedForMenuDefinitions() bool {
return false return false
} }
return p.kind == kinds.KindHome || p.kind == kinds.KindSection || p.kind == kinds.KindPage return p.pageConfig.Kind == kinds.KindHome || p.pageConfig.Kind == kinds.KindSection || p.pageConfig.Kind == kinds.KindPage
} }
func (p *pageMeta) noRender() bool { func (p *pageMeta) noRender() bool {
return p.buildConfig.Render != pagemeta.Always return p.pageConfig.Build.Render != pagemeta.Always
} }
func (p *pageMeta) noLink() bool { func (p *pageMeta) noLink() bool {
return p.buildConfig.Render == pagemeta.Never return p.pageConfig.Build.Render == pagemeta.Never
} }
func (p *pageMeta) applyDefaultValues() error { func (p *pageMeta) applyDefaultValues() error {
if p.buildConfig.IsZero() { if p.pageConfig.Build.IsZero() {
p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil) p.pageConfig.Build, _ = pagemeta.DecodeBuildConfig(nil)
} }
if !p.s.conf.IsKindEnabled(p.Kind()) { if !p.s.conf.IsKindEnabled(p.Kind()) {
(&p.buildConfig).Disable() (&p.pageConfig.Build).Disable()
} }
if p.markup == "" { if p.pageConfig.Markup == "" {
if p.File() != nil { if p.File() != nil {
// Fall back to file extension // Fall back to file extension
p.markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) p.pageConfig.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
} }
if p.markup == "" { if p.pageConfig.Markup == "" {
p.markup = "markdown" p.pageConfig.Markup = "markdown"
} }
} }
if p.title == "" && p.f == nil { if p.pageConfig.Title == "" && p.f == nil {
switch p.Kind() { switch p.Kind() {
case kinds.KindHome: case kinds.KindHome:
p.title = p.s.Title() p.pageConfig.Title = p.s.Title()
case kinds.KindSection: case kinds.KindSection:
sectionName := p.pathInfo.Unmormalized().BaseNameNoIdentifier() sectionName := p.pathInfo.Unmormalized().BaseNameNoIdentifier()
if p.s.conf.PluralizeListTitles { if p.s.conf.PluralizeListTitles {
sectionName = flect.Pluralize(sectionName) sectionName = flect.Pluralize(sectionName)
} }
p.title = p.s.conf.C.CreateTitle(sectionName) p.pageConfig.Title = p.s.conf.C.CreateTitle(sectionName)
case kinds.KindTerm: case kinds.KindTerm:
if p.term != "" { if p.term != "" {
p.title = p.s.conf.C.CreateTitle(p.term) p.pageConfig.Title = p.s.conf.C.CreateTitle(p.term)
} else { } else {
panic("term not set") panic("term not set")
} }
case kinds.KindTaxonomy: case kinds.KindTaxonomy:
p.title = strings.Replace(p.s.conf.C.CreateTitle(p.pathInfo.Unmormalized().BaseNameNoIdentifier()), "-", " ", -1) p.pageConfig.Title = strings.Replace(p.s.conf.C.CreateTitle(p.pathInfo.Unmormalized().BaseNameNoIdentifier()), "-", " ", -1)
case kinds.KindStatus404: case kinds.KindStatus404:
p.title = "404 Page not found" p.pageConfig.Title = "404 Page not found"
} }
} }
@ -767,7 +787,7 @@ func (m *pageMeta) outputFormats() output.Formats {
} }
func (p *pageMeta) Slug() string { func (p *pageMeta) Slug() string {
return p.urlPaths.Slug return p.pageConfig.Slug
} }
func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any { func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any {
@ -805,26 +825,6 @@ func getParamToLower(m resource.ResourceParamsProvider, key string) any {
return getParam(m, key, true) return getParam(m, key, true)
} }
type pageMetaDates struct {
dates resource.Dates
}
func (d *pageMetaDates) Date() time.Time {
return d.dates.Date()
}
func (d *pageMetaDates) Lastmod() time.Time {
return d.dates.Lastmod()
}
func (d *pageMetaDates) PublishDate() time.Time {
return d.dates.PublishDate()
}
func (d *pageMetaDates) ExpiryDate() time.Time {
return d.dates.ExpiryDate()
}
func (ps *pageState) initLazyProviders() error { func (ps *pageState) initLazyProviders() error {
ps.init.Add(func(ctx context.Context) (any, error) { ps.init.Add(func(ctx context.Context) (any, error) {
pp, err := newPagePaths(ps) pp, err := newPagePaths(ps)

View file

@ -15,45 +15,106 @@ package hugolib
import ( import (
"fmt" "fmt"
"path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
) )
var pageIDCounter atomic.Uint64 var pageIDCounter atomic.Uint64
func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) { func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
if m.pathInfo == nil { m.Staler = &resources.AtomicStaler{}
if m.pageConfig == nil {
m.pageMetaParams = pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Params: maps.Params{},
},
}
}
var sourceKey string
if m.f != nil {
sourceKey = filepath.ToSlash(m.f.Filename())
}
pid := pageIDCounter.Add(1)
pi, err := m.parseFrontMatter(h, pid, sourceKey)
if err != nil {
return nil, nil, err
}
if err := m.setMetaPre(pi, h.Conf); err != nil {
return nil, nil, m.wrapError(err, h.BaseFs.SourceFs)
}
pcfg := m.pageConfig
if pcfg.Path != "" {
s := m.pageConfig.Path
if !paths.HasExt(s) {
var (
isBranch bool
ext string = "md"
)
if pcfg.Kind != "" {
isBranch = kinds.IsBranch(pcfg.Kind)
} else if m.pathInfo != nil {
isBranch = m.pathInfo.IsBranchBundle()
if m.pathInfo.Ext() != "" {
ext = m.pathInfo.Ext()
}
} else if m.f != nil {
pi := m.f.FileInfo().Meta().PathInfo
isBranch = pi.IsBranchBundle()
if pi.Ext() != "" {
ext = pi.Ext()
}
}
if isBranch {
s += "/_index." + ext
} else {
s += "/index." + ext
}
}
m.pathInfo = h.Conf.PathParser().Parse(files.ComponentFolderContent, s)
} else if m.pathInfo == nil {
if m.f != nil { if m.f != nil {
m.pathInfo = m.f.FileInfo().Meta().PathInfo m.pathInfo = m.f.FileInfo().Meta().PathInfo
} }
if m.pathInfo == nil { if m.pathInfo == nil {
panic(fmt.Sprintf("missing pathInfo in %v", m)) panic(fmt.Sprintf("missing pathInfo in %v", m))
} }
} }
m.Staler = &resources.AtomicStaler{}
ps, err := func() (*pageState, error) { ps, err := func() (*pageState, error) {
if m.s == nil { if m.s == nil {
// Identify the Site/language to associate this Page with. // Identify the Site/language to associate this Page with.
var lang string var lang string
if m.f != nil { if pcfg.Lang != "" {
lang = pcfg.Lang
} else if m.f != nil {
meta := m.f.FileInfo().Meta() meta := m.f.FileInfo().Meta()
lang = meta.Lang lang = meta.Lang
m.s = h.Sites[meta.LangIndex] m.s = h.Sites[meta.LangIndex]
} else { } else {
lang = m.pathInfo.Lang() lang = m.pathInfo.Lang()
} }
if lang == "" {
lang = h.Conf.DefaultContentLanguage()
}
var found bool var found bool
for _, ss := range h.Sites { for _, ss := range h.Sites {
if ss.Lang() == lang { if ss.Lang() == lang {
@ -62,51 +123,49 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
break break
} }
} }
if !found { if !found {
return nil, fmt.Errorf("no site found for language %q", lang) return nil, fmt.Errorf("no site found for language %q", lang)
} }
} }
// Identify Page Kind. // Identify Page Kind.
if m.kind == "" { if m.pageConfig.Kind == "" {
m.kind = kinds.KindSection m.pageConfig.Kind = kinds.KindSection
if m.pathInfo.Base() == "/" { if m.pathInfo.Base() == "/" {
m.kind = kinds.KindHome m.pageConfig.Kind = kinds.KindHome
} else if m.pathInfo.IsBranchBundle() { } else if m.pathInfo.IsBranchBundle() {
// A section, taxonomy or term. // A section, taxonomy or term.
tc := m.s.pageMap.cfg.getTaxonomyConfig(m.Path()) tc := m.s.pageMap.cfg.getTaxonomyConfig(m.Path())
if !tc.IsZero() { if !tc.IsZero() {
// Either a taxonomy or a term. // Either a taxonomy or a term.
if tc.pluralTreeKey == m.Path() { if tc.pluralTreeKey == m.Path() {
m.kind = kinds.KindTaxonomy m.pageConfig.Kind = kinds.KindTaxonomy
} else { } else {
m.kind = kinds.KindTerm m.pageConfig.Kind = kinds.KindTerm
} }
} }
} else if m.f != nil { } else if m.f != nil {
m.kind = kinds.KindPage m.pageConfig.Kind = kinds.KindPage
} }
} }
if m.kind == kinds.KindPage && !m.s.conf.IsKindEnabled(m.kind) { if m.pageConfig.Kind == kinds.KindPage && !m.s.conf.IsKindEnabled(m.pageConfig.Kind) {
return nil, nil return nil, nil
} }
pid := pageIDCounter.Add(1)
// Parse page content.
cachedContent, err := newCachedContent(m, pid)
if err != nil {
return nil, m.wrapError(err)
}
var dependencyManager identity.Manager = identity.NopManager var dependencyManager identity.Manager = identity.NopManager
if m.s.conf.Internal.Watch { if m.s.conf.Internal.Watch {
dependencyManager = identity.NewManager(m.Path()) dependencyManager = identity.NewManager(m.Path())
} }
// Parse the rest of the page content.
m.content, err = m.newCachedContent(h, pi)
if err != nil {
return nil, m.wrapError(err, h.SourceFs)
}
ps := &pageState{ ps := &pageState{
pid: pid, pid: pid,
pageOutput: nopPageOutput, pageOutput: nopPageOutput,
@ -115,7 +174,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
Staler: m, Staler: m,
dependencyManager: dependencyManager, dependencyManager: dependencyManager,
pageCommon: &pageCommon{ pageCommon: &pageCommon{
content: cachedContent,
FileProvider: m, FileProvider: m,
AuthorProvider: m, AuthorProvider: m,
Scratcher: maps.NewScratcher(), Scratcher: maps.NewScratcher(),
@ -168,10 +226,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
ps.ShortcodeInfoProvider = ps ps.ShortcodeInfoProvider = ps
ps.AlternativeOutputFormatsProvider = ps ps.AlternativeOutputFormatsProvider = ps
if err := ps.setMetaPre(); err != nil {
return nil, ps.wrapError(err)
}
if err := ps.initLazyProviders(); err != nil { if err := ps.initLazyProviders(); err != nil {
return nil, ps.wrapError(err) return nil, ps.wrapError(err)
} }
@ -182,5 +236,9 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
m.MarkStale() m.MarkStale()
} }
return ps, err if ps == nil {
return nil, nil, err
}
return ps, ps.PathInfo(), err
} }

View file

@ -127,7 +127,7 @@ func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error)
Section: pageInfoCurrentSection, Section: pageInfoCurrentSection,
UglyURLs: s.h.Conf.IsUglyURLs(p.Section()), UglyURLs: s.h.Conf.IsUglyURLs(p.Section()),
ForcePrefix: s.h.Conf.IsMultihost() || alwaysInSubDir, ForcePrefix: s.h.Conf.IsMultihost() || alwaysInSubDir,
URL: pm.urlPaths.URL, URL: pm.pageConfig.URL,
} }
if pm.Slug() != "" { if pm.Slug() != "" {

View file

@ -104,12 +104,12 @@ func (pco *pageContentOutput) Reset() {
} }
func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments { func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments {
return pco.po.p.content.mustContentToC(ctx, pco).tableOfContents return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContents
} }
func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) { func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) {
content := pco.po.p.content content := pco.po.p.m.content
source, err := content.contentSource() source, err := content.pi.contentSource(content)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -125,7 +125,7 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
insertPlaceholders = true insertPlaceholders = true
} }
c := make([]byte, 0, len(source)+(len(source)/10)) c := make([]byte, 0, len(source)+(len(source)/10))
for _, it := range content.parseInfo.itemsStep2 { for _, it := range content.pi.itemsStep2 {
switch v := it.(type) { switch v := it.(type) {
case pageparser.Item: case pageparser.Item:
c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...) c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
@ -169,12 +169,12 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
} }
func (pco *pageContentOutput) Content(ctx context.Context) (any, error) { func (pco *pageContentOutput) Content(ctx context.Context) (any, error) {
r, err := pco.po.p.content.contentRendered(ctx, pco) r, err := pco.po.p.m.content.contentRendered(ctx, pco)
return r.content, err return r.content, err
} }
func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
return pco.po.p.content.mustContentToC(ctx, pco).tableOfContentsHTML return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContentsHTML
} }
func (p *pageContentOutput) Len(ctx context.Context) int { func (p *pageContentOutput) Len(ctx context.Context) int {
@ -182,7 +182,7 @@ func (p *pageContentOutput) Len(ctx context.Context) int {
} }
func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSummary { func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSummary {
r, err := pco.po.p.content.contentRendered(ctx, pco) r, err := pco.po.p.m.content.contentRendered(ctx, pco)
if err != nil { if err != nil {
pco.fail(err) pco.fail(err)
} }
@ -190,7 +190,7 @@ func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSu
} }
func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords { func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords {
r, err := pco.po.p.content.contentPlain(ctx, pco) r, err := pco.po.p.m.content.contentPlain(ctx, pco)
if err != nil { if err != nil {
pco.fail(err) pco.fail(err)
} }
@ -270,7 +270,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
} }
conv := pco.po.p.getContentConverter() conv := pco.po.p.getContentConverter()
if opts.Markup != "" && opts.Markup != pco.po.p.m.markup { if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.Markup {
var err error var err error
conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup) conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)
if err != nil { if err != nil {
@ -281,6 +281,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
var rendered []byte var rendered []byte
parseInfo := &contentParseInfo{ parseInfo := &contentParseInfo{
h: pco.po.p.s.h,
pid: pco.po.p.pid, pid: pco.po.p.pid,
} }
@ -293,7 +294,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
} }
s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s) s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s)
if err := parseInfo.mapItems(contentToRenderb, s); err != nil { if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil {
return "", err return "", err
} }
@ -320,7 +321,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
tokenHandler := func(ctx context.Context, token string) ([]byte, error) { tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
if token == tocShortcodePlaceholder { if token == tocShortcodePlaceholder {
toc, err := pco.po.p.content.contentToC(ctx, pco) toc, err := pco.po.p.m.content.contentToC(ctx, pco)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -350,7 +351,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
} }
// We need a consolidated view in $page.HasShortcode // We need a consolidated view in $page.HasShortcode
pco.po.p.content.shortcodeState.transferNames(s) pco.po.p.m.content.shortcodeState.transferNames(s)
} else { } else {
c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false) c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
@ -411,7 +412,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
var renderCacheMu sync.Mutex var renderCacheMu sync.Mutex
resolvePosition := func(ctx any) text.Position { resolvePosition := func(ctx any) text.Position {
source := pco.po.p.content.mustSource() source := pco.po.p.m.content.mustSource()
var offset int var offset int
switch v := ctx.(type) { switch v := ctx.(type) {

View file

@ -13,7 +13,11 @@
package hugolib package hugolib
import "testing" import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestFrontMatterParamsInItsOwnSection(t *testing.T) { func TestFrontMatterParamsInItsOwnSection(t *testing.T) {
t.Parallel() t.Parallel()
@ -52,3 +56,106 @@ Summary: {{ .Summary }}|
"Summary: frontmatter.summary|", "Summary: frontmatter.summary|",
) )
} }
func TestFrontMatterParamsKindPath(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.org/"
disableKinds = ["taxonomy", "term"]
-- content/p1.md --
---
title: "P1"
date: 2019-08-07
path: "/a/b/c"
slug: "s1"
---
-- content/mysection.md --
---
title: "My Section"
kind: "section"
date: 2022-08-07
path: "/a/b"
---
-- layouts/index.html --
RegularPages: {{ range site.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Date.Format "2006-02-01" }}| Slug: {{ .Params.slug }}|{{ end }}$
Sections: {{ range site.Sections }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Date.Format "2006-02-01" }}| Slug: {{ .Params.slug }}|{{ end }}$
{{ $ab := site.GetPage "a/b" }}
a/b pages: {{ range $ab.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ end }}$
`
b := Test(t, files)
b.AssertFileContent("public/index.html",
"RegularPages: /a/b/c|/a/b/s1/|P1|2019-07-08| Slug: s1|$",
"Sections: /a|/a/|As",
"a/b pages: /a/b/c|/a/b/s1/|$",
)
}
func TestFrontMatterParamsLang(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.org/"
disableKinds = ["taxonomy", "term"]
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.nn]
weight = 2
-- content/p1.md --
---
title: "P1 nn"
lang: "nn"
---
-- content/p2.md --
---
title: "P2"
---
-- layouts/index.html --
RegularPages: {{ range site.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ end }}$
`
b := Test(t, files)
b.AssertFileContent("public/en/index.html",
"RegularPages: /p2|/en/p2/|P2|$",
)
b.AssertFileContent("public/nn/index.html",
"RegularPages: /p1|/nn/p1/|P1 nn|$",
)
}
func TestFrontMatterParamsLangNoCascade(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.org/"
disableKinds = ["taxonomy", "term"]
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.nn]
weight = 2
-- content/_index.md --
+++
[[cascade]]
background = 'yosemite.jpg'
lang = 'nn'
+++
`
b, err := TestE(t, files)
b.Assert(err, qt.IsNotNil)
}

View file

@ -315,7 +315,7 @@ func prepareShortcode(
isRenderString bool, isRenderString bool,
) (shortcodeRenderer, error) { ) (shortcodeRenderer, error) {
toParseErr := func(err error) error { toParseErr := func(err error) error {
source := p.content.mustSource() source := p.m.content.mustSource()
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos) return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
} }
@ -443,7 +443,7 @@ func doRenderShortcode(
// unchanged. // unchanged.
// 2 If inner does not have a newline, strip the wrapping <p> block and // 2 If inner does not have a newline, strip the wrapping <p> block and
// the newline. // the newline.
switch p.m.markup { switch p.m.pageConfig.Markup {
case "", "markdown": case "", "markdown":
if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match { if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match {
cleaner, err := regexp.Compile(innerCleanupRegexp) cleaner, err := regexp.Compile(innerCleanupRegexp)

View file

@ -40,6 +40,7 @@ import (
"github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/page/siteidentities" "github.com/gohugoio/hugo/resources/page/siteidentities"
@ -281,6 +282,7 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []
page.Pages](d.MemCache, "/pags/all", page.Pages](d.MemCache, "/pags/all",
dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}, dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild},
), ),
cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](d.MemCache, "/cont/src", dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
translationKeyPages: maps.NewSliceCache[page.Page](), translationKeyPages: maps.NewSliceCache[page.Page](),
currentSite: sites[0], currentSite: sites[0],
skipRebuildForFilenames: make(map[string]bool), skipRebuildForFilenames: make(map[string]bool),

View file

@ -129,7 +129,7 @@ func pageRenderer(
continue continue
} }
if p.m.buildConfig.PublishResources { if p.m.pageConfig.Build.PublishResources {
if err := p.renderResources(); err != nil { if err := p.renderResources(); err != nil {
s.SendError(p.errorf(err, "failed to render page resources")) s.SendError(p.errorf(err, "failed to render page resources"))
continue continue

View file

@ -82,6 +82,14 @@ func (m PageMatcher) Matches(p Page) bool {
return true return true
} }
var disallowedCascadeKeys = map[string]bool{
// These define the structure of the page tree and cannot
// currently be set in the cascade.
"kind": true,
"path": true,
"lang": true,
}
func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, map[PageMatcher]maps.Params], error) { func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, map[PageMatcher]maps.Params], error) {
buildConfig := func(in any) (map[PageMatcher]maps.Params, any, error) { buildConfig := func(in any) (map[PageMatcher]maps.Params, any, error) {
cascade := make(map[PageMatcher]maps.Params) cascade := make(map[PageMatcher]maps.Params)
@ -101,6 +109,11 @@ func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsCon
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for k := range m {
if disallowedCascadeKeys[k] {
return nil, nil, fmt.Errorf("key %q not allowed in cascade config", k)
}
}
cfgs = append(cfgs, c) cfgs = append(cfgs, c)
} }

View file

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved. // Copyright 2024 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -19,15 +19,76 @@ import (
"github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/spf13/cast" "github.com/spf13/cast"
) )
type Dates struct {
Date time.Time
Lastmod time.Time
PublishDate time.Time
ExpiryDate time.Time
}
func (d Dates) IsDateOrLastModAfter(in Dates) bool {
return d.Date.After(in.Date) || d.Lastmod.After(in.Lastmod)
}
func (d *Dates) UpdateDateAndLastmodIfAfter(in Dates) {
if in.Date.After(d.Date) {
d.Date = in.Date
}
if in.Lastmod.After(d.Lastmod) {
d.Lastmod = in.Lastmod
}
}
func (d Dates) IsAllDatesZero() bool {
return d.Date.IsZero() && d.Lastmod.IsZero() && d.PublishDate.IsZero() && d.ExpiryDate.IsZero()
}
// 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 holds the fource core dates for this page.
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.
Markup string // The markup used in the content file.
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.
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 // 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.
// These build options are set in the front matter,
// but not passed on to .Params.
Resources []map[string]any
Cascade map[page.PageMatcher]maps.Params // Only relevant for branch nodes.
Sitemap config.SitemapConfig
Build BuildConfig
// User defined params.
Params maps.Params
}
// FrontMatterHandler maps front matter into Page fields and .Params. // FrontMatterHandler maps front matter into Page fields and .Params.
// Note that we currently have only extracted the date logic. // Note that we currently have only extracted the date logic.
type FrontMatterHandler struct { type FrontMatterHandler struct {
@ -47,9 +108,6 @@ type FrontMatterHandler struct {
// FrontMatterDescriptor describes how to handle front matter for a given Page. // FrontMatterDescriptor describes how to handle front matter for a given Page.
// It has pointers to values in the receiving page which gets updated. // It has pointers to values in the receiving page which gets updated.
type FrontMatterDescriptor struct { type FrontMatterDescriptor struct {
// This is the Page's params.
Params map[string]any
// This is the Page's base filename (BaseFilename), e.g. page.md., or // This is the Page's base filename (BaseFilename), e.g. page.md., or
// if page is a leaf bundle, the bundle folder name (ContentBaseName). // if page is a leaf bundle, the bundle folder name (ContentBaseName).
BaseFilename string BaseFilename string
@ -60,13 +118,8 @@ type FrontMatterDescriptor struct {
// May be set from the author date in Git. // May be set from the author date in Git.
GitAuthorDate time.Time GitAuthorDate time.Time
// The below are pointers to values on Page and will be modified. // The below will be modified.
PageConfig *PageConfig
// This is the Page's dates.
Dates *resource.Dates
// This is the Page's Slug etc.
PageURLs *URLPath
// The Location to use to parse dates without time zone info. // The Location to use to parse dates without time zone info.
Location *time.Location Location *time.Location
@ -83,8 +136,8 @@ var dateFieldAliases = map[string][]string{
// supplied front matter params. Note that this requires all lower-case keys // supplied front matter params. Note that this requires all lower-case keys
// in the params map. // in the params map.
func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error { func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error {
if d.Dates == nil { if d.PageConfig == nil {
panic("missing dates") panic("missing pageConfig")
} }
if f.dateHandler == nil { if f.dateHandler == nil {
@ -297,7 +350,7 @@ func (f *FrontMatterHandler) createHandlers() error {
if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date, if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date,
func(d *FrontMatterDescriptor, t time.Time) { func(d *FrontMatterDescriptor, t time.Time) {
d.Dates.FDate = t d.PageConfig.Date = t
setParamIfNotSet(fmDate, t, d) setParamIfNotSet(fmDate, t, d)
}); err != nil { }); err != nil {
return err return err
@ -306,7 +359,7 @@ func (f *FrontMatterHandler) createHandlers() error {
if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod, if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod,
func(d *FrontMatterDescriptor, t time.Time) { func(d *FrontMatterDescriptor, t time.Time) {
setParamIfNotSet(fmLastmod, t, d) setParamIfNotSet(fmLastmod, t, d)
d.Dates.FLastmod = t d.PageConfig.Lastmod = t
}); err != nil { }); err != nil {
return err return err
} }
@ -314,7 +367,7 @@ func (f *FrontMatterHandler) createHandlers() error {
if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate, if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate,
func(d *FrontMatterDescriptor, t time.Time) { func(d *FrontMatterDescriptor, t time.Time) {
setParamIfNotSet(fmPubDate, t, d) setParamIfNotSet(fmPubDate, t, d)
d.Dates.FPublishDate = t d.PageConfig.PublishDate = t
}); err != nil { }); err != nil {
return err return err
} }
@ -322,7 +375,7 @@ func (f *FrontMatterHandler) createHandlers() error {
if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate, if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate,
func(d *FrontMatterDescriptor, t time.Time) { func(d *FrontMatterDescriptor, t time.Time) {
setParamIfNotSet(fmExpiryDate, t, d) setParamIfNotSet(fmExpiryDate, t, d)
d.Dates.FExpiryDate = t d.PageConfig.ExpiryDate = t
}); err != nil { }); err != nil {
return err return err
} }
@ -331,10 +384,10 @@ func (f *FrontMatterHandler) createHandlers() error {
} }
func setParamIfNotSet(key string, value any, d *FrontMatterDescriptor) { func setParamIfNotSet(key string, value any, d *FrontMatterDescriptor) {
if _, found := d.Params[key]; found { if _, found := d.PageConfig.Params[key]; found {
return return
} }
d.Params[key] = value d.PageConfig.Params[key] = value
} }
func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) { func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) {
@ -361,7 +414,7 @@ type frontmatterFieldHandlers int
func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler { func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
return func(d *FrontMatterDescriptor) (bool, error) { return func(d *FrontMatterDescriptor) (bool, error) {
v, found := d.Params[key] v, found := d.PageConfig.Params[key]
if !found { if !found {
return false, nil return false, nil
@ -377,7 +430,7 @@ func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d
setter(d, date) setter(d, date)
// This is the params key as set in front matter. // This is the params key as set in front matter.
d.Params[key] = date d.PageConfig.Params[key] = date
return true, nil return true, nil
} }
@ -392,9 +445,9 @@ func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMa
setter(d, date) setter(d, date)
if _, found := d.Params["slug"]; !found { if _, found := d.PageConfig.Params["slug"]; !found {
// Use slug from filename // Use slug from filename
d.PageURLs.Slug = slug d.PageConfig.Slug = slug
} }
return true, nil return true, nil

View file

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved. // Copyright 2024 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -22,16 +22,15 @@ import (
"github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
func newTestFd() *pagemeta.FrontMatterDescriptor { func newTestFd() *pagemeta.FrontMatterDescriptor {
return &pagemeta.FrontMatterDescriptor{ return &pagemeta.FrontMatterDescriptor{
Params: make(map[string]any), PageConfig: &pagemeta.PageConfig{
Dates: &resource.Dates{}, Params: make(map[string]interface{}),
PageURLs: &pagemeta.URLPath{}, },
Location: time.UTC, Location: time.UTC,
} }
} }
@ -105,16 +104,16 @@ func TestFrontMatterDatesHandlers(t *testing.T) {
case ":git": case ":git":
d.GitAuthorDate = d1 d.GitAuthorDate = d1
} }
d.Params["date"] = d2 d.PageConfig.Params["date"] = d2
c.Assert(handler.HandleDates(d), qt.IsNil) c.Assert(handler.HandleDates(d), qt.IsNil)
c.Assert(d.Dates.FDate, qt.Equals, d1) c.Assert(d.PageConfig.Dates.Date, qt.Equals, d1)
c.Assert(d.Params["date"], qt.Equals, d2) c.Assert(d.PageConfig.Params["date"], qt.Equals, d2)
d = newTestFd() d = newTestFd()
d.Params["date"] = d2 d.PageConfig.Params["date"] = d2
c.Assert(handler.HandleDates(d), qt.IsNil) c.Assert(handler.HandleDates(d), qt.IsNil)
c.Assert(d.Dates.FDate, qt.Equals, d2) c.Assert(d.PageConfig.Dates.Date, qt.Equals, d2)
c.Assert(d.Params["date"], qt.Equals, d2) c.Assert(d.PageConfig.Params["date"], qt.Equals, d2)
} }
} }
@ -137,15 +136,15 @@ func TestFrontMatterDatesDefaultKeyword(t *testing.T) {
testDate, _ := time.Parse("2006-01-02", "2018-02-01") testDate, _ := time.Parse("2006-01-02", "2018-02-01")
d := newTestFd() d := newTestFd()
d.Params["mydate"] = testDate d.PageConfig.Params["mydate"] = testDate
d.Params["date"] = testDate.Add(1 * 24 * time.Hour) d.PageConfig.Params["date"] = testDate.Add(1 * 24 * time.Hour)
d.Params["mypubdate"] = testDate.Add(2 * 24 * time.Hour) d.PageConfig.Params["mypubdate"] = testDate.Add(2 * 24 * time.Hour)
d.Params["publishdate"] = testDate.Add(3 * 24 * time.Hour) d.PageConfig.Params["publishdate"] = testDate.Add(3 * 24 * time.Hour)
c.Assert(handler.HandleDates(d), qt.IsNil) c.Assert(handler.HandleDates(d), qt.IsNil)
c.Assert(d.Dates.FDate.Day(), qt.Equals, 1) c.Assert(d.PageConfig.Dates.Date.Day(), qt.Equals, 1)
c.Assert(d.Dates.FLastmod.Day(), qt.Equals, 2) c.Assert(d.PageConfig.Dates.Lastmod.Day(), qt.Equals, 2)
c.Assert(d.Dates.FPublishDate.Day(), qt.Equals, 4) c.Assert(d.PageConfig.Dates.PublishDate.Day(), qt.Equals, 4)
c.Assert(d.Dates.FExpiryDate.IsZero(), qt.Equals, true) c.Assert(d.PageConfig.Dates.ExpiryDate.IsZero(), qt.Equals, true)
} }

View file

@ -17,13 +17,6 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
type URLPath struct {
URL string
Permalink string
Slug string
Section string
}
const ( const (
Never = "never" Never = "never"
Always = "always" Always = "always"

View file

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved. // Copyright 2024 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -19,8 +19,6 @@ import (
"github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/htime"
) )
var _ Dated = Dates{}
// Dated wraps a "dated resource". These are the 4 dates that makes // Dated wraps a "dated resource". These are the 4 dates that makes
// the date logic in Hugo. // the date logic in Hugo.
type Dated interface { type Dated interface {
@ -37,27 +35,6 @@ type Dated interface {
ExpiryDate() time.Time ExpiryDate() time.Time
} }
// Dates holds the 4 Hugo dates.
type Dates struct {
FDate time.Time
FLastmod time.Time
FPublishDate time.Time
FExpiryDate time.Time
}
func (d *Dates) IsDateOrLastModAfter(in Dated) bool {
return d.Date().After(in.Date()) || d.Lastmod().After(in.Lastmod())
}
func (d *Dates) UpdateDateAndLastmodIfAfter(in Dated) {
if in.Date().After(d.Date()) {
d.FDate = in.Date()
}
if in.Lastmod().After(d.Lastmod()) {
d.FLastmod = in.Lastmod()
}
}
// IsFuture returns whether the argument represents the future. // IsFuture returns whether the argument represents the future.
func IsFuture(d Dated) bool { func IsFuture(d Dated) bool {
if d.PublishDate().IsZero() { if d.PublishDate().IsZero() {
@ -79,19 +56,3 @@ func IsExpired(d Dated) bool {
func IsZeroDates(d Dated) bool { func IsZeroDates(d Dated) bool {
return d.Date().IsZero() && d.Lastmod().IsZero() && d.ExpiryDate().IsZero() && d.PublishDate().IsZero() return d.Date().IsZero() && d.Lastmod().IsZero() && d.ExpiryDate().IsZero() && d.PublishDate().IsZero()
} }
func (p Dates) Date() time.Time {
return p.FDate
}
func (p Dates) Lastmod() time.Time {
return p.FLastmod
}
func (p Dates) PublishDate() time.Time {
return p.FPublishDate
}
func (p Dates) ExpiryDate() time.Time {
return p.FExpiryDate
}