Repository: gohugoio/hugo (language: go)

Issue:
Add page.Store (a page.Scratch variant)
We currently have a `page.Scratch` as a _scratch pad_ for each page. 

This gets reset on server rebuilds, and that's not something we can/want change.

This has the unfortunate side effect that scratch values written to page in, say, a shortcode, is not visible on server rebuilds (unless the content is also rebuilt).

I have thought a fair amount on the above, and have concluded that the easiest is just to add a new method:

```go
type ScratchProvider interface {
	// Scratch returns a Scratch that can be used to store temporary state.
	// Note that this Scratch gets reset on server rebuilds. See Store() for a variant that survives.
	Scratch() *maps.Scratch

	// Store returns a Scratch that can be used to store temporary state.
	// In contrast to Scratch(), this Scratch is not reset on server rebuilds.
	Store() *maps.Scratch
}
```

/cc @jmooring @regisphilibert 


Discussion / hints:

Fix patch:
diff --git a/hugolib/page__common.go b/hugolib/page__common.go
index bf11ae7d671..0294393e42b 100644
--- a/hugolib/page__common.go
+++ b/hugolib/page__common.go
@@ -60,6 +60,9 @@ type pageCommon struct {
 	// Lazily initialized dependencies.
 	init *lazy.Init
 
+	// Store holds state that survives server rebuilds.
+	store *maps.Scratch
+
 	// All of these represents the common parts of a page.Page
 	maps.Scratcher
 	navigation.PageMenusProvider
@@ -134,6 +137,10 @@ type pageCommon struct {
 	forceRender bool
 }
 
+func (p *pageCommon) Store() *maps.Scratch {
+	return p.store
+}
+
 type pagePages struct {
 	pagesInit sync.Once
 	pages     page.Pages
diff --git a/hugolib/page__new.go b/hugolib/page__new.go
index 5efebe4f599..91847784313 100644
--- a/hugolib/page__new.go
+++ b/hugolib/page__new.go
@@ -41,6 +41,7 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) {
 			FileProvider:            metaProvider,
 			AuthorProvider:          metaProvider,
 			Scratcher:               maps.NewScratcher(),
+			store:                   maps.NewScratch(),
 			Positioner:              page.NopPage,
 			InSectionPositioner:     page.NopPage,
 			ResourceMetaProvider:    metaProvider,
diff --git a/resources/page/page.go b/resources/page/page.go
index d1790806ead..1ad536e9436 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -261,7 +261,15 @@ type PageWithoutContent interface {
 	// Helper methods
 	ShortcodeInfoProvider
 	compare.Eqer
+
+	// Scratch returns a Scratch that can be used to store temporary state.
+	// Note that this Scratch gets reset on server rebuilds. See Store() for a variant that survives.
 	maps.Scratcher
+
+	// Store returns a Scratch that can be used to store temporary state.
+	// In contrast to Scratch(), this Scratch is not reset on server rebuilds.
+	Store() *maps.Scratch
+
 	RelatedKeywordsProvider
 
 	// GetTerms gets the terms of a given taxonomy,
diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go
index 011fabfc05c..fd706f99405 100644
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -418,6 +418,10 @@ func (p *nopPage) Scratch() *maps.Scratch {
 	return nil
 }
 
+func (p *nopPage) Store() *maps.Scratch {
+	return nil
+}
+
 func (p *nopPage) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
 	return nil, nil
 }


Test patch:
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
index 6b35e48144a..1edef622bbe 100644
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -1769,7 +1769,7 @@ Summary: In Chinese, 好 means good.
 	b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 652")
 }
 
-func TestScratchSite(t *testing.T) {
+func TestScratch(t *testing.T) {
 	t.Parallel()
 
 	b := newTestSitesBuilder(t)
@@ -1796,6 +1796,50 @@ title: Scratch Me!
 	b.AssertFileContent("public/scratchme/index.html", "C: cv")
 }
 
+func TestScratchRebuild(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+-- content/p1.md --
+---
+title: "p1"
+---
+{{< scratchme >}}
+-- layouts/shortcodes/foo.html --
+notused
+-- layouts/shortcodes/scratchme.html --
+{{ .Page.Scratch.Set "scratch" "foo" }}
+{{ .Page.Store.Set "scratch" "bar" }}
+-- layouts/_default/single.html --
+{{ .Content }}
+Scratch: {{ .Scratch.Get "scratch" }}|
+Store: {{ .Store.Get "scratch" }}|
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			Running:     true,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", `
+Scratch: foo|
+Store: bar|
+	`)
+
+	b.EditFiles("layouts/shortcodes/foo.html", "edit")
+
+	b.Build()
+
+	b.AssertFileContent("public/p1/index.html", `
+Scratch: |
+Store: bar|
+	`)
+}
+
 func TestPageParam(t *testing.T) {
 	t.Parallel()
 
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
index 57077ecf871..df4e79db484 100644
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -498,6 +498,10 @@ func (p *testPage) Scratch() *maps.Scratch {
 	panic("not implemented")
 }
 
+func (p *testPage) Store() *maps.Scratch {
+	panic("not implemented")
+}
+
 func (p *testPage) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
 	v, err := p.Param(cfg.Name)
 	if err != nil {