Repository: gohugoio/hugo (language: go)

Issue:
Add .RenderShortcodes
I have been hit by this issue twice recently -- so I assume this is a problem worth solving (I know there are similar issue, and this also relates to `.RenderString`.

Currently we have:

* `.Content` which gives rendered HTML with shortcodes rendered.
* `.RawContent` which gives the raw source excluding front matter. No shortcodes rendered.

A common pattern is to include files using `.GetPage` via a shortcode, e.g.:

```md

# Title

{{< include "foo.md" >}}

```

Using the `{{<` means you can include just about anything and it will be preserved as-is. But it will not partake in the global rendering context re. table of contents, foot notes etc.

You can do this in your shortcode:

```html
{{ (site.GetPage "foo.md").RawContent n}}
```

And then

```md

# Title

{{% include "foo.md" %}}

```

The above will work but any shortcodes in `foo.md` will not be rendered.

Which this issue is all about.

I assume that it makes sense that `.RenderString` uses this method internally by default, but what I need is basically:

```html
{{ $markdownWithRenderedShortcodes := .RawContent | .RenderShortcodes }}
```

These should also work:

```html
{{ $markdownWithRenderedShortcodes := .RenderShortcodes }}
{{ $markdownWithRenderedShortcodes := "string with shortcodes" | .RenderShortcodes }}
```

/cc @regisphilibert @onedrawingperday @digitalcraftsman @moorereason  and gang

And yes, this is tightly connected to a `Page` so I think a method makes sense.







Discussion / hints:
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.

Would this feature, if implemented, render the shortcodes for the current output format? That'd be super great!
Yes.
Is this going to get implemented at all any time soon? People keep asking how to build my PR and since no equivalent functionality exists in mainline and @bep outright rejected my PR likely as they felt this was a better way to implement this (and therefore it became _the_ only implementation they'd accept, apparently) I don't have any actual alternative option to get shortcodes processed in JSON output.
Checking your PR it was rejected not outright for a feeling but for missing formalities. Open an issue, describe and discuss your issue, then if it is deemed a good addition to hugo open a PR referencing that issue number.
A need for this came up again yesterday:
<https://discourse.gohugo.io/t/export-to-markdown-with-shortcodes/39590>
This came up again as the potential solution to a forum issue I posted: https://discourse.gohugo.io/t/table-of-contents-render-hook/45352

Fix patch:
diff --git a/hugolib/page.go b/hugolib/page.go
index f8f96615688..81ba68aa42a 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -114,6 +114,10 @@ func (pa pageSiteAdapter) GetPage(ref string) (page.Page, error) {
 }
 
 type pageState struct {
+	// Incremented for each new page created.
+	// Note that this will change between builds for a given Page.
+	id int
+
 	// This slice will be of same length as the number of global slice of output
 	// formats (for all sites).
 	pageOutputs []*pageOutput
@@ -772,7 +776,7 @@ Loop:
 			currShortcode.pos = it.Pos()
 			currShortcode.length = iter.Current().Pos() - it.Pos()
 			if currShortcode.placeholder == "" {
-				currShortcode.placeholder = createShortcodePlaceholder("s", currShortcode.ordinal)
+				currShortcode.placeholder = createShortcodePlaceholder("s", p.id, currShortcode.ordinal)
 			}
 
 			if currShortcode.name != "" {
@@ -784,7 +788,7 @@ Loop:
 				currShortcode.params = s
 			}
 
-			currShortcode.placeholder = createShortcodePlaceholder("s", ordinal)
+			currShortcode.placeholder = createShortcodePlaceholder("s", p.id, ordinal)
 			ordinal++
 			s.shortcodes = append(s.shortcodes, currShortcode)
 
diff --git a/hugolib/page__new.go b/hugolib/page__new.go
index 14db28c3df6..108e5717f3f 100644
--- a/hugolib/page__new.go
+++ b/hugolib/page__new.go
@@ -31,14 +31,19 @@ import (
 	"github.com/gohugoio/hugo/resources/page"
 )
 
+var pageIdCounter atomic.Int64
+
 func newPageBase(metaProvider *pageMeta) (*pageState, error) {
 	if metaProvider.s == nil {
 		panic("must provide a Site")
 	}
 
+	id := int(pageIdCounter.Add(1))
+
 	s := metaProvider.s
 
 	ps := &pageState{
+		id:                                id,
 		pageOutput:                        nopPageOutput,
 		pageOutputTemplateVariationsState: atomic.NewUint32(0),
 		pageCommon: &pageCommon{
diff --git a/hugolib/page__output.go b/hugolib/page__output.go
index 25ce26b7a4f..21f58e79586 100644
--- a/hugolib/page__output.go
+++ b/hugolib/page__output.go
@@ -86,6 +86,7 @@ type pageOutput struct {
 	page.ContentProvider
 	page.PageRenderProvider
 	page.TableOfContentsProvider
+	page.RenderShortcodesProvider
 
 	// May be nil.
 	cp *pageContentOutput
@@ -99,6 +100,7 @@ func (p *pageOutput) initContentProvider(cp *pageContentOutput) {
 	p.ContentProvider = cp
 	p.PageRenderProvider = cp
 	p.TableOfContentsProvider = cp
+	p.RenderShortcodesProvider = cp
 	p.cp = cp
 
 }
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index 89dc5ac77a6..e806ca339ec 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -103,6 +103,30 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
 			return err
 		}
 
+		ctxCallback := func(cp2 *pageContentOutput) {
+			cp.p.cmap.hasNonMarkdownShortcode = cp.p.cmap.hasNonMarkdownShortcode || cp2.p.cmap.hasNonMarkdownShortcode
+			// Merge content placeholders
+			for k, v := range cp2.contentPlaceholders {
+				cp.contentPlaceholders[k] = v
+			}
+
+			if p.s.watching() {
+				for _, s := range cp2.p.shortcodeState.shortcodes {
+					for _, templ := range s.templs {
+						dependencyTracker.Add(templ.(identity.Manager))
+					}
+				}
+			}
+
+			// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
+			cp.p.shortcodeState.transferNames(cp2.p.shortcodeState)
+			if cp2.p.pageOutputTemplateVariationsState.Load() == 2 {
+				cp.p.pageOutputTemplateVariationsState.Store(2)
+			}
+		}
+
+		ctx = tpl.SetCallbackFunctionInContext(ctx, ctxCallback)
+
 		var hasVariants bool
 		cp.workContent, hasVariants, err = p.contentToRender(ctx, p.source.parsed, p.cmap, cp.contentPlaceholders)
 		if err != nil {
@@ -350,6 +374,63 @@ func (p *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Frag
 	return p.tableOfContents
 }
 
+func (p *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) {
+	p.p.s.initInit(ctx, p.initToC, p.p)
+	source := p.p.source.parsed.Input()
+	renderedShortcodes := p.contentPlaceholders
+	var insertPlaceholders bool
+	var hasVariants bool
+	var cb func(*pageContentOutput)
+	if v := tpl.GetCallbackFunctionFromContext(ctx); v != nil {
+		if fn, ok := v.(func(*pageContentOutput)); ok {
+			insertPlaceholders = true
+			cb = fn
+		}
+	}
+	c := make([]byte, 0, len(source)+(len(source)/10))
+	for _, it := range p.p.cmap.items {
+		switch v := it.(type) {
+		case pageparser.Item:
+			c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
+		case pageContentReplacement:
+			// Ignore.
+		case *shortcode:
+			if !insertPlaceholders || !v.insertPlaceholder() {
+				// Insert the rendered shortcode.
+				renderedShortcode, found := renderedShortcodes[v.placeholder]
+				if !found {
+					// This should never happen.
+					panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
+				}
+
+				b, more, err := renderedShortcode.renderShortcode(ctx)
+				if err != nil {
+					return "", fmt.Errorf("failed to render shortcode: %w", err)
+				}
+				hasVariants = hasVariants || more
+				c = append(c, []byte(b)...)
+
+			} else {
+				// Insert the placeholder so we can insert the content after
+				// markdown processing.
+				c = append(c, []byte(v.placeholder)...)
+			}
+		default:
+			panic(fmt.Sprintf("unknown item type %T", it))
+		}
+	}
+
+	if hasVariants {
+		p.p.pageOutputTemplateVariationsState.Store(2)
+	}
+
+	if cb != nil {
+		cb(p)
+	}
+
+	return helpers.BytesToHTML(c), nil
+}
+
 func (p *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
 	p.p.s.initInit(ctx, p.initToC, p.p)
 	return p.tableOfContentsHTML
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index c12fb888b06..e201d4d6ba3 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -185,8 +185,8 @@ func (scp *ShortcodeWithPage) page() page.Page {
 // Note - this value must not contain any markup syntax
 const shortcodePlaceholderPrefix = "HAHAHUGOSHORTCODE"
 
-func createShortcodePlaceholder(id string, ordinal int) string {
-	return shortcodePlaceholderPrefix + id + strconv.Itoa(ordinal) + "HBHB"
+func createShortcodePlaceholder(sid string, id, ordinal int) string {
+	return shortcodePlaceholderPrefix + strconv.Itoa(id) + sid + strconv.Itoa(ordinal) + "HBHB"
 }
 
 type shortcode struct {
diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go
index 20fa22d2fe6..f351daae0bc 100644
--- a/hugolib/shortcode_page.go
+++ b/hugolib/shortcode_page.go
@@ -21,7 +21,7 @@ import (
 )
 
 // A placeholder for the TableOfContents markup. This is what we pass to the Goldmark etc. renderers.
-var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0)
+var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0, 0)
 
 // shortcodeRenderer is typically used to delay rendering of inner shortcodes
 // marked with placeholders in the content.
diff --git a/resources/page/page.go b/resources/page/page.go
index b1c86719588..54fc71f3310 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -277,6 +277,7 @@ type PageRenderProvider interface {
 // PageWithoutContent is the Page without any of the content methods.
 type PageWithoutContent interface {
 	RawContentProvider
+	RenderShortcodesProvider
 	resource.Resource
 	PageMetaProvider
 	resource.LanguageProvider
@@ -362,6 +363,11 @@ type RawContentProvider interface {
 	RawContent() string
 }
 
+type RenderShortcodesProvider interface {
+	// RenderShortcodes returns RawContent with any shortcodes rendered.
+	RenderShortcodes(context.Context) (template.HTML, error)
+}
+
 // RefProvider provides the methods needed to create reflinks to pages.
 type RefProvider interface {
 	// Ref returns an absolute URl to a page.
diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go
index 59765ebf296..735d6eea808 100644
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -401,6 +401,10 @@ func (p *nopPage) RawContent() string {
 	return ""
 }
 
+func (p *nopPage) RenderShortcodes(ctx context.Context) (template.HTML, error) {
+	return "", nil
+}
+
 func (p *nopPage) ReadingTime(context.Context) int {
 	return 0
 }
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
index acfa2f16630..78be55e1858 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -60,9 +60,10 @@ func NewExecuter(helper ExecHelper) Executer {
 }
 
 type (
-	pageContextKeyType    string
-	hasLockContextKeyType string
-	stackContextKeyType   string
+	pageContextKeyType     string
+	hasLockContextKeyType  string
+	stackContextKeyType    string
+	callbackContextKeyType string
 )
 
 const (
@@ -70,6 +71,9 @@ const (
 	PageContextKey = pageContextKeyType("page")
 	// Used in partialCached to signal to nested templates that a lock is already taken.
 	HasLockContextKey = hasLockContextKeyType("hasLock")
+
+	// Used to pass down a callback function to nested templates.
+	CallbackContextKey = callbackContextKeyType("callback")
 )
 
 // Note: The context is currently not fully implemented in Hugo. This is a work in progress.
diff --git a/tpl/template.go b/tpl/template.go
index 91a482c2504..70fdf2d3d21 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -191,6 +191,14 @@ func SetHasLockInContext(ctx context.Context, hasLock bool) context.Context {
 	return context.WithValue(ctx, texttemplate.HasLockContextKey, hasLock)
 }
 
+func GetCallbackFunctionFromContext(ctx context.Context) any {
+	return ctx.Value(texttemplate.CallbackContextKey)
+}
+
+func SetCallbackFunctionInContext(ctx context.Context, fn any) context.Context {
+	return context.WithValue(ctx, texttemplate.CallbackContextKey, fn)
+}
+
 const hugoNewLinePlaceholder = "___hugonl_"
 
 var (


Test patch:
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
index 5237b634096..fd115385dd9 100644
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -1998,7 +1998,6 @@ func TestRenderWithoutArgument(t *testing.T) {
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
 		},
 	).BuildE()
 
diff --git a/hugolib/rendershortcodes_test.go b/hugolib/rendershortcodes_test.go
new file mode 100644
index 00000000000..c6fa711cce7
--- /dev/null
+++ b/hugolib/rendershortcodes_test.go
@@ -0,0 +1,232 @@
+// Copyright 2023 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.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugolib
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestRenderShortcodesBasic(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ["home", "taxonomy", "term"]
+-- content/p1.md --
+---
+title: "p1"
+---
+## p1-h1
+{{% include "p2" %}}
+-- content/p2.md --
+---
+title: "p2"
+---
+### p2-h1
+{{< withhtml >}}
+### p2-h2
+{{% withmarkdown %}}
+### p2-h3
+{{% include "p3" %}}
+-- content/p3.md --
+---
+title: "p3"
+---
+### p3-h1
+{{< withhtml >}}
+### p3-h2
+{{% withmarkdown %}}
+{{< level3 >}}
+-- layouts/shortcodes/include.html --
+{{ $p := site.GetPage (.Get 0) }}
+{{ $p.RenderShortcodes }}
+-- layouts/shortcodes/withhtml.html --
+<div>{{ .Page.Title }} withhtml</div>
+-- layouts/shortcodes/withmarkdown.html --
+#### {{ .Page.Title }} withmarkdown
+-- layouts/shortcodes/level3.html --
+Level 3: {{ .Page.Title }}
+-- layouts/_default/single.html --
+Fragments: {{ .Fragments.Identifiers }}|
+HasShortcode Level 1: {{ .HasShortcode "include" }}|
+HasShortcode Level 2: {{ .HasShortcode "withmarkdown" }}|
+HasShortcode Level 3: {{ .HasShortcode "level3" }}|
+HasSHortcode not found: {{ .HasShortcode "notfound" }}|
+Content: {{ .Content }}|
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html",
+		"Fragments: [p1-h1 p2-h1 p2-h2 p2-h3 p2-withmarkdown p3-h1 p3-h2 p3-withmarkdown]|",
+		"HasShortcode Level 1: true|",
+		"HasShortcode Level 2: true|",
+		"HasShortcode Level 3: true|",
+		"HasSHortcode not found: false|",
+	)
+
+	// TODO1 more assertions.
+
+}
+
+func TestRenderShortcodesNestedMultipleOutputFormatTemplates(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"]
+[outputs]
+page = ["html", "json"]
+-- content/p1.md --
+---
+title: "p1"
+---
+## p1-h1
+{{% include "p2" %}}
+-- content/p2.md --
+---
+title: "p2"
+---
+### p2-h1
+{{% myshort %}}
+-- layouts/shortcodes/include.html --
+{{ $p := site.GetPage (.Get 0) }}
+{{ $p.RenderShortcodes }}
+-- layouts/shortcodes/myshort.html --
+Myshort HTML.
+-- layouts/shortcodes/myshort.json --
+Myshort JSON.
+-- layouts/_default/single.html --
+HTML: {{ .Content }}
+-- layouts/_default/single.json --
+JSON: {{ .Content }}
+
+
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", "Myshort HTML")
+	b.AssertFileContent("public/p1/index.json", "Myshort JSON")
+
+}
+
+func TestRenderShortcodesEditNested(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableLiveReload = true
+disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"]
+-- content/p1.md --
+---
+title: "p1"
+---
+## p1-h1
+{{% include "p2" %}}
+-- content/p2.md --
+---
+title: "p2"
+---
+### p2-h1
+{{% myshort %}}
+-- layouts/shortcodes/include.html --
+{{ $p := site.GetPage (.Get 0) }}
+{{ $p.RenderShortcodes }}
+-- layouts/shortcodes/myshort.html --
+Myshort Original.
+-- layouts/_default/single.html --
+ {{ .Content }}
+
+
+
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			Running:     true,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", "Myshort Original.")
+
+	b.EditFileReplace("layouts/shortcodes/myshort.html", func(s string) string {
+		return "Myshort Edited."
+	})
+	b.Build()
+	b.AssertFileContent("public/p1/index.html", "Myshort Edited.")
+
+}
+
+func TestRenderShortcodesEditIncludedPage(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableLiveReload = true
+disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"]
+-- content/p1.md --
+---
+title: "p1"
+---
+## p1-h1
+{{% include "p2" %}}
+-- content/p2.md --
+---
+title: "p2"
+---
+### Original
+{{% myshort %}}
+-- layouts/shortcodes/include.html --
+{{ $p := site.GetPage (.Get 0) }}
+{{ $p.RenderShortcodes }}
+-- layouts/shortcodes/myshort.html --
+Myshort Original.
+-- layouts/_default/single.html --
+ {{ .Content }}
+
+
+
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			Running:     true,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", "Original")
+
+	b.EditFileReplace("content/p2.md", func(s string) string {
+		return strings.Replace(s, "Original", "Edited", 1)
+	})
+	b.Build()
+	b.AssertFileContent("public/p1/index.html", "Edited")
+
+}
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
index 774794c56f6..6ee68673f8a 100644
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -948,7 +948,6 @@ title: "p1"
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
 		},
 	).Build()
 
@@ -991,7 +990,6 @@ title: "p1"
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
 		},
 	).Build()
 
@@ -1023,7 +1021,6 @@ echo "foo";
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
 		},
 	).Build()
 
@@ -1061,7 +1058,6 @@ title: "p1"
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
 		},
 	).Build()
 
@@ -1098,8 +1094,8 @@ Title: {{ .Get "title" | safeHTML }}
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
-			Verbose:     true,
+
+			Verbose: true,
 		},
 	).Build()
 
@@ -1191,8 +1187,8 @@ C'est un test
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
-			Verbose:     true,
+
+			Verbose: true,
 		},
 	).Build()
 
