Repository: gohugoio/hugo (language: go)

Issue:
Page.Resources.Get is broken in 0.123.0-DEV
The problem is: hugo now somehow converts resources names - it brings all to lowercase and replaces spaces with dashes, and .Resources.Get no longer work.

For example, we have a page resource named `Test 1.jpg`. Formerly `.Resources.Get "Test 1.jpg"` worked, but not with development version (we've tried it since we really need the optimizations that are coming next version). Also, dumping `.Resources` shows that internally hugo represents that resource name as `test-1.jpg`.

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.123.0-DEV+extended linux/amd64 BuildDate=unknown
</pre>

### Does this issue reproduce with the latest release?

No, it works fine in 0.122, but it is broken in dev version (commit ec22bb3).
Add warnidf template function
Ref: <https://discourse.gohugo.io/t/35565/7>

>  But what we may want to consider adding is a `Warnidf` function (we have a `Erroridf` function), that would allow the end user to turn off a given warning when he/she has “understood it”.


Discussion / hints:

Fix patch:
diff --git a/common/constants/constants.go b/common/constants/constants.go
index e4f5a63a279..f8f057e053b 100644
--- a/common/constants/constants.go
+++ b/common/constants/constants.go
@@ -13,12 +13,14 @@
 
 package constants
 
-// Error IDs.
+// Error/Warning IDs.
 // Do not change these values.
 const (
 	// IDs for remote errors in tpl/data.
 	ErrRemoteGetJSON = "error-remote-getjson"
 	ErrRemoteGetCSV  = "error-remote-getcsv"
+
+	WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
 )
 
 // Field/method names with special meaning.
diff --git a/common/loggers/logger.go b/common/loggers/logger.go
index c4d81fb8305..f851513b3eb 100644
--- a/common/loggers/logger.go
+++ b/common/loggers/logger.go
@@ -179,9 +179,9 @@ type Logger interface {
 	Debugln(v ...any)
 	Error() logg.LevelLogger
 	Errorf(format string, v ...any)
+	Erroridf(id, format string, v ...any)
 	Errorln(v ...any)
 	Errors() string
-	Errorsf(id, format string, v ...any)
 	Info() logg.LevelLogger
 	InfoCommand(command string) logg.LevelLogger
 	Infof(format string, v ...any)
@@ -197,6 +197,7 @@ type Logger interface {
 	Warn() logg.LevelLogger
 	WarnCommand(command string) logg.LevelLogger
 	Warnf(format string, v ...any)
+	Warnidf(id, format string, v ...any)
 	Warnln(v ...any)
 	Deprecatef(fail bool, format string, v ...any)
 	Trace(s logg.StringFunc)
@@ -321,10 +322,20 @@ func (l *logAdapter) Errors() string {
 	return l.errors.String()
 }
 
-func (l *logAdapter) Errorsf(id, format string, v ...any) {
+func (l *logAdapter) Erroridf(id, format string, v ...any) {
+	format += l.idfInfoStatement("error", id, format)
 	l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...)
 }
 
+func (l *logAdapter) Warnidf(id, format string, v ...any) {
+	format += l.idfInfoStatement("warning", id, format)
+	l.warnl.WithField(FieldNameStatementID, id).Logf(format, v...)
+}
+
+func (l *logAdapter) idfInfoStatement(what, id, format string) string {
+	return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id)
+}
+
 func (l *logAdapter) Trace(s logg.StringFunc) {
 	l.tracel.Log(s)
 }
diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
index 5788e792bdb..26e40230537 100644
--- a/config/allconfig/allconfig.go
+++ b/config/allconfig/allconfig.go
@@ -206,7 +206,7 @@ func (c Config) cloneForLang() *Config {
 	x.DisableKinds = copyStringSlice(x.DisableKinds)
 	x.DisableLanguages = copyStringSlice(x.DisableLanguages)
 	x.MainSections = copyStringSlice(x.MainSections)
-	x.IgnoreErrors = copyStringSlice(x.IgnoreErrors)
+	x.IgnoreLogs = copyStringSlice(x.IgnoreLogs)
 	x.IgnoreFiles = copyStringSlice(x.IgnoreFiles)
 	x.Theme = copyStringSlice(x.Theme)
 
@@ -299,9 +299,9 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
 		}
 	}
 
-	ignoredErrors := make(map[string]bool)
-	for _, err := range c.IgnoreErrors {
-		ignoredErrors[strings.ToLower(err)] = true
+	ignoredLogIDs := make(map[string]bool)
+	for _, err := range c.IgnoreLogs {
+		ignoredLogIDs[strings.ToLower(err)] = true
 	}
 
 	baseURL, err := urls.NewBaseURLFromString(c.BaseURL)
@@ -357,7 +357,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
 		BaseURLLiveReload: baseURL,
 		DisabledKinds:     disabledKinds,
 		DisabledLanguages: disabledLangs,
-		IgnoredErrors:     ignoredErrors,
+		IgnoredLogs:       ignoredLogIDs,
 		KindOutputFormats: kindOutputFormats,
 		CreateTitle:       helpers.GetTitleFunc(c.TitleCaseStyle),
 		IsUglyURLSection:  isUglyURL,
@@ -394,7 +394,7 @@ type ConfigCompiled struct {
 	KindOutputFormats map[string]output.Formats
 	DisabledKinds     map[string]bool
 	DisabledLanguages map[string]bool
-	IgnoredErrors     map[string]bool
+	IgnoredLogs       map[string]bool
 	CreateTitle       func(s string) string
 	IsUglyURLSection  func(section string) bool
 	IgnoreFile        func(filename string) bool
@@ -501,8 +501,8 @@ type RootConfig struct {
 	// Enable to disable the build lock file.
 	NoBuildLock bool
 
-	// A list of error IDs to ignore.
-	IgnoreErrors []string
+	// A list of log IDs to ignore.
+	IgnoreLogs []string
 
 	// A list of regexps that match paths to ignore.
 	// Deprecated: Use the settings on module imports.
diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go
index 71bd232de17..0b4c7427832 100644
--- a/config/allconfig/configlanguage.go
+++ b/config/allconfig/configlanguage.go
@@ -89,8 +89,8 @@ func (c ConfigLanguage) IsLangDisabled(lang string) bool {
 	return c.config.C.DisabledLanguages[lang]
 }
 
-func (c ConfigLanguage) IgnoredErrors() map[string]bool {
-	return c.config.C.IgnoredErrors
+func (c ConfigLanguage) IgnoredLogs() map[string]bool {
+	return c.config.C.IgnoredLogs
 }
 
 func (c ConfigLanguage) NoBuildLock() bool {
diff --git a/config/allconfig/load.go b/config/allconfig/load.go
index eceed31f4ba..cb267422f15 100644
--- a/config/allconfig/load.go
+++ b/config/allconfig/load.go
@@ -141,6 +141,7 @@ func (l configLoader) applyConfigAliases() error {
 		{Key: "indexes", Value: "taxonomies"},
 		{Key: "logI18nWarnings", Value: "printI18nWarnings"},
 		{Key: "logPathWarnings", Value: "printPathWarnings"},
+		{Key: "ignoreErrors", Value: "ignoreLogs"},
 	}
 
 	for _, alias := range aliases {
diff --git a/config/configProvider.go b/config/configProvider.go
index 2536639ead9..21d832f17e0 100644
--- a/config/configProvider.go
+++ b/config/configProvider.go
@@ -67,7 +67,7 @@ type AllProvider interface {
 	NewContentEditor() string
 	Timeout() time.Duration
 	StaticDirs() []string
-	IgnoredErrors() map[string]bool
+	IgnoredLogs() map[string]bool
 	WorkingDir() string
 	EnableEmoji() bool
 }
diff --git a/go.mod b/go.mod
index e8beedbfb56..c74972d0bb8 100644
--- 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
diff --git a/go.sum b/go.sum
index 3c1cfe7ff3b..4795d0d688a 100644
--- 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=
diff --git a/hugolib/content_map.go b/hugolib/content_map.go
index fefa90bf152..96013c4ed8c 100644
--- 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,
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
index 536f23ccd3f..9accd190ec4 100644
--- 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
 			}
 
@@ -1545,7 +1548,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
 						return false, nil
 					}
 
-					relPathOriginal := rs.path.PathRel(ps.m.pathInfo)
+					relPathOriginal := rs.path.Unmormalized().PathRel(ps.m.pathInfo.Unmormalized())
 					relPath := rs.path.BaseRel(ps.m.pathInfo)
 
 					var targetBasePaths []string
@@ -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)
 		}
 	}
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
index 80e75445374..1b284061734 100644
--- 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.
diff --git a/hugolib/page.go b/hugolib/page.go
index f8ec5e22578..822b7c021f4 100644
--- 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
diff --git a/hugolib/page__common.go b/hugolib/page__common.go
index 0881affe7be..164776842e7 100644
--- 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
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
index 64ce83f0ebf..62e78c61271 100644
--- 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
 			}
diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
index a549356fd93..e4cb2b83bc3 100644
--- 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.
@@ -31,6 +31,7 @@ import (
 
 	"github.com/gohugoio/hugo/source"
 
+	"github.com/gohugoio/hugo/common/constants"
 	"github.com/gohugoio/hugo/common/hugo"
 	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/common/paths"
@@ -48,13 +49,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 +65,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 +81,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 +131,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 +156,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 +168,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 +191,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 +215,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 +245,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 +267,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 +325,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 +346,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 +373,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 +388,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 +409,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 +425,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 +454,7 @@ func (p *pageState) setMetaPostParams() error {
 				return err
 			}
 			userParams = vv
-			delete(pm.params, k)
+			delete(pcfg.Params, k)
 			continue
 		}
 
@@ -450,43 +473,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 +524,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 +585,7 @@ func (p *pageState) setMetaPostParams() error {
 			}
 
 			if handled {
-				pm.params[loki] = resources
-				pm.resourcesMetadata = resources
+				pcfg.Resources = resources
 				break
 			}
 			fallthrough
@@ -586,51 +607,54 @@ 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
+		if _, found := params[k]; found {
+			p.s.Log.Warnidf(constants.WarnFrontMatterParamsOverrides, "Hugo front matter key %q is overridden in params section.", k)
+		}
+		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 +667,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 +691,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 +791,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 +829,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)
diff --git a/hugolib/page__new.go b/hugolib/page__new.go
index 89eeb2e0e37..3bef55f43fd 100644
--- 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
 }
