URI: 
       Add path, kind and lang to content front matter - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit f31a6db797c9251a362ef9f8ad4c03fb608b5ac0
   DIR parent ec22bb31a89883db5ca95404cda4f74344fd3762
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Mon, 29 Jan 2024 10:02:24 +0100
       
       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
       
       Diffstat:
         M go.mod                              |       2 +-
         M go.sum                              |       2 ++
         M hugolib/content_map.go              |       6 ++++--
         M hugolib/content_map_page.go         |      89 ++++++++++++++++++-------------
         M hugolib/hugo_sites.go               |       3 +++
         M hugolib/integrationtest_builder.go  |       9 +++++++++
         M hugolib/page.go                     |      33 ++++++++++++++++---------------
         M hugolib/page__common.go             |       3 ---
         M hugolib/page__content.go            |     180 +++++++++++++++++++++----------
         M hugolib/page__meta.go               |     352 ++++++++++++++++----------------
         M hugolib/page__new.go                |     112 +++++++++++++++++++++++--------
         M hugolib/page__paths.go              |       2 +-
         M hugolib/page__per_output.go         |      27 ++++++++++++++-------------
         M hugolib/params_test.go              |     109 ++++++++++++++++++++++++++++++-
         M hugolib/shortcode.go                |       4 ++--
         M hugolib/site_new.go                 |       2 ++
         M hugolib/site_render.go              |       2 +-
         M resources/page/page_matcher.go      |      13 +++++++++++++
         M resources/page/pagemeta/page_front… |     101 +++++++++++++++++++++++--------
         M resources/page/pagemeta/page_front… |      37 +++++++++++++++----------------
         M resources/page/pagemeta/pagemeta.go |       7 -------
         M resources/resource/dates.go         |      41 +------------------------------
       
       22 files changed, 707 insertions(+), 429 deletions(-)
       ---
   DIR diff --git a/go.mod b/go.mod
       @@ -48,7 +48,7 @@ require (
                github.com/marekm4/color-extractor v1.2.1
                github.com/mattn/go-isatty v0.0.20
                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/niklasfasching/go-org v1.7.0
                github.com/olekukonko/tablewriter v0.0.5
   DIR diff --git a/go.sum b/go.sum
       @@ -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/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.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/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
        github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
   DIR diff --git a/hugolib/content_map.go b/hugolib/content_map.go
       @@ -187,7 +187,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
                        if pi.IsContent() {
                                // Create the page now as we need it at assemembly time.
                                // The other resources are created if needed.
       -                        pageResource, err := m.s.h.newPage(
       +                        pageResource, pi, err := m.s.h.newPage(
                                        &pageMeta{
                                                f:        source.NewFileInfo(fim),
                                                pathInfo: pi,
       @@ -197,6 +197,8 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
                                if err != nil {
                                        return err
                                }
       +                        key = pi.Base()
       +
                                rs = &resourceSource{r: pageResource}
                        } else {
                                rs = &resourceSource{path: pi, opener: r, fi: fim}
       @@ -226,7 +228,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
                                },
                        ))
                        // A content file.
       -                p, err := m.s.h.newPage(
       +                p, pi, err := m.s.h.newPage(
                                &pageMeta{
                                        f:        source.NewFileInfo(fi),
                                        pathInfo: pi,
   DIR diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
       @@ -43,6 +43,7 @@ import (
        
                "github.com/gohugoio/hugo/resources/kinds"
                "github.com/gohugoio/hugo/resources/page"
       +        "github.com/gohugoio/hugo/resources/page/pagemeta"
                "github.com/gohugoio/hugo/resources/resource"
        )
        
       @@ -97,7 +98,6 @@ type pageMap struct {
                cacheContentRendered   *dynacache.Partition[string, *resources.StaleValue[contentSummary]]
                cacheContentPlain      *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]]
                contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]]
       -        cacheContentSource     *dynacache.Partition[string, *resources.StaleValue[[]byte]]
        
                cfg contentMapConfig
        }
       @@ -147,7 +147,6 @@ func (t *pageTrees) collectIdentities(key string) []identity.Identity {
        
        // collectIdentitiesSurrounding collects all identities surrounding the given key.
        func (t *pageTrees) collectIdentitiesSurrounding(key string, maxSamplesPerTree int) []identity.Identity {
       -        // TODO1 test language coverage from this.
                ids := t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treePages)
                ids = append(ids, t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treeResources)...)
                return ids
       @@ -483,7 +482,7 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources 
                                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.
                                // Merge in resources from the other languages.
                                translatedPages, _ := m.s.h.translationKeyPages.Get(translationKey)
       @@ -539,9 +538,9 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources 
        
                        sort.SliceStable(res, lessFunc)
        
       -                if len(ps.m.resourcesMetadata) > 0 {
       +                if len(ps.m.pageConfig.Resources) > 0 {
                                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)
                        }
       @@ -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}),
                        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}),
       -                cacheContentSource:     dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](mcache, fmt.Sprintf("/cont/src/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
        
                        cfg: contentMapConfig{
                                lang:                 s.Lang(),
       @@ -1215,7 +1213,7 @@ func (sa *sitePagesAssembler) applyAggregates() error {
                                // Home page gets it's cascade from the site config.
                                cascade = sa.conf.Cascade.Config
        
       -                        if pageBundle.m.cascade == nil {
       +                        if pageBundle.m.pageConfig.Cascade == nil {
                                        // Pass the site cascade downwards.
                                        pw.WalkContext.Data().Insert(keyPage, cascade)
                                }
       @@ -1227,12 +1225,12 @@ func (sa *sitePagesAssembler) applyAggregates() error {
                        }
        
                        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.
                                pw.WalkContext.AddPostHook(
                                        func() error {
       -                                        if oldDates != pageBundle.m.dates {
       +                                        if oldDates != pageBundle.m.pageConfig.Dates {
                                                        sa.assembleChanges.Add(pageBundle)
                                                }
                                                return nil
       @@ -1251,11 +1249,12 @@ func (sa *sitePagesAssembler) applyAggregates() error {
        
                        const eventName = "dates"
                        if n.isContentNodeBranch() {
       -                        if pageBundle.m.cascade != nil {
       +                        if pageBundle.m.pageConfig.Cascade != nil {
                                        // 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() {
                                        pw.WalkContext.AddEventListener(eventName, keyPage, func(e *doctree.Event[contentNodeI]) {
                                                sp, ok := e.Source.(*pageState)
       @@ -1264,15 +1263,15 @@ func (sa *sitePagesAssembler) applyAggregates() error {
                                                }
        
                                                if wasZeroDates {
       -                                                pageBundle.m.dates.UpdateDateAndLastmodIfAfter(sp.m.dates)
       +                                                pageBundle.m.pageConfig.Dates.UpdateDateAndLastmodIfAfter(sp.m.pageConfig.Dates)
                                                }
        
                                                if pageBundle.IsHome() {
       -                                                if pageBundle.m.dates.Lastmod().After(pageBundle.s.lastmod) {
       -                                                        pageBundle.s.lastmod = pageBundle.m.dates.Lastmod()
       +                                                if pageBundle.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) {
       +                                                        pageBundle.s.lastmod = pageBundle.m.pageConfig.Dates.Lastmod
                                                        }
       -                                                if sp.m.dates.Lastmod().After(pageBundle.s.lastmod) {
       -                                                        pageBundle.s.lastmod = sp.m.dates.Lastmod()
       +                                                if sp.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) {
       +                                                        pageBundle.s.lastmod = sp.m.pageConfig.Dates.Lastmod
                                                        }
                                                }
                                        })
       @@ -1351,9 +1350,9 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error {
                                        p := n.(*pageState)
                                        if p.Kind() != kinds.KindTerm {
                                                // The other kinds were handled in applyAggregates.
       -                                        if p.m.cascade != nil {
       +                                        if p.m.pageConfig.Cascade != nil {
                                                        // 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.
                                        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]) {
                                                        sp, ok := e.Source.(*pageState)
                                                        if !ok {
                                                                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.
                                // Check if translationKey is set by user.
                                // This is to support the manual way of setting the translationKey in front matter.
       -                        if ps.m.translationKey != "" {
       -                                sa.s.h.translationKeyPages.Append(ps.m.translationKey, ps)
       +                        if ps.m.pageConfig.TranslationKey != "" {
       +                                sa.s.h.translationKeyPages.Append(ps.m.pageConfig.TranslationKey, ps)
                                }
        
                                if sa.pageMap.cfg.taxonomyTermDisabled {
       @@ -1477,9 +1476,13 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error {
                                                                singular: viewName.singular,
                                                                s:        sa.Site,
                                                                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 {
                                                                return false, err
                                                        }
       @@ -1524,7 +1527,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
                                targetPaths := ps.targetPaths()
                                baseTarget := targetPaths.SubResourceBaseTarget
                                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
                                }
        
       @@ -1566,7 +1569,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
                                                        BasePathTargetPath:   baseTarget,
                                                        Name:                 relPath,
                                                        NameOriginal:         relPathOriginal,
       -                                                LazyPublish:          !ps.m.buildConfig.PublishResources,
       +                                                LazyPublish:          !ps.m.pageConfig.Build.PublishResources,
                                                }
                                                r, err := ps.m.s.ResourceSpec.NewResource(rd)
                                                if err != nil {
       @@ -1631,7 +1634,7 @@ func (sa *sitePagesAssembler) removeShouldNotBuild() error {
                                        case kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy:
                                                // We need to keep these for the structure, but disable
                                                // them so they don't get listed/rendered.
       -                                        (&p.m.buildConfig).Disable()
       +                                        (&p.m.pageConfig.Build).Disable()
                                        default:
                                                keys = append(keys, key)
                                        }
       @@ -1673,13 +1676,17 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
                        }
        
                        m := &pageMeta{
       -                        s:                      s,
       -                        pathInfo:               s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
       -                        kind:                   kind,
       +                        s:        s,
       +                        pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
       +                        pageMetaParams: pageMetaParams{
       +                                pageConfig: &pagemeta.PageConfig{
       +                                        Kind: kind,
       +                                },
       +                        },
                                standaloneOutputFormat: f,
                        }
        
       -                p, _ := s.h.newPage(m)
       +                p, _, _ := s.h.newPage(m)
        
                        tree.InsertIntoValuesDimension(key, p)
                }
       @@ -1756,7 +1763,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
                                                pathInfo: pth,
                                        }
        
       -                                ps, err := sa.h.newPage(m)
       +                                ps, pth, err := sa.h.newPage(m)
                                        if err != nil {
                                                return false, err
                                        }
       @@ -1781,9 +1788,13 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
                        m := &pageMeta{
                                s:        sa.Site,
                                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 {
                                return err
                        }
       @@ -1810,10 +1821,14 @@ func (sa *sitePagesAssembler) addMissingTaxonomies() error {
                                m := &pageMeta{
                                        s:        sa.Site,
                                        pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"),
       -                                kind:     kinds.KindTaxonomy,
       +                                pageMetaParams: pageMetaParams{
       +                                        pageConfig: &pagemeta.PageConfig{
       +                                                Kind: kinds.KindTaxonomy,
       +                                        },
       +                                },
                                        singular: viewName.singular,
                                }
       -                        p, _ := sa.h.newPage(m)
       +                        p, _, _ := sa.h.newPage(m)
                                tree.InsertIntoValuesDimension(key, p)
                        }
                }
   DIR diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
       @@ -26,6 +26,7 @@ import (
                "github.com/gohugoio/hugo/config/allconfig"
                "github.com/gohugoio/hugo/hugofs/glob"
                "github.com/gohugoio/hugo/hugolib/doctree"
       +        "github.com/gohugoio/hugo/resources"
        
                "github.com/fsnotify/fsnotify"
        
       @@ -72,6 +73,8 @@ type HugoSites struct {
        
                // Cache for page listings.
                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
                // that could be overridden in front matter.
   DIR diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
       @@ -80,6 +80,15 @@ func Test(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder {
                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.
        // Deprecated: Use Test with TestOptRunning instead.
        func TestRunning(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder {
   DIR diff --git a/hugolib/page.go b/hugolib/page.go
       @@ -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");
        // 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/layouts"
                "github.com/gohugoio/hugo/related"
       +        "github.com/spf13/afero"
        
                "github.com/gohugoio/hugo/markup/converter"
                "github.com/gohugoio/hugo/markup/tableofcontents"
       @@ -197,7 +198,7 @@ func (p *pageHeadingsFiltered) page() page.Page {
        
        // For internal use by the related content feature.
        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 {
                        panic(err)
                }
       @@ -313,14 +314,14 @@ func (p *pageState) Pages() page.Pages {
        // RawContent returns the un-rendered source content without
        // any leading front matter.
        func (p *pageState) RawContent() string {
       -        if p.content.parseInfo.itemsStep2 == nil {
       +        if p.m.content.pi.itemsStep2 == nil {
                        return ""
                }
       -        start := p.content.parseInfo.posMainContent
       +        start := p.m.content.pi.posMainContent
                if start == -1 {
                        start = 0
                }
       -        source, err := p.content.contentSource()
       +        source, err := p.m.content.pi.contentSource(p.m.content)
                if err != nil {
                        panic(err)
                }
       @@ -332,11 +333,11 @@ func (p *pageState) Resources() resource.Resources {
        }
        
        func (p *pageState) HasShortcode(name string) bool {
       -        if p.content.shortcodeState == nil {
       +        if p.m.content.shortcodeState == nil {
                        return false
                }
        
       -        return p.content.shortcodeState.hasName(name)
       +        return p.m.content.shortcodeState.hasName(name)
        }
        
        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.
        func (p *pageState) TranslationKey() string {
       -        if p.m.translationKey != "" {
       -                return p.m.translationKey
       +        if p.m.pageConfig.TranslationKey != "" {
       +                return p.m.pageConfig.TranslationKey
                }
                return p.Path()
        }
       @@ -365,9 +366,9 @@ func (p *pageState) TranslationKey() string {
        func (p *pageState) AllTranslations() page.Pages {
                key := p.Path() + "/" + "translations-all"
                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.
       -                        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))
                                copy(pasc, pas)
                                page.SortByLanguage(pasc)
       @@ -534,7 +535,7 @@ var defaultRenderStringOpts = renderStringOpts{
                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 {
                        panic("wrapError with nil")
                }
       @@ -544,18 +545,18 @@ func (p *pageMeta) wrapError(err error) error {
                        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
        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 {
                var err error
                p.contentConverterInit.Do(func() {
       -                markup := p.m.markup
       +                markup := p.m.pageConfig.Markup
                        if markup == "html" {
                                // Only used for shortcode inner content.
                                markup = "markdown"
       @@ -612,7 +613,7 @@ func (p *pageState) posFromInput(input []byte, 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
   DIR diff --git a/hugolib/page__common.go b/hugolib/page__common.go
       @@ -91,9 +91,6 @@ type pageCommon struct {
                layoutDescriptor     layouts.LayoutDescriptor
                layoutDescriptorInit sync.Once
        
       -        // The source and the parsed page content.
       -        content *cachedContent
       -
                // Set if feature enabled and this is in a Git repo.
                gitInfo    source.GitInfo
                codeowners []string
   DIR diff --git a/hugolib/page__content.go b/hugolib/page__content.go
       @@ -20,6 +20,7 @@ import (
                "fmt"
                "html/template"
                "io"
       +        "strconv"
                "strings"
                "unicode/utf8"
        
       @@ -53,9 +54,8 @@ type pageContentReplacement struct {
                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 filename string
                if m.f != nil {
                        meta := m.f.FileInfo().Meta()
                        openSource = func() (hugio.ReadSeekCloser, error) {
       @@ -65,6 +65,44 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) {
                                }
                                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()
                }
        
       @@ -72,15 +110,11 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) {
                        pm:             m.s.pageMap,
                        StaleInfo:      m,
                        shortcodeState: newShortcodeHandler(filename, m.s),
       -                parseInfo: &contentParseInfo{
       -                        pid: pid,
       -                },
       -                cacheBaseKey: m.pathInfo.PathNoLang(),
       -                openSource:   openSource,
       -                enableEmoji:  m.s.conf.EnableEmoji,
       +                pi:             pi,
       +                enableEmoji:    m.s.conf.EnableEmoji,
                }
        
       -        source, err := c.contentSource()
       +        source, err := c.pi.contentSource(m)
                if err != nil {
                        return nil, err
                }
       @@ -95,23 +129,25 @@ func newCachedContent(m *pageMeta, pid uint64) (*cachedContent, error) {
        type cachedContent struct {
                pm *pageMap
        
       -        cacheBaseKey string
       -
       -        // The source bytes.
       -        openSource hugio.OpenReadSeekCloser
       -
                resource.StaleInfo
        
                shortcodeState *shortcodeHandler
        
                // Parsed content.
       -        parseInfo *contentParseInfo
       +        pi *contentParseInfo
        
                enableEmoji bool
        }
        
        type contentParseInfo struct {
       -        pid         uint64
       +        h *HugoSites
       +
       +        pid       uint64
       +        sourceKey string
       +
       +        // The source bytes.
       +        openSource hugio.OpenReadSeekCloser
       +
                frontMatter map[string]any
        
                // 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 {
       -        return len(c.parseInfo.itemsStep2) == 0
       +        return len(c.pi.itemsStep2) == 0
        }
        
        func (c *cachedContent) parseContentFile(source []byte) error {
       -        if source == nil || c.openSource == nil {
       +        if source == nil || c.pi.openSource == nil {
                        return nil
                }
        
       -        items, err := pageparser.ParseBytes(
       -                source,
       -                pageparser.Config{},
       -        )
       -        if err != nil {
       -                return err
       -        }
       -
       -        c.parseInfo.itemsStep1 = items
       -
       -        return c.parseInfo.mapItems(source, c.shortcodeState)
       +        return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState)
        }
        
        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
        }
        
       -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,
                s *shortcodeHandler,
        ) error {
       @@ -273,13 +341,7 @@ Loop:
                        switch {
                        case it.Type == pageparser.TypeIgnore:
                        case it.IsFrontMatter():
       -                        if err := rn.parseFrontMatter(it, iter, source); err != nil {
       -                                return err
       -                        }
       -                        next := iter.Peek()
       -                        if !next.IsDone() {
       -                                rn.posMainContent = next.Pos()
       -                        }
       +                        // Ignore.
                        case it.Type == pageparser.TypeLeadSummaryDivider:
                                posBody := -1
                                f := func(item pageparser.Item) bool {
       @@ -347,16 +409,16 @@ Loop:
        }
        
        func (c *cachedContent) mustSource() []byte {
       -        source, err := c.contentSource()
       +        source, err := c.pi.contentSource(c)
                if err != nil {
                        panic(err)
                }
                return source
        }
        
       -func (c *cachedContent) contentSource() ([]byte, error) {
       -        key := c.cacheBaseKey
       -        v, err := c.pm.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
       +func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
       +        key := c.sourceKey
       +        v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
                        b, err := c.readSourceAll()
                        if err != nil {
                                return nil, err
       @@ -365,7 +427,7 @@ func (c *cachedContent) contentSource() ([]byte, error) {
                        return &resources.StaleValue[[]byte]{
                                Value: b,
                                IsStaleFunc: func() bool {
       -                                return c.IsStale()
       +                                return s.IsStale()
                                },
                        }, nil
                })
       @@ -376,7 +438,7 @@ func (c *cachedContent) contentSource() ([]byte, error) {
                return v.Value, nil
        }
        
       -func (c *cachedContent) readSourceAll() ([]byte, error) {
       +func (c *contentParseInfo) readSourceAll() ([]byte, error) {
                if c.openSource == nil {
                        return []byte{}, nil
                }
       @@ -424,7 +486,7 @@ type contentPlainPlainWords struct {
        
        func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) {
                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
        
                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.
                                return rs, nil
                        }
       @@ -501,8 +563,8 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
        
                        var result contentSummary // hasVariants bool
        
       -                if c.parseInfo.hasSummaryDivider {
       -                        isHTML := cp.po.p.m.markup == "html"
       +                if c.pi.hasSummaryDivider {
       +                        isHTML := cp.po.p.m.pageConfig.Markup == "html"
                                if isHTML {
                                        // Use the summary sections as provided by the user.
                                        i := bytes.Index(b, internalSummaryDividerPre)
       @@ -510,7 +572,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
                                        b = b[i+len(internalSummaryDividerPre):]
        
                                } 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 {
                                                cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err)
                                        } else {
       @@ -518,7 +580,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
                                                result.summary = helpers.BytesToHTML(summary)
                                        }
                                }
       -                        result.summaryTruncated = c.parseInfo.summaryTruncated
       +                        result.summaryTruncated = c.pi.summaryTruncated
                        }
                        result.content = helpers.BytesToHTML(b)
                        rs.Value = result
       @@ -543,11 +605,11 @@ func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutpu
        var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback")
        
        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
        
                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 {
                                return nil, err
                        }
       @@ -572,7 +634,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
                                }
        
                                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 {
                                                        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.
       -                        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 {
                                        cp.po.p.pageOutputTemplateVariationsState.Add(1)
                                }
       @@ -589,7 +651,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
                        ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
        
                        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 {
                                return nil, err
                        }
       @@ -598,7 +660,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
                                p.pageOutputTemplateVariationsState.Add(1)
                        }
        
       -                isHTML := cp.po.p.m.markup == "html"
       +                isHTML := cp.po.p.m.pageConfig.Markup == "html"
        
                        if !isHTML {
                                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) {
       -        key := c.cacheBaseKey + "/" + cp.po.f.Name
       +        key := c.pi.sourceKey + "/" + cp.po.f.Name
        
                versionv := cp.contentRenderedVersion
        
       @@ -681,7 +743,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
                        result.plain = tpl.StripHTML(string(rendered.content))
                        result.plainWords = strings.Fields(result.plain)
        
       -                isCJKLanguage := cp.po.p.m.isCJKLanguage
       +                isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
        
                        if isCJKLanguage {
                                result.wordCount = 0
       @@ -711,8 +773,8 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
                        if rendered.summary != "" {
                                result.summary = rendered.summary
                                result.summaryTruncated = rendered.summaryTruncated
       -                } else if cp.po.p.m.summary != "" {
       -                        b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.summary), false)
       +                } else if cp.po.p.m.pageConfig.Summary != "" {
       +                        b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false)
                                if err != nil {
                                        return nil, err
                                }
   DIR diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
       @@ -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");
        // 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}`)
        
        type pageMeta struct {
       -        kind     string // Page kind.
                term     string // Set for kind == KindTerm.
                singular string // Set for kind == KindTerm and kind == KindTaxonomy.
        
                resource.Staler
                pageMetaParams
       -
                pageMetaFrontMatter
        
                // 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.
                f        *source.File
        
       +        content *cachedContent // The source and the parsed page content.
       +
                s *Site // The site this page belongs to.
        }
        
        // Prepare for a rebuild of the data passed in from front matter.
        func (m *pageMeta) setMetaPostPrepareRebuild() {
                params := xmaps.Clone[map[string]any](m.paramsOriginal)
       -        m.pageMetaParams.params = params
       +        m.pageMetaParams.pageConfig.Params = params
                m.pageMetaFrontMatter = pageMetaFrontMatter{}
        }
        
       @@ -80,48 +80,28 @@ type pageMetaParams struct {
                setMetaPostCount          int
                setMetaPostCascadeChanged bool
        
       -        params  map[string]any                   // Params contains configuration defined in the params section of page frontmatter.
       -        cascade map[page.PageMatcher]maps.Params // cascade contains default configuration to be cascaded downwards.
       +        pageConfig *pagemeta.PageConfig
        
                // 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.
                cascadeOriginal map[page.PageMatcher]maps.Params // contains the original cascade as defined in the front matter.
        }
        
        // From page front matter.
        type pageMetaFrontMatter struct {
       -        draft          bool // Only published when running with -D flag
       -        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
       +        configuredOutputFormats output.Formats // outputs defiend in front matter.
        }
        
        func (m *pageMetaParams) init(preserveOringal bool) {
                if preserveOringal {
       -                m.paramsOriginal = xmaps.Clone[maps.Params](m.params)
       -                m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.cascade)
       +                m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params)
       +                m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.Cascade)
                }
        }
        
        func (p *pageMeta) Aliases() []string {
       -        return p.aliases
       +        return p.pageConfig.Aliases
        }
        
        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 {
       -        return p.description
       +        return p.pageConfig.Description
        }
        
        func (p *pageMeta) Lang() string {
       @@ -159,7 +155,7 @@ func (p *pageMeta) Lang() string {
        }
        
        func (p *pageMeta) Draft() bool {
       -        return p.draft
       +        return p.pageConfig.Draft
        }
        
        func (p *pageMeta) File() *source.File {
       @@ -171,20 +167,20 @@ func (p *pageMeta) IsHome() bool {
        }
        
        func (p *pageMeta) Keywords() []string {
       -        return p.keywords
       +        return p.pageConfig.Keywords
        }
        
        func (p *pageMeta) Kind() string {
       -        return p.kind
       +        return p.pageConfig.Kind
        }
        
        func (p *pageMeta) Layout() string {
       -        return p.layout
       +        return p.pageConfig.Layout
        }
        
        func (p *pageMeta) LinkTitle() string {
       -        if p.linkTitle != "" {
       -                return p.linkTitle
       +        if p.pageConfig.LinkTitle != "" {
       +                return p.pageConfig.LinkTitle
                }
        
                return p.Title()
       @@ -194,7 +190,7 @@ func (p *pageMeta) Name() string {
                if p.resourcePath != "" {
                        return p.resourcePath
                }
       -        if p.kind == kinds.KindTerm {
       +        if p.pageConfig.Kind == kinds.KindTerm {
                        return p.pathInfo.Unmormalized().BaseNameNoIdentifier()
                }
                return p.Title()
       @@ -218,7 +214,7 @@ func (p *pageMeta) Param(key any) (any, error) {
        }
        
        func (p *pageMeta) Params() maps.Params {
       -        return p.params
       +        return p.pageConfig.Params
        }
        
        func (p *pageMeta) Path() string {
       @@ -248,18 +244,18 @@ func (p *pageMeta) Section() string {
        }
        
        func (p *pageMeta) Sitemap() config.SitemapConfig {
       -        return p.sitemap
       +        return p.pageConfig.Sitemap
        }
        
        func (p *pageMeta) Title() string {
       -        return p.title
       +        return p.pageConfig.Title
        }
        
        const defaultContentType = "page"
        
        func (p *pageMeta) Type() string {
       -        if p.contentType != "" {
       -                return p.contentType
       +        if p.pageConfig.Type != "" {
       +                return p.pageConfig.Type
                }
        
                if sect := p.Section(); sect != "" {
       @@ -270,36 +266,56 @@ func (p *pageMeta) Type() string {
        }
        
        func (p *pageMeta) Weight() int {
       -        return p.weight
       +        return p.pageConfig.Weight
        }
        
       -func (ps *pageState) setMetaPre() error {
       -        pm := ps.m
       -        p := ps
       -        frontmatter := p.content.parseInfo.frontMatter
       -        watching := p.s.watching()
       -
       +func (p *pageMeta) setMetaPre(pi *contentParseInfo, conf config.AllProvider) error {
       +        frontmatter := pi.frontMatter
                if frontmatter != nil {
       +                pcfg := p.pageConfig
       +                if pcfg == nil {
       +                        panic("pageConfig not set")
       +                }
                        // Needed for case insensitive fetching of params values
                        maps.PrepareParams(frontmatter)
       -                pm.pageMetaParams.params = frontmatter
       -                if p.IsNode() {
       -                        // Check for any cascade define on itself.
       -                        if cv, found := frontmatter["cascade"]; found {
       -                                var err error
       -                                cascade, err := page.DecodeCascade(cv)
       -                                if err != nil {
       -                                        return err
       -                                }
       -                                pm.pageMetaParams.cascade = cascade
       +                pcfg.Params = frontmatter
       +                // Check for any cascade define on itself.
       +                if cv, found := frontmatter["cascade"]; found {
       +                        var err error
       +                        cascade, err := page.DecodeCascade(cv)
       +                        if err != nil {
       +                                return err
       +                        }
       +                        pcfg.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 {
       -                pm.pageMetaParams.params = make(maps.Params)
       +                if v, found := frontmatter["kind"]; found {
       +                        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
        }
       @@ -308,18 +324,18 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
                ps.m.setMetaPostCount++
                var cascadeHashPre uint64
                if ps.m.setMetaPostCount > 1 {
       -                cascadeHashPre = identity.HashUint64(ps.m.cascade)
       -                ps.m.cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
       +                cascadeHashPre = identity.HashUint64(ps.m.pageConfig.Cascade)
       +                ps.m.pageConfig.Cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
        
                }
        
                // Apply cascades first so they can be overriden later.
                if cascade != nil {
       -                if ps.m.cascade != nil {
       +                if ps.m.pageConfig.Cascade != nil {
                                for k, v := range cascade {
       -                                vv, found := ps.m.cascade[k]
       +                                vv, found := ps.m.pageConfig.Cascade[k]
                                        if !found {
       -                                        ps.m.cascade[k] = v
       +                                        ps.m.pageConfig.Cascade[k] = v
                                        } else {
                                                // Merge
                                                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 {
       -                        ps.m.cascade = cascade
       +                        ps.m.pageConfig.Cascade = cascade
                        }
                }
        
                if cascade == nil {
       -                cascade = ps.m.cascade
       +                cascade = ps.m.pageConfig.Cascade
                }
        
                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 {
                                // 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
                        }
                        ps.m.setMetaPostPrepareRebuild()
       @@ -356,8 +372,8 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
                                continue
                        }
                        for kk, vv := range v {
       -                        if _, found := ps.m.params[kk]; !found {
       -                                ps.m.params[kk] = vv
       +                        if _, found := ps.m.pageConfig.Params[kk]; !found {
       +                                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.
       -        ps.m.datesOriginal = ps.m.pageMetaDates
       +        ps.m.datesOriginal = ps.m.pageConfig.Dates
        
                return nil
        }
       @@ -392,13 +408,8 @@ func (p *pageState) setMetaPostParams() error {
                        gitAuthorDate = p.gitInfo.AuthorDate
                }
        
       -        pm.pageMetaDates = pageMetaDates{}
       -        pm.urlPaths = pagemeta.URLPath{}
       -
                descriptor := &pagemeta.FrontMatterDescriptor{
       -                Params:        pm.params,
       -                Dates:         &pm.pageMetaDates.dates,
       -                PageURLs:      &pm.urlPaths,
       +                PageConfig:    pm.pageConfig,
                        BaseFilename:  contentBaseName,
                        ModTime:       mtime,
                        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)
                }
        
       -        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 {
                        return err
                }
        
                var sitemapSet bool
        
       +        pcfg := pm.pageConfig
       +
       +        params := pcfg.Params
       +
                var draft, published, isCJKLanguage *bool
                var userParams map[string]any
       -        for k, v := range pm.params {
       +        for k, v := range pcfg.Params {
                        loki := strings.ToLower(k)
        
                        if loki == "params" {
       @@ -431,7 +453,7 @@ func (p *pageState) setMetaPostParams() error {
                                        return err
                                }
                                userParams = vv
       -                        delete(pm.params, k)
       +                        delete(pcfg.Params, k)
                                continue
                        }
        
       @@ -450,43 +472,43 @@ func (p *pageState) setMetaPostParams() error {
        
                        switch loki {
                        case "title":
       -                        pm.title = cast.ToString(v)
       -                        pm.params[loki] = pm.title
       +                        pcfg.Title = cast.ToString(v)
       +                        params[loki] = pcfg.Title
                        case "linktitle":
       -                        pm.linkTitle = cast.ToString(v)
       -                        pm.params[loki] = pm.linkTitle
       +                        pcfg.LinkTitle = cast.ToString(v)
       +                        params[loki] = pcfg.LinkTitle
                        case "summary":
       -                        pm.summary = cast.ToString(v)
       -                        pm.params[loki] = pm.summary
       +                        pcfg.Summary = cast.ToString(v)
       +                        params[loki] = pcfg.Summary
                        case "description":
       -                        pm.description = cast.ToString(v)
       -                        pm.params[loki] = pm.description
       +                        pcfg.Description = cast.ToString(v)
       +                        params[loki] = pcfg.Description
                        case "slug":
                                // Don't start or end with a -
       -                        pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-")
       -                        pm.params[loki] = pm.Slug()
       +                        pcfg.Slug = strings.Trim(cast.ToString(v), "-")
       +                        params[loki] = pm.Slug()
                        case "url":
                                url := cast.ToString(v)
                                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())
                                }
       -                        pm.urlPaths.URL = url
       -                        pm.params[loki] = url
       +                        pcfg.URL = url
       +                        params[loki] = url
                        case "type":
       -                        pm.contentType = cast.ToString(v)
       -                        pm.params[loki] = pm.contentType
       +                        pcfg.Type = cast.ToString(v)
       +                        params[loki] = pcfg.Type
                        case "keywords":
       -                        pm.keywords = cast.ToStringSlice(v)
       -                        pm.params[loki] = pm.keywords
       +                        pcfg.Keywords = cast.ToStringSlice(v)
       +                        params[loki] = pcfg.Keywords
                        case "headless":
                                // Legacy setting for leaf bundles.
                                // This is since Hugo 0.63 handled in a more general way for all
                                // pages.
                                isHeadless := cast.ToBool(v)
       -                        pm.params[loki] = isHeadless
       +                        params[loki] = isHeadless
                                if p.File().TranslationBaseName() == "index" && isHeadless {
       -                                pm.buildConfig.List = pagemeta.Never
       -                                pm.buildConfig.Render = pagemeta.Never
       +                                pm.pageConfig.Build.List = pagemeta.Never
       +                                pm.pageConfig.Build.Render = pagemeta.Never
                                }
                        case "outputs":
                                o := cast.ToStringSlice(v)
       @@ -501,43 +523,42 @@ func (p *pageState) setMetaPostParams() error {
                                                p.s.Log.Errorf("Failed to resolve output formats: %s", err)
                                        } else {
                                                pm.configuredOutputFormats = outFormats
       -                                        pm.params[loki] = outFormats
       +                                        params[loki] = outFormats
                                        }
                                }
                        case "draft":
                                draft = new(bool)
                                *draft = cast.ToBool(v)
                        case "layout":
       -                        pm.layout = cast.ToString(v)
       -                        pm.params[loki] = pm.layout
       +                        pcfg.Layout = cast.ToString(v)
       +                        params[loki] = pcfg.Layout
                        case "markup":
       -                        pm.markup = cast.ToString(v)
       -                        pm.params[loki] = pm.markup
       +                        pcfg.Markup = cast.ToString(v)
       +                        params[loki] = pcfg.Markup
                        case "weight":
       -                        pm.weight = cast.ToInt(v)
       -                        pm.params[loki] = pm.weight
       +                        pcfg.Weight = cast.ToInt(v)
       +                        params[loki] = pcfg.Weight
                        case "aliases":
       -                        pm.aliases = cast.ToStringSlice(v)
       -                        for i, alias := range pm.aliases {
       +                        pcfg.Aliases = cast.ToStringSlice(v)
       +                        for i, alias := range pcfg.Aliases {
                                        if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
                                                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":
       -                        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 {
                                        return fmt.Errorf("failed to decode sitemap config in front matter: %s", err)
                                }
       -                        pm.params[loki] = p.m.sitemap
                                sitemapSet = true
                        case "iscjklanguage":
                                isCJKLanguage = new(bool)
                                *isCJKLanguage = cast.ToBool(v)
                        case "translationkey":
       -                        pm.translationKey = cast.ToString(v)
       -                        pm.params[loki] = pm.translationKey
       +                        pcfg.TranslationKey = cast.ToString(v)
       +                        params[loki] = pcfg.TranslationKey
                        case "resources":
                                var resources []map[string]any
                                handled := true
       @@ -563,8 +584,7 @@ func (p *pageState) setMetaPostParams() error {
                                }
        
                                if handled {
       -                                pm.params[loki] = resources
       -                                pm.resourcesMetadata = resources
       +                                pcfg.Resources = resources
                                        break
                                }
                                fallthrough
       @@ -586,51 +606,51 @@ func (p *pageState) setMetaPostParams() error {
                                                        for i, u := range vv {
                                                                a[i] = cast.ToString(u)
                                                        }
       -                                                pm.params[loki] = a
       +                                                params[loki] = a
                                                } else {
       -                                                pm.params[loki] = vv
       +                                                params[loki] = vv
                                                }
                                        } else {
       -                                        pm.params[loki] = []string{}
       +                                        params[loki] = []string{}
                                        }
        
                                default:
       -                                pm.params[loki] = vv
       +                                params[loki] = vv
                                }
                        }
                }
        
                for k, v := range userParams {
       -                pm.params[strings.ToLower(k)] = v
       +                params[strings.ToLower(k)] = v
                }
        
                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 {
       -                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())
                } else if draft != nil {
       -                pm.draft = *draft
       +                pcfg.Draft = *draft
                } else if published != nil {
       -                pm.draft = !*published
       +                pcfg.Draft = !*published
                }
       -        pm.params["draft"] = pm.draft
       +        params["draft"] = pcfg.Draft
        
                if isCJKLanguage != nil {
       -                pm.isCJKLanguage = *isCJKLanguage
       -        } else if p.s.conf.HasCJKLanguage && p.content.openSource != nil {
       -                if cjkRe.Match(p.content.mustSource()) {
       -                        pm.isCJKLanguage = true
       +                pcfg.IsCJKLanguage = *isCJKLanguage
       +        } else if p.s.conf.HasCJKLanguage && p.m.content.pi.openSource != nil {
       +                if cjkRe.Match(p.m.content.mustSource()) {
       +                        pcfg.IsCJKLanguage = true
                        } else {
       -                        pm.isCJKLanguage = false
       +                        pcfg.IsCJKLanguage = false
                        }
                }
        
       -        pm.params["iscjklanguage"] = p.m.isCJKLanguage
       +        params["iscjklanguage"] = pcfg.IsCJKLanguage
        
                return nil
        }
       @@ -643,7 +663,7 @@ func (p *pageMeta) shouldList(global bool) bool {
                        return false
                }
        
       -        switch p.buildConfig.List {
       +        switch p.pageConfig.Build.List {
                case pagemeta.Always:
                        return true
                case pagemeta.Never:
       @@ -667,56 +687,56 @@ func (p *pageMeta) shouldBeCheckedForMenuDefinitions() bool {
                        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 {
       -        return p.buildConfig.Render != pagemeta.Always
       +        return p.pageConfig.Build.Render != pagemeta.Always
        }
        
        func (p *pageMeta) noLink() bool {
       -        return p.buildConfig.Render == pagemeta.Never
       +        return p.pageConfig.Build.Render == pagemeta.Never
        }
        
        func (p *pageMeta) applyDefaultValues() error {
       -        if p.buildConfig.IsZero() {
       -                p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil)
       +        if p.pageConfig.Build.IsZero() {
       +                p.pageConfig.Build, _ = pagemeta.DecodeBuildConfig(nil)
                }
        
                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 {
                                // 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 == "" {
       -                        p.markup = "markdown"
       +                if p.pageConfig.Markup == "" {
       +                        p.pageConfig.Markup = "markdown"
                        }
                }
        
       -        if p.title == "" && p.f == nil {
       +        if p.pageConfig.Title == "" && p.f == nil {
                        switch p.Kind() {
                        case kinds.KindHome:
       -                        p.title = p.s.Title()
       +                        p.pageConfig.Title = p.s.Title()
                        case kinds.KindSection:
                                sectionName := p.pathInfo.Unmormalized().BaseNameNoIdentifier()
                                if p.s.conf.PluralizeListTitles {
                                        sectionName = flect.Pluralize(sectionName)
                                }
       -                        p.title = p.s.conf.C.CreateTitle(sectionName)
       +                        p.pageConfig.Title = p.s.conf.C.CreateTitle(sectionName)
                        case kinds.KindTerm:
                                if p.term != "" {
       -                                p.title = p.s.conf.C.CreateTitle(p.term)
       +                                p.pageConfig.Title = p.s.conf.C.CreateTitle(p.term)
                                } else {
                                        panic("term not set")
                                }
                        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:
       -                        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 {
       -        return p.urlPaths.Slug
       +        return p.pageConfig.Slug
        }
        
        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)
        }
        
       -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 {
                ps.init.Add(func(ctx context.Context) (any, error) {
                        pp, err := newPagePaths(ps)
   DIR diff --git a/hugolib/page__new.go b/hugolib/page__new.go
       @@ -15,45 +15,106 @@ package hugolib
        
        import (
                "fmt"
       +        "path/filepath"
                "sync"
                "sync/atomic"
        
       +        "github.com/gohugoio/hugo/hugofs/files"
                "github.com/gohugoio/hugo/identity"
                "github.com/gohugoio/hugo/resources"
        
                "github.com/gohugoio/hugo/common/maps"
       +        "github.com/gohugoio/hugo/common/paths"
        
                "github.com/gohugoio/hugo/lazy"
        
                "github.com/gohugoio/hugo/resources/kinds"
                "github.com/gohugoio/hugo/resources/page"
       +        "github.com/gohugoio/hugo/resources/page/pagemeta"
        )
        
        var pageIDCounter atomic.Uint64
        
       -func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
       -        if m.pathInfo == nil {
       +func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
       +        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 {
                                m.pathInfo = m.f.FileInfo().Meta().PathInfo
                        }
       +
                        if m.pathInfo == nil {
                                panic(fmt.Sprintf("missing pathInfo in %v", m))
                        }
                }
        
       -        m.Staler = &resources.AtomicStaler{}
       -
                ps, err := func() (*pageState, error) {
                        if m.s == nil {
                                // Identify the Site/language to associate this Page with.
                                var lang string
       -                        if m.f != nil {
       +                        if pcfg.Lang != "" {
       +                                lang = pcfg.Lang
       +                        } else if m.f != nil {
                                        meta := m.f.FileInfo().Meta()
                                        lang = meta.Lang
                                        m.s = h.Sites[meta.LangIndex]
                                } else {
                                        lang = m.pathInfo.Lang()
                                }
       +                        if lang == "" {
       +                                lang = h.Conf.DefaultContentLanguage()
       +                        }
                                var found bool
                                for _, ss := range h.Sites {
                                        if ss.Lang() == lang {
       @@ -62,51 +123,49 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
                                                break
                                        }
                                }
       +
                                if !found {
                                        return nil, fmt.Errorf("no site found for language %q", lang)
                                }
       -
                        }
        
                        // Identify Page Kind.
       -                if m.kind == "" {
       -                        m.kind = kinds.KindSection
       +                if m.pageConfig.Kind == "" {
       +                        m.pageConfig.Kind = kinds.KindSection
                                if m.pathInfo.Base() == "/" {
       -                                m.kind = kinds.KindHome
       +                                m.pageConfig.Kind = kinds.KindHome
                                } else if m.pathInfo.IsBranchBundle() {
                                        // A section, taxonomy or term.
                                        tc := m.s.pageMap.cfg.getTaxonomyConfig(m.Path())
                                        if !tc.IsZero() {
                                                // Either a taxonomy or a term.
                                                if tc.pluralTreeKey == m.Path() {
       -                                                m.kind = kinds.KindTaxonomy
       +                                                m.pageConfig.Kind = kinds.KindTaxonomy
                                                } else {
       -                                                m.kind = kinds.KindTerm
       +                                                m.pageConfig.Kind = kinds.KindTerm
                                                }
                                        }
                                } 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
                        }
        
       -                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
        
                        if m.s.conf.Internal.Watch {
                                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{
                                pid:                               pid,
                                pageOutput:                        nopPageOutput,
       @@ -115,7 +174,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
                                Staler:                            m,
                                dependencyManager:                 dependencyManager,
                                pageCommon: &pageCommon{
       -                                content:                   cachedContent,
                                        FileProvider:              m,
                                        AuthorProvider:            m,
                                        Scratcher:                 maps.NewScratcher(),
       @@ -168,10 +226,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
                        ps.ShortcodeInfoProvider = ps
                        ps.AlternativeOutputFormatsProvider = ps
        
       -                if err := ps.setMetaPre(); err != nil {
       -                        return nil, ps.wrapError(err)
       -                }
       -
                        if err := ps.initLazyProviders(); err != nil {
                                return nil, ps.wrapError(err)
                        }
       @@ -182,5 +236,9 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, error) {
                        m.MarkStale()
                }
        
       -        return ps, err
       +        if ps == nil {
       +                return nil, nil, err
       +        }
       +
       +        return ps, ps.PathInfo(), err
        }
   DIR diff --git a/hugolib/page__paths.go b/hugolib/page__paths.go
       @@ -127,7 +127,7 @@ func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error)
                        Section:     pageInfoCurrentSection,
                        UglyURLs:    s.h.Conf.IsUglyURLs(p.Section()),
                        ForcePrefix: s.h.Conf.IsMultihost() || alwaysInSubDir,
       -                URL:         pm.urlPaths.URL,
       +                URL:         pm.pageConfig.URL,
                }
        
                if pm.Slug() != "" {
   DIR diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
       @@ -104,12 +104,12 @@ func (pco *pageContentOutput) Reset() {
        }
        
        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) {
       -        content := pco.po.p.content
       -        source, err := content.contentSource()
       +        content := pco.po.p.m.content
       +        source, err := content.pi.contentSource(content)
                if err != nil {
                        return "", err
                }
       @@ -125,7 +125,7 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
                        insertPlaceholders = true
                }
                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) {
                        case pageparser.Item:
                                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) {
       -        r, err := pco.po.p.content.contentRendered(ctx, pco)
       +        r, err := pco.po.p.m.content.contentRendered(ctx, pco)
                return r.content, err
        }
        
        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 {
       @@ -182,7 +182,7 @@ func (p *pageContentOutput) Len(ctx context.Context) int {
        }
        
        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 {
                        pco.fail(err)
                }
       @@ -190,7 +190,7 @@ func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSu
        }
        
        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 {
                        pco.fail(err)
                }
       @@ -270,7 +270,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
                }
        
                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
                        conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)
                        if err != nil {
       @@ -281,6 +281,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
                var rendered []byte
        
                parseInfo := &contentParseInfo{
       +                h:   pco.po.p.s.h,
                        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)
       -                if err := parseInfo.mapItems(contentToRenderb, s); err != nil {
       +                if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil {
                                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) {
                                        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 {
                                                        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
       -                pco.po.p.content.shortcodeState.transferNames(s)
       +                pco.po.p.m.content.shortcodeState.transferNames(s)
        
                } else {
                        c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
       @@ -411,7 +412,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
                        var renderCacheMu sync.Mutex
        
                        resolvePosition := func(ctx any) text.Position {
       -                        source := pco.po.p.content.mustSource()
       +                        source := pco.po.p.m.content.mustSource()
                                var offset int
        
                                switch v := ctx.(type) {
   DIR diff --git a/hugolib/params_test.go b/hugolib/params_test.go
       @@ -13,7 +13,11 @@
        
        package hugolib
        
       -import "testing"
       +import (
       +        "testing"
       +
       +        qt "github.com/frankban/quicktest"
       +)
        
        func TestFrontMatterParamsInItsOwnSection(t *testing.T) {
                t.Parallel()
       @@ -52,3 +56,106 @@ Summary: {{ .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)
       +}
   DIR diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
       @@ -315,7 +315,7 @@ func prepareShortcode(
                isRenderString bool,
        ) (shortcodeRenderer, 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)
                }
        
       @@ -443,7 +443,7 @@ func doRenderShortcode(
                                //     unchanged.
                                // 2   If inner does not have a newline, strip the wrapping <p> block and
                                //     the newline.
       -                        switch p.m.markup {
       +                        switch p.m.pageConfig.Markup {
                                case "", "markdown":
                                        if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match {
                                                cleaner, err := regexp.Compile(innerCleanupRegexp)
   DIR diff --git a/hugolib/site_new.go b/hugolib/site_new.go
       @@ -40,6 +40,7 @@ import (
                "github.com/gohugoio/hugo/navigation"
                "github.com/gohugoio/hugo/output"
                "github.com/gohugoio/hugo/publisher"
       +        "github.com/gohugoio/hugo/resources"
                "github.com/gohugoio/hugo/resources/page"
                "github.com/gohugoio/hugo/resources/page/pagemeta"
                "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",
                                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](),
                        currentSite:             sites[0],
                        skipRebuildForFilenames: make(map[string]bool),
   DIR diff --git a/hugolib/site_render.go b/hugolib/site_render.go
       @@ -129,7 +129,7 @@ func pageRenderer(
                                continue
                        }
        
       -                if p.m.buildConfig.PublishResources {
       +                if p.m.pageConfig.Build.PublishResources {
                                if err := p.renderResources(); err != nil {
                                        s.SendError(p.errorf(err, "failed to render page resources"))
                                        continue
   DIR diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go
       @@ -82,6 +82,14 @@ func (m PageMatcher) Matches(p Page) bool {
                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) {
                buildConfig := func(in any) (map[PageMatcher]maps.Params, any, error) {
                        cascade := make(map[PageMatcher]maps.Params)
       @@ -101,6 +109,11 @@ func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsCon
                                if err != nil {
                                        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)
                        }
        
   DIR diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go
       @@ -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");
        // 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/loggers"
       +        "github.com/gohugoio/hugo/common/maps"
                "github.com/gohugoio/hugo/common/paths"
       +        "github.com/gohugoio/hugo/resources/page"
        
                "github.com/gohugoio/hugo/helpers"
       -        "github.com/gohugoio/hugo/resources/resource"
        
                "github.com/gohugoio/hugo/config"
                "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.
        // Note that we currently have only extracted the date logic.
        type FrontMatterHandler struct {
       @@ -47,9 +108,6 @@ type FrontMatterHandler struct {
        // FrontMatterDescriptor describes how to handle front matter for a given Page.
        // It has pointers to values in the receiving page which gets updated.
        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
                // if page is a leaf bundle, the bundle folder name (ContentBaseName).
                BaseFilename string
       @@ -60,13 +118,8 @@ type FrontMatterDescriptor struct {
                // May be set from the author date in Git.
                GitAuthorDate time.Time
        
       -        // The below are pointers to values on Page and will be modified.
       -
       -        // This is the Page's dates.
       -        Dates *resource.Dates
       -
       -        // This is the Page's Slug etc.
       -        PageURLs *URLPath
       +        // The below will be modified.
       +        PageConfig *PageConfig
        
                // The Location to use to parse dates without time zone info.
                Location *time.Location
       @@ -83,8 +136,8 @@ var dateFieldAliases = map[string][]string{
        // supplied front matter params. Note that this requires all lower-case keys
        // in the params map.
        func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error {
       -        if d.Dates == nil {
       -                panic("missing dates")
       +        if d.PageConfig == nil {
       +                panic("missing pageConfig")
                }
        
                if f.dateHandler == nil {
       @@ -297,7 +350,7 @@ func (f *FrontMatterHandler) createHandlers() error {
        
                if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date,
                        func(d *FrontMatterDescriptor, t time.Time) {
       -                        d.Dates.FDate = t
       +                        d.PageConfig.Date = t
                                setParamIfNotSet(fmDate, t, d)
                        }); err != nil {
                        return err
       @@ -306,7 +359,7 @@ func (f *FrontMatterHandler) createHandlers() error {
                if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod,
                        func(d *FrontMatterDescriptor, t time.Time) {
                                setParamIfNotSet(fmLastmod, t, d)
       -                        d.Dates.FLastmod = t
       +                        d.PageConfig.Lastmod = t
                        }); err != nil {
                        return err
                }
       @@ -314,7 +367,7 @@ func (f *FrontMatterHandler) createHandlers() error {
                if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate,
                        func(d *FrontMatterDescriptor, t time.Time) {
                                setParamIfNotSet(fmPubDate, t, d)
       -                        d.Dates.FPublishDate = t
       +                        d.PageConfig.PublishDate = t
                        }); err != nil {
                        return err
                }
       @@ -322,7 +375,7 @@ func (f *FrontMatterHandler) createHandlers() error {
                if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate,
                        func(d *FrontMatterDescriptor, t time.Time) {
                                setParamIfNotSet(fmExpiryDate, t, d)
       -                        d.Dates.FExpiryDate = t
       +                        d.PageConfig.ExpiryDate = t
                        }); err != nil {
                        return err
                }
       @@ -331,10 +384,10 @@ func (f *FrontMatterHandler) createHandlers() error {
        }
        
        func setParamIfNotSet(key string, value any, d *FrontMatterDescriptor) {
       -        if _, found := d.Params[key]; found {
       +        if _, found := d.PageConfig.Params[key]; found {
                        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) {
       @@ -361,7 +414,7 @@ type frontmatterFieldHandlers int
        
        func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
                return func(d *FrontMatterDescriptor) (bool, error) {
       -                v, found := d.Params[key]
       +                v, found := d.PageConfig.Params[key]
        
                        if !found {
                                return false, nil
       @@ -377,7 +430,7 @@ func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d
                        setter(d, date)
        
                        // This is the params key as set in front matter.
       -                d.Params[key] = date
       +                d.PageConfig.Params[key] = date
        
                        return true, nil
                }
       @@ -392,9 +445,9 @@ func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMa
        
                        setter(d, date)
        
       -                if _, found := d.Params["slug"]; !found {
       +                if _, found := d.PageConfig.Params["slug"]; !found {
                                // Use slug from filename
       -                        d.PageURLs.Slug = slug
       +                        d.PageConfig.Slug = slug
                        }
        
                        return true, nil
   DIR diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go
       @@ -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");
        // 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/resources/page/pagemeta"
       -        "github.com/gohugoio/hugo/resources/resource"
        
                qt "github.com/frankban/quicktest"
        )
        
        func newTestFd() *pagemeta.FrontMatterDescriptor {
                return &pagemeta.FrontMatterDescriptor{
       -                Params:   make(map[string]any),
       -                Dates:    &resource.Dates{},
       -                PageURLs: &pagemeta.URLPath{},
       +                PageConfig: &pagemeta.PageConfig{
       +                        Params: make(map[string]interface{}),
       +                },
                        Location: time.UTC,
                }
        }
       @@ -105,16 +104,16 @@ func TestFrontMatterDatesHandlers(t *testing.T) {
                        case ":git":
                                d.GitAuthorDate = d1
                        }
       -                d.Params["date"] = d2
       +                d.PageConfig.Params["date"] = d2
                        c.Assert(handler.HandleDates(d), qt.IsNil)
       -                c.Assert(d.Dates.FDate, qt.Equals, d1)
       -                c.Assert(d.Params["date"], qt.Equals, d2)
       +                c.Assert(d.PageConfig.Dates.Date, qt.Equals, d1)
       +                c.Assert(d.PageConfig.Params["date"], qt.Equals, d2)
        
                        d = newTestFd()
       -                d.Params["date"] = d2
       +                d.PageConfig.Params["date"] = d2
                        c.Assert(handler.HandleDates(d), qt.IsNil)
       -                c.Assert(d.Dates.FDate, qt.Equals, d2)
       -                c.Assert(d.Params["date"], qt.Equals, d2)
       +                c.Assert(d.PageConfig.Dates.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")
                d := newTestFd()
       -        d.Params["mydate"] = testDate
       -        d.Params["date"] = testDate.Add(1 * 24 * time.Hour)
       -        d.Params["mypubdate"] = testDate.Add(2 * 24 * time.Hour)
       -        d.Params["publishdate"] = testDate.Add(3 * 24 * time.Hour)
       +        d.PageConfig.Params["mydate"] = testDate
       +        d.PageConfig.Params["date"] = testDate.Add(1 * 24 * time.Hour)
       +        d.PageConfig.Params["mypubdate"] = testDate.Add(2 * 24 * time.Hour)
       +        d.PageConfig.Params["publishdate"] = testDate.Add(3 * 24 * time.Hour)
        
                c.Assert(handler.HandleDates(d), qt.IsNil)
        
       -        c.Assert(d.Dates.FDate.Day(), qt.Equals, 1)
       -        c.Assert(d.Dates.FLastmod.Day(), qt.Equals, 2)
       -        c.Assert(d.Dates.FPublishDate.Day(), qt.Equals, 4)
       -        c.Assert(d.Dates.FExpiryDate.IsZero(), qt.Equals, true)
       +        c.Assert(d.PageConfig.Dates.Date.Day(), qt.Equals, 1)
       +        c.Assert(d.PageConfig.Dates.Lastmod.Day(), qt.Equals, 2)
       +        c.Assert(d.PageConfig.Dates.PublishDate.Day(), qt.Equals, 4)
       +        c.Assert(d.PageConfig.Dates.ExpiryDate.IsZero(), qt.Equals, true)
        }
   DIR diff --git a/resources/page/pagemeta/pagemeta.go b/resources/page/pagemeta/pagemeta.go
       @@ -17,13 +17,6 @@ import (
                "github.com/mitchellh/mapstructure"
        )
        
       -type URLPath struct {
       -        URL       string
       -        Permalink string
       -        Slug      string
       -        Section   string
       -}
       -
        const (
                Never       = "never"
                Always      = "always"
   DIR diff --git a/resources/resource/dates.go b/resources/resource/dates.go
       @@ -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");
        // you may not use this file except in compliance with the License.
       @@ -19,8 +19,6 @@ import (
                "github.com/gohugoio/hugo/common/htime"
        )
        
       -var _ Dated = Dates{}
       -
        // Dated wraps a "dated resource". These are the 4 dates that makes
        // the date logic in Hugo.
        type Dated interface {
       @@ -37,27 +35,6 @@ type Dated interface {
                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.
        func IsFuture(d Dated) bool {
                if d.PublishDate().IsZero() {
       @@ -79,19 +56,3 @@ func IsExpired(d Dated) bool {
        func IsZeroDates(d Dated) bool {
                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
       -}