@@ -1229,8 +1225,8 @@ InnerDeindent: {{ .Get 0 }}: {{ len .InnerDeindent }}
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
-			Verbose:     true,
+
+			Verbose: true,
 		},
 	).Build()
 
@@ -1269,8 +1265,8 @@ Inner: {{ .Get 0 }}: {{ len .Inner }}
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
-			Verbose:     true,
+
+			Verbose: true,
 		},
 	).BuildE()
 
@@ -1306,8 +1302,8 @@ Hello.
 		IntegrationTestConfig{
 			T:           t,
 			TxtarString: files,
-			Running:     true,
-			Verbose:     true,
+
+			Verbose: true,
 		},
 	).Build()
 
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
index 50f297caba6..ca2c4ff53f5 100644
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -118,15 +118,15 @@ func (p *testPage) Err() resource.ResourceError {
 }
 
 func (p *testPage) Aliases() []string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) AllTranslations() Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) AlternativeOutputFormats() OutputFormats {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Author() Author {
@@ -138,19 +138,19 @@ func (p *testPage) Authors() AuthorList {
 }
 
 func (p *testPage) BaseFileName() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) BundleType() files.ContentClass {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Content(context.Context) (any, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) ContentBaseName() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) CurrentSection() Page {
@@ -178,11 +178,11 @@ func (p *testPage) Description() string {
 }
 
 func (p *testPage) Dir() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Draft() bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Eq(other any) bool {
@@ -194,11 +194,11 @@ func (p *testPage) ExpiryDate() time.Time {
 }
 
 func (p *testPage) Ext() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Extension() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) File() source.File {
@@ -206,15 +206,15 @@ func (p *testPage) File() source.File {
 }
 
 func (p *testPage) FileInfo() hugofs.FileMetaInfo {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Filename() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) FirstSection() Page {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) FuzzyWordCount(context.Context) int {
@@ -222,19 +222,19 @@ func (p *testPage) FuzzyWordCount(context.Context) int {
 }
 
 func (p *testPage) GetPage(ref string) (Page, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) GetPageWithTemplateInfo(info tpl.Info, ref string) (Page, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) GetParam(key string) any {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) GetTerms(taxonomy string) Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
@@ -250,27 +250,27 @@ func (p *testPage) CodeOwners() []string {
 }
 
 func (p *testPage) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) HasShortcode(name string) bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Hugo() hugo.HugoInfo {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) InSection(other any) (bool, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsAncestor(other any) (bool, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsDescendant(other any) (bool, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsDraft() bool {
@@ -278,27 +278,27 @@ func (p *testPage) IsDraft() bool {
 }
 
 func (p *testPage) IsHome() bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsNode() bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsPage() bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsSection() bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) IsTranslated() bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Keywords() []string {
@@ -314,7 +314,7 @@ func (p *testPage) Lang() string {
 }
 
 func (p *testPage) Language() *langs.Language {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) LanguagePrefix() string {
@@ -348,11 +348,11 @@ func (p *testPage) LinkTitle() string {
 }
 
 func (p *testPage) LogicalName() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) MediaType() media.Type {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Menus() navigation.PageMenus {
@@ -360,11 +360,11 @@ func (p *testPage) Menus() navigation.PageMenus {
 }
 
 func (p *testPage) Name() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Next() Page {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) NextInSection() Page {
@@ -376,19 +376,19 @@ func (p *testPage) NextPage() Page {
 }
 
 func (p *testPage) OutputFormats() OutputFormats {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Pages() Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) RegularPages() Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) RegularPagesRecursive() Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Paginate(seq any, options ...any) (*Pager, error) {
@@ -412,11 +412,11 @@ func (p *testPage) Page() Page {
 }
 
 func (p *testPage) Parent() Page {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Ancestors() Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Path() string {
@@ -428,19 +428,19 @@ func (p *testPage) Pathc() string {
 }
 
 func (p *testPage) Permalink() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Plain(context.Context) string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) PlainWords(context.Context) []string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Prev() Page {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) PrevInSection() Page {
@@ -460,15 +460,19 @@ func (p *testPage) RSSLink() template.URL {
 }
 
 func (p *testPage) RawContent() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
+}
+
+func (p *testPage) RenderShortcodes(context.Context) (template.HTML, error) {
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) ReadingTime(context.Context) int {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Ref(argsm map[string]any) (string, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) RefFrom(argsm map[string]any, source any) (string, error) {
@@ -476,11 +480,11 @@ func (p *testPage) RefFrom(argsm map[string]any, source any) (string, error) {
 }
 
 func (p *testPage) RelPermalink() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) RelRef(argsm map[string]any) (string, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) RelRefFrom(argsm map[string]any, source any) (string, error) {
@@ -488,27 +492,27 @@ func (p *testPage) RelRefFrom(argsm map[string]any, source any) (string, error)
 }
 
 func (p *testPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) ResourceType() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Resources() resource.Resources {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Scratch() *maps.Scratch {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Store() *maps.Scratch {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
@@ -525,7 +529,7 @@ func (p *testPage) Section() string {
 }
 
 func (p *testPage) Sections() Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) SectionsEntries() []string {
@@ -541,7 +545,7 @@ func (p *testPage) Site() Site {
 }
 
 func (p *testPage) Sites() Sites {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Slug() string {
@@ -553,11 +557,11 @@ func (p *testPage) String() string {
 }
 
 func (p *testPage) Summary(context.Context) template.HTML {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) TableOfContents(context.Context) template.HTML {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Title() string {
@@ -565,7 +569,7 @@ func (p *testPage) Title() string {
 }
 
 func (p *testPage) TranslationBaseName() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) TranslationKey() string {
@@ -573,11 +577,11 @@ func (p *testPage) TranslationKey() string {
 }
 
 func (p *testPage) Translations() Pages {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Truncated(context.Context) bool {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Type() string {
@@ -589,7 +593,7 @@ func (p *testPage) URL() string {
 }
 
 func (p *testPage) UniqueID() string {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) Weight() int {
@@ -597,11 +601,11 @@ func (p *testPage) Weight() int {
 }
 
 func (p *testPage) WordCount(context.Context) int {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func (p *testPage) GetIdentity() identity.Identity {
-	panic("tespage: not implemented")
+	panic("testpage: not implemented")
 }
 
 func createTestPages(num int) Pages {