diff --git a/hugolib/page__paths.go b/hugolib/page__paths.go
index 6e7980a6d29..b0d2009e432 100644
--- 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() != "" {
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index 3d86cdece90..e0026ed1fbd 100644
--- 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) {
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index c5125f717d0..0ec5d5ce462 100644
--- 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)
diff --git a/hugolib/site_new.go b/hugolib/site_new.go
index ddf45c28696..debb81e77b1 100644
--- 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"
@@ -123,7 +124,7 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
 			Stdout:             cfg.LogOut,
 			Stderr:             cfg.LogOut,
 			StoreErrors:        conf.Running(),
-			SuppressStatements: conf.IgnoredErrors(),
+			SuppressStatements: conf.IgnoredLogs(),
 		}
 		logger = loggers.New(logOpts)
 	}
@@ -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),
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index 379dd6e867b..138530680aa 100644
--- 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
diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go
index 8529b0e2810..fbdb25d7247 100644
--- 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)
 		}
 
diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go
index d804f27a711..8abd3807fe4 100644
--- 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,77 @@ 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 four 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.
+	URL            string   // The URL to the rendered page, e.g. /sect/mypage.html.
+	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 +109,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 +119,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 +137,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 +351,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 +360,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 +368,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 +376,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 +385,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 +415,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 +431,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 +446,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
diff --git a/resources/page/pagemeta/pagemeta.go b/resources/page/pagemeta/pagemeta.go
index 94c6b00aaef..f5b6380bc20 100644
--- 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"
diff --git a/resources/resource/dates.go b/resources/resource/dates.go
index 88968750def..d84e26d57e4 100644
--- 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
-}
diff --git a/tpl/data/data.go b/tpl/data/data.go
index 7eb730c4c06..78f1f3f48e2 100644
--- a/tpl/data/data.go
+++ b/tpl/data/data.go
@@ -94,7 +94,7 @@ func (ns *Namespace) GetCSV(sep string, args ...any) (d [][]string, err error) {
 		if security.IsAccessDenied(err) {
 			return nil, err
 		}
-		ns.deps.Log.Errorsf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err)
+		ns.deps.Log.Erroridf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err)
 		return nil, nil
 	}
 
@@ -132,7 +132,7 @@ func (ns *Namespace) GetJSON(args ...any) (any, error) {
 		if security.IsAccessDenied(err) {
 			return nil, err
 		}
-		ns.deps.Log.Errorsf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err)
+		ns.deps.Log.Erroridf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err)
 		return nil, nil
 	}
 
diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go
index 4f18758e043..04dbd339c56 100644
--- a/tpl/fmt/fmt.go
+++ b/tpl/fmt/fmt.go
@@ -68,9 +68,7 @@ func (ns *Namespace) Errorf(format string, args ...any) string {
 // an information text that the error with the given id can be suppressed in config.
 // It returns an empty string.
 func (ns *Namespace) Erroridf(id, format string, args ...any) string {
-	format += "\nYou can suppress this error by adding the following to your site configuration:\nignoreErrors = ['%s']"
-	args = append(args, id)
-	ns.logger.Errorsf(id, format, args...)
+	ns.logger.Erroridf(id, format, args...)
 	return ""
 }
 
@@ -81,6 +79,14 @@ func (ns *Namespace) Warnf(format string, args ...any) string {
 	return ""
 }
 
+// Warnidf formats args according to a format specifier and logs an WARNING and
+// an information text that the warning with the given id can be suppressed in config.
+// It returns an empty string.
+func (ns *Namespace) Warnidf(id, format string, args ...any) string {
+	ns.logger.Warnidf(id, format, args...)
+	return ""
+}
+
 // Warnmf is epxermimental and subject to change at any time.
 func (ns *Namespace) Warnmf(m any, format string, args ...any) string {
 	return ns.logmf(ns.logger.Warn(), m, format, args...)
diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go
index 8000627e211..701bd3b6a6a 100644
--- a/tpl/fmt/init.go
+++ b/tpl/fmt/init.go
@@ -66,6 +66,13 @@ func init() {
 			},
 		)
 
+		ns.AddMethodMapping(ctx.Warnidf,
+			[]string{"warnidf"},
+			[][2]string{
+				{`{{ warnidf "my-warn-id" "%s." "warning" }}`, ``},
+			},
+		)
+
 		ns.AddMethodMapping(ctx.Warnf,
 			[]string{"warnf"},
 			[][2]string{


Test patch:
diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go
index a41b2aae916..7843ad28589 100644
--- a/hugolib/content_map_test.go
+++ b/hugolib/content_map_test.go
@@ -280,3 +280,22 @@ P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}|
 		filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
 	)
 }
+
+// Issue #11944
+func TestBundleResourcesGetWithSpacesInFilename(t *testing.T) {
+	files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["taxonomy", "term"]
+-- content/bundle/index.md --
+-- content/bundle/data with Spaces.txt --
+Data.
+-- layouts/index.html --
+{{ $bundle := site.GetPage "bundle" }}
+{{ $r := $bundle.Resources.Get "data with Spaces.txt" }}
+R: {{ with $r }}{{ .Content }}{{ end }}|
+`
+	b := Test(t, files)
+
+	b.AssertFileContent("public/index.html", "R: Data.")
+}
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index 3fec04df0f7..222c8ec2dd7 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -57,6 +57,13 @@ func TestOptDebug() TestOpt {
 	}
 }
 
+// TestOptWarn will enable warn logging in integration tests.
+func TestOptWarn() TestOpt {
+	return func(c *IntegrationTestConfig) {
+		c.LogLevel = logg.LevelWarn
+	}
+}
+
 // TestOptWithNFDOnDarwin will normalize the Unicode filenames to NFD on Darwin.
 func TestOptWithNFDOnDarwin() TestOpt {
 	return func(c *IntegrationTestConfig) {
@@ -80,6 +87,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 {
@@ -172,9 +188,18 @@ func (b *lockingBuffer) Write(p []byte) (n int, err error) {
 	return
 }
 
-func (s *IntegrationTestBuilder) AssertLogContains(text string) {
+func (s *IntegrationTestBuilder) AssertLogContains(els ...string) {
 	s.Helper()
-	s.Assert(s.logBuff.String(), qt.Contains, text)
+	for _, el := range els {
+		s.Assert(s.logBuff.String(), qt.Contains, el)
+	}
+}
+
+func (s *IntegrationTestBuilder) AssertLogNotContains(els ...string) {
+	s.Helper()
+	for _, el := range els {
+		s.Assert(s.logBuff.String(), qt.Not(qt.Contains), el)
+	}
 }
 
 func (s *IntegrationTestBuilder) AssertLogMatches(expression string) {
diff --git a/hugolib/params_test.go b/hugolib/params_test.go
index 32b4bd7c339..6f890b43b78 100644
--- 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,128 @@ 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 TestFrontMatterTitleOverrideWarn(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+baseURL = "https://example.org/"
+disableKinds = ["taxonomy", "term"]
+-- content/p1.md --
+---
+title: "My title"
+params:
+  title: "My title from params"
+---
+
+
+`
+
+	b := Test(t, files, TestOptWarn())
+
+	b.AssertLogContains("ARN  Hugo front matter key \"title\" is overridden in params section", "You can suppress this warning")
+}
+
+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)
+}
diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go
index 1aff8b511a5..9e1151f22c2 100644
--- 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)
 }
diff --git a/tpl/fmt/fmt_integration_test.go b/tpl/fmt/fmt_integration_test.go
index 40bfefcdc4e..74322770e08 100644
--- a/tpl/fmt/fmt_integration_test.go
+++ b/tpl/fmt/fmt_integration_test.go
@@ -16,6 +16,7 @@ package fmt_test
 import (
 	"testing"
 
+	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/hugolib"
 )
 
@@ -32,13 +33,25 @@ ignoreErrors = ['error-b']
 {{ erroridf "error-b" "%s" "b"}}
   `
 
-	b := hugolib.NewIntegrationTestBuilder(
-		hugolib.IntegrationTestConfig{
-			T:           t,
-			TxtarString: files,
-		},
-	)
+	b, err := hugolib.TestE(t, files)
 
-	b.BuildE()
-	b.AssertLogMatches(`^ERROR a\nYou can suppress this error by adding the following to your site configuration:\nignoreErrors = \['error-a'\]\n$`)
+	b.Assert(err, qt.IsNotNil)
+	b.AssertLogMatches(`^ERROR a\nYou can suppress this error by adding the following to your site configuration:\nignoreLogs = \['error-a'\]\n$`)
+}
+
+func TestWarnidf(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+ignoreLogs = ['warning-b']
+-- layouts/index.html --
+{{ warnidf "warning-a" "%s" "a"}}
+{{ warnidf "warning-b" "%s" "b"}}
+  `
+
+	b := hugolib.Test(t, files, hugolib.TestOptWarn())
+	b.AssertLogContains("WARN  a", "You can suppress this warning", "ignoreLogs", "['warning-a']")
+	b.AssertLogNotContains("['warning-b']")
 }