Repository: gohugoio/hugo (language: go)

Issue:
Add page template function
We have several issues discussing something like this (re partials and `.Render`), but I haven't found a way (or at least not a reasonably simple way) to implement it ...

... until now.

This function will return the _top level data context_  (the `Page`) when called from anywhere (shortcodes, partials, render hooks, view templates).

So you would do:

`partials/mypartial`:

```
{{ render.Context.Title }}
```

Will print the entry Page's title.

My explanation above may not be perfect, but this is plenty useful -- the hard part is getting the name right.

/cc @regisphilibert @jmooring etc.


Discussion / hints:
Since this "context" is always a `Page`, maybe we could just call it ... `page`, which would be analogue to the `server` global.

So, in the top level `single.html` etc. template, these will be the same:

```
{{ .Title }}
{{ page.Title }}
```
The issue I remember with `.Render` is <https://github.com/gohugoio/hugo/issues/8182#issuecomment-801194305>, where it would be convenient to do something like:

```
{{ .Render "summary" (dict "page" .Page "myVar" "foo") }}
```

It doesn't seem like the proposed `page` function would address this particular case  (`myVar` isn't on `.Page`). Please comment.

Also, what is the difference between `page.Title` and `$.Title`? It seems like both return the top level data context.


OK, I  may have forgot what the ".Render problem" was ... but I suspect I'm talking about "another one".

>Also, what is the difference between page.Title and $.Title? It seems like both return the top level data context.

So,

```
{{ $.Title }}
{{ page.Title }}
```

The above would only be the same in the _entry_ template (e.g. `single.html`). The `$` does not travel into `partials` and `template` constructs.


Does that mean I would be able to call a partial without passing the page context? 

layouts/_default/single.html

```
{{ partial "render-some-page-stuff" }}
```

layouts/partials/render-some-page-stuff.html

```
Title: {{ page.Title }}<br>
Date: {{ page.Date }}<br>
```
We have `site` which is a invoking the page's `.Site` method throughout. 

So it would make sense that this is called `page`.

I think this is great, the more simple the context passed to a partial is — and now we'll never need to pass the page — the better. 

I don't foresee any problems. Except making sure users understand the fact that any template be it a partial a ".Render" or a returning partial are bound to a page and even though it has no consequence it might create confusion when using `partialCached` where your partial might end up with the same `page`. But that's just logical and only concerns users using the new feature. 
>Does that mean I would be able to call a partial without passing the page context?

Yes. But it's important to note that this is not only partials, and it would work in any nested situations, which is where I find the most pains in the current situation -- partial calling partial calling partial calling partial.

>partialCached where your partial might end up with the same page.

That is true, but that is also a "problem" you would have today.


Fix patch:
diff --git a/commands/server.go b/commands/server.go
index 332e35c01b9..4f7d4db8f76 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -577,7 +577,7 @@ func (c *commandeer) serve(s *serverCmd) error {
 			// to cached values if nil.
 			templ, handler := getErrorTemplateAndHandler(c.hugoTry())
 			b := &bytes.Buffer{}
-			err := handler.Execute(templ, b, ctx)
+			err := handler.ExecuteWithContext(context.Background(), templ, b, ctx)
 			return b, err
 		},
 	}
diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go
index 1b7e5acf722..17afbf9127e 100644
--- a/common/hreflect/helpers.go
+++ b/common/hreflect/helpers.go
@@ -208,6 +208,23 @@ func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) {
 	return time.Time{}, false
 }
 
+func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
+	fn := v.MethodByName(name)
+	var args []reflect.Value
+	tp := fn.Type()
+	if tp.NumIn() > 0 {
+		if tp.NumIn() > 1 {
+			panic("not supported")
+		}
+		first := tp.In(0)
+		if first.Implements(ContextInterface) {
+			args = append(args, reflect.ValueOf(cxt))
+		}
+	}
+
+	return fn.Call(args)
+}
+
 // Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
 func indirectInterface(v reflect.Value) reflect.Value {
 	if v.Kind() != reflect.Interface {
diff --git a/hugolib/alias.go b/hugolib/alias.go
index 2609cd6bb49..071f73d412a 100644
--- a/hugolib/alias.go
+++ b/hugolib/alias.go
@@ -15,6 +15,7 @@ package hugolib
 
 import (
 	"bytes"
+	"context"
 	"errors"
 	"fmt"
 	"io"
@@ -64,8 +65,10 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err
 		p,
 	}
 
+	ctx := tpl.SetPageInContext(context.Background(), p)
+
 	buffer := new(bytes.Buffer)
-	err := a.t.Execute(templ, buffer, data)
+	err := a.t.ExecuteWithContext(ctx, templ, buffer, data)
 	if err != nil {
 		return nil, err
 	}
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
index cdc5d97fb78..65cc23971db 100644
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -767,10 +767,11 @@ func (h *HugoSites) renderCrossSitesSitemap() error {
 	}
 
 	s := h.Sites[0]
+	// We don't have any page context to pass in here.
+	ctx := context.Background()
 
 	templ := s.lookupLayouts("sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml")
-
-	return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex",
+	return s.renderAndWriteXML(ctx, &s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex",
 		s.siteCfg.sitemap.Filename, h.toSiteInfos(), templ)
 }
 
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index ce3498e0ef4..be65ad9e723 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -740,6 +740,7 @@ func (cp *pageContentOutput) ParseContent(ctx context.Context, content []byte) (
 		return nil, ok, nil
 	}
 	rctx := converter.RenderContext{
+		Ctx:         ctx,
 		Src:         content,
 		RenderTOC:   true,
 		GetRenderer: cp.renderHooks.getRenderer,
@@ -758,6 +759,7 @@ func (cp *pageContentOutput) RenderContent(ctx context.Context, content []byte,
 		return nil, ok, nil
 	}
 	rctx := converter.RenderContext{
+		Ctx:         ctx,
 		Src:         content,
 		RenderTOC:   true,
 		GetRenderer: cp.renderHooks.getRenderer,
@@ -777,6 +779,7 @@ func (cp *pageContentOutput) RenderContent(ctx context.Context, content []byte,
 func (cp *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) {
 	r, err := c.Convert(
 		converter.RenderContext{
+			Ctx:         ctx,
 			Src:         content,
 			RenderTOC:   renderTOC,
 			GetRenderer: cp.renderHooks.getRenderer,
diff --git a/hugolib/site.go b/hugolib/site.go
index e90fa41ff63..45b4643f035 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -1710,12 +1710,12 @@ func (s *Site) lookupLayouts(layouts ...string) tpl.Template {
 	return nil
 }
 
-func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath string, d any, templ tpl.Template) error {
+func (s *Site) renderAndWriteXML(ctx context.Context, statCounter *uint64, name string, targetPath string, d any, templ tpl.Template) error {
 	s.Log.Debugf("Render XML for %q to %q", name, targetPath)
 	renderBuffer := bp.GetBuffer()
 	defer bp.PutBuffer(renderBuffer)
 
-	if err := s.renderForTemplate(name, "", d, renderBuffer, templ); err != nil {
+	if err := s.renderForTemplate(ctx, name, "", d, renderBuffer, templ); err != nil {
 		return err
 	}
 
@@ -1739,8 +1739,9 @@ func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath s
 	defer bp.PutBuffer(renderBuffer)
 
 	of := p.outputFormat()
+	ctx := tpl.SetPageInContext(context.Background(), p)
 
-	if err := s.renderForTemplate(p.Kind(), of.Name, p, renderBuffer, templ); err != nil {
+	if err := s.renderForTemplate(ctx, p.Kind(), of.Name, p, renderBuffer, templ); err != nil {
 		return err
 	}
 
@@ -1797,16 +1798,16 @@ type hookRendererTemplate struct {
 	resolvePosition func(ctx any) text.Position
 }
 
-func (hr hookRendererTemplate) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
-	return hr.templateHandler.Execute(hr.templ, w, ctx)
+func (hr hookRendererTemplate) RenderLink(cctx context.Context, w io.Writer, ctx hooks.LinkContext) error {
+	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
 }
 
-func (hr hookRendererTemplate) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
-	return hr.templateHandler.Execute(hr.templ, w, ctx)
+func (hr hookRendererTemplate) RenderHeading(cctx context.Context, w io.Writer, ctx hooks.HeadingContext) error {
+	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
 }
 
-func (hr hookRendererTemplate) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
-	return hr.templateHandler.Execute(hr.templ, w, ctx)
+func (hr hookRendererTemplate) RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
+	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
 }
 
 func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
@@ -1817,13 +1818,15 @@ func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
 	return false
 }
 
-func (s *Site) renderForTemplate(name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
+func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
 	if templ == nil {
 		s.logMissingLayout(name, "", "", outputFormat)
 		return nil
 	}
 
-	ctx := context.Background()
+	if ctx == nil {
+		panic("nil context")
+	}
 
 	if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
 		return fmt.Errorf("render of %q failed: %w", name, err)
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index 51d638ddef0..f105a1ae4c1 100644
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -14,6 +14,7 @@
 package hugolib
 
 import (
+	"context"
 	"fmt"
 	"path"
 	"strings"
@@ -197,7 +198,7 @@ func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
 		d.Addends = fmt.Sprintf("/%s/%d", paginatePath, 1)
 		targetPaths := page.CreateTargetPaths(d)
 
-		if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, nil); err != nil {
+		if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, p); err != nil {
 			return err
 		}
 	}
@@ -278,6 +279,7 @@ func (s *Site) renderSitemap() error {
 	}
 
 	targetPath := p.targetPaths().TargetFilename
+	ctx := tpl.SetPageInContext(context.Background(), p)
 
 	if targetPath == "" {
 		return errors.New("failed to create targetPath for sitemap")
@@ -285,7 +287,7 @@ func (s *Site) renderSitemap() error {
 
 	templ := s.lookupLayouts("sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml")
 
-	return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemap", targetPath, p, templ)
+	return s.renderAndWriteXML(ctx, &s.PathSpec.ProcessingStats.Sitemaps, "sitemap", targetPath, p, templ)
 }
 
 func (s *Site) renderRobotsTXT() error {
diff --git a/markup/converter/converter.go b/markup/converter/converter.go
index e5a07f1a134..544d4841a89 100644
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -15,6 +15,7 @@ package converter
 
 import (
 	"bytes"
+	"context"
 
 	"github.com/gohugoio/hugo/common/hexec"
 	"github.com/gohugoio/hugo/common/loggers"
@@ -141,6 +142,9 @@ type DocumentContext struct {
 
 // RenderContext holds contextual information about the content to render.
 type RenderContext struct {
+	// Ctx is the context.Context for the current Page render.
+	Ctx context.Context
+
 	// Src is the content to render.
 	Src []byte
 
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index 7eede071033..55d7c1127fc 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -14,6 +14,7 @@
 package hooks
 
 import (
+	"context"
 	"io"
 
 	"github.com/gohugoio/hugo/common/hugio"
@@ -85,12 +86,12 @@ type AttributesOptionsSliceProvider interface {
 }
 
 type LinkRenderer interface {
-	RenderLink(w io.Writer, ctx LinkContext) error
+	RenderLink(cctx context.Context, w io.Writer, ctx LinkContext) error
 	identity.Provider
 }
 
 type CodeBlockRenderer interface {
-	RenderCodeblock(w hugio.FlexiWriter, ctx CodeblockContext) error
+	RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx CodeblockContext) error
 	identity.Provider
 }
 
@@ -119,7 +120,7 @@ type HeadingContext interface {
 // HeadingRenderer describes a uniquely identifiable rendering hook.
 type HeadingRenderer interface {
 	// Render writes the rendered content to w using the data in w.
-	RenderHeading(w io.Writer, ctx HeadingContext) error
+	RenderHeading(cctx context.Context, w io.Writer, ctx HeadingContext) error
 	identity.Provider
 }
 
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
index 739781de1c9..cf5a0f2966c 100644
--- a/markup/goldmark/codeblocks/render.go
+++ b/markup/goldmark/codeblocks/render.go
@@ -124,6 +124,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
 	cr := renderer.(hooks.CodeBlockRenderer)
 
 	err := cr.RenderCodeblock(
+		ctx.RenderContext().Ctx,
 		w,
 		cbctx,
 	)
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index f36f9f4e690..0bd800dc0d9 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -181,6 +181,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
 	attrs := r.filterInternalAttributes(n.Attributes())
 
 	err := lr.RenderLink(
+		ctx.RenderContext().Ctx,
 		w,
 		imageLinkContext{
 			linkContext: linkContext{
@@ -271,6 +272,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
 	ctx.Buffer.Truncate(pos)
 
 	err := lr.RenderLink(
+		ctx.RenderContext().Ctx,
 		w,
 		linkContext{
 			page:             ctx.DocumentContext().Document,
@@ -340,6 +342,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
 	}
 
 	err := lr.RenderLink(
+		ctx.RenderContext().Ctx,
 		w,
 		linkContext{
 			page:             ctx.DocumentContext().Document,
@@ -428,6 +431,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
 	anchor := anchori.([]byte)
 
 	err := hr.RenderHeading(
+		ctx.RenderContext().Ctx,
 		w,
 		headingContext{
 			page:             ctx.DocumentContext().Document,
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
index b7499770055..410beb74068 100644
--- a/markup/highlight/highlight.go
+++ b/markup/highlight/highlight.go
@@ -14,6 +14,7 @@
 package highlight
 
 import (
+	"context"
 	"fmt"
 	gohtml "html"
 	"html/template"
@@ -122,7 +123,7 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts a
 	}, nil
 }
 
-func (h chromaHighlighter) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
+func (h chromaHighlighter) RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
 	cfg := h.cfg
 
 	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
diff --git a/resources/errorResource.go b/resources/errorResource.go
index 42edb0bd0a7..c8c32dfc313 100644
--- a/resources/errorResource.go
+++ b/resources/errorResource.go
@@ -135,3 +135,7 @@ func (e *errorResource) DecodeImage() (image.Image, error) {
 func (e *errorResource) Transform(...ResourceTransformation) (ResourceTransformer, error) {
 	panic(e.ResourceError)
 }
+
+func (e *errorResource) TransformWithContext(context.Context, ...ResourceTransformation) (ResourceTransformer, error) {
+	panic(e.ResourceError)
+}
diff --git a/resources/page/pagegroup.go b/resources/page/pagegroup.go
index bac5d8327ea..99f1af3ff7e 100644
--- a/resources/page/pagegroup.go
+++ b/resources/page/pagegroup.go
@@ -159,12 +159,7 @@ func (p Pages) GroupBy(ctx context.Context, key string, order ...string) (PagesG
 		case reflect.StructField:
 			fv = ppv.Elem().FieldByName(key)
 		case reflect.Method:
-			var args []reflect.Value
-			fn := hreflect.GetMethodByName(ppv, key)
-			if fn.Type().NumIn() > 0 && fn.Type().In(0).Implements(hreflect.ContextInterface) {
-				args = []reflect.Value{reflect.ValueOf(ctx)}
-			}
-			fv = fn.Call(args)[0]
+			fv = hreflect.CallMethodByName(ctx, key, ppv)[0]
 		}
 		if !fv.IsValid() {
 			continue
diff --git a/resources/resource.go b/resources/resource.go
index 8a524247ae3..7ccc5da39de 100644
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -104,6 +104,7 @@ type ResourceTransformer interface {
 
 type Transformer interface {
 	Transform(...ResourceTransformation) (ResourceTransformer, error)
+	TransformWithContext(context.Context, ...ResourceTransformation) (ResourceTransformer, error)
 }
 
 func NewFeatureNotAvailableTransformer(key string, elements ...any) ResourceTransformation {
diff --git a/resources/resource_transformers/templates/execute_as_template.go b/resources/resource_transformers/templates/execute_as_template.go
index 5fe4230f133..4d4aba39607 100644
--- a/resources/resource_transformers/templates/execute_as_template.go
+++ b/resources/resource_transformers/templates/execute_as_template.go
@@ -15,6 +15,7 @@
 package templates
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/gohugoio/hugo/helpers"
@@ -61,11 +62,11 @@ func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransforma
 
 	ctx.OutPath = t.targetPath
 
-	return t.t.Tmpl().Execute(templ, ctx.To, t.data)
+	return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
 }
 
-func (c *Client) ExecuteAsTemplate(res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {
-	return res.Transform(&executeAsTemplateTransform{
+func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {
+	return res.TransformWithContext(ctx, &executeAsTemplateTransform{
 		rs:         c.rs,
 		targetPath: helpers.ToSlashTrimLeading(targetPath),
 		t:          c.t,
diff --git a/resources/transform.go b/resources/transform.go
index 3477c710f34..4ab51485e1b 100644
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -69,6 +69,7 @@ func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResour
 	return &resourceAdapter{
 		resourceTransformations: &resourceTransformations{},
 		resourceAdapterInner: &resourceAdapterInner{
+			ctx:         context.TODO(),
 			spec:        spec,
 			publishOnce: po,
 			target:      target,
@@ -84,6 +85,9 @@ type ResourceTransformation interface {
 }
 
 type ResourceTransformationCtx struct {
+	// The context that started the transformation.
+	Ctx context.Context
+
 	// The content to transform.
 	From io.Reader
 
@@ -180,6 +184,7 @@ func (r *resourceAdapter) Data() any {
 func (r resourceAdapter) cloneTo(targetPath string) resource.Resource {
 	newtTarget := r.target.cloneTo(targetPath)
 	newInner := &resourceAdapterInner{
+		ctx:    r.ctx,
 		spec:   r.spec,
 		target: newtTarget.(transformableResource),
 	}
@@ -278,11 +283,17 @@ func (r *resourceAdapter) Title() string {
 }
 
 func (r resourceAdapter) Transform(t ...ResourceTransformation) (ResourceTransformer, error) {
+	return r.TransformWithContext(context.Background(), t...)
+}
+
+func (r resourceAdapter) TransformWithContext(ctx context.Context, t ...ResourceTransformation) (ResourceTransformer, error) {
+
 	r.resourceTransformations = &resourceTransformations{
 		transformations: append(r.transformations, t...),
 	}
 
 	r.resourceAdapterInner = &resourceAdapterInner{
+		ctx:         ctx,
 		spec:        r.spec,
 		publishOnce: &publishOnce{},
 		target:      r.target,
@@ -377,6 +388,7 @@ func (r *resourceAdapter) transform(publish, setContent bool) error {
 	defer bp.PutBuffer(b2)
 
 	tctx := &ResourceTransformationCtx{
+		Ctx:                   r.ctx,
 		Data:                  make(map[string]any),
 		OpenResourcePublisher: r.target.openPublishFileForWriting,
 	}
@@ -599,6 +611,9 @@ func (r *resourceAdapter) initTransform(publish, setContent bool) {
 }
 
 type resourceAdapterInner struct {
+	// The context that started this transformation.
+	ctx context.Context
+
 	target transformableResource
 
 	spec *Spec
diff --git a/tpl/cast/init.go b/tpl/cast/init.go
index f1badf993c6..84211a00b0a 100644
--- a/tpl/cast/init.go
+++ b/tpl/cast/init.go
@@ -14,6 +14,8 @@
 package cast
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.ToInt,
diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go
index 74ecc5b196f..1dc09c8e51a 100644
--- a/tpl/collections/apply.go
+++ b/tpl/collections/apply.go
@@ -40,7 +40,7 @@ func (ns *Namespace) Apply(ctx context.Context, c any, fname string, args ...any
 		return nil, errors.New("can't iterate over a nil value")
 	}
 
-	fnv, found := ns.lookupFunc(fname)
+	fnv, found := ns.lookupFunc(ctx, fname)
 	if !found {
 		return nil, errors.New("can't find function " + fname)
 	}
@@ -106,7 +106,7 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
 	return reflect.ValueOf(nil), res[1].Interface().(error)
 }
 
-func (ns *Namespace) lookupFunc(fname string) (reflect.Value, bool) {
+func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
 	namespace, methodName, ok := strings.Cut(fname, ".")
 	if !ok {
 		templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter)
@@ -114,16 +114,16 @@ func (ns *Namespace) lookupFunc(fname string) (reflect.Value, bool) {
 	}
 
 	// Namespace
-	nv, found := ns.lookupFunc(namespace)
+	nv, found := ns.lookupFunc(ctx, namespace)
 	if !found {
 		return reflect.Value{}, false
 	}
 
-	fn, ok := nv.Interface().(func(...any) (any, error))
+	fn, ok := nv.Interface().(func(context.Context, ...any) (any, error))
 	if !ok {
 		return reflect.Value{}, false
 	}
-	v, err := fn()
+	v, err := fn(ctx)
 	if err != nil {
 		panic(err)
 	}
diff --git a/tpl/collections/init.go b/tpl/collections/init.go
index c992d3fb2a1..8801422ac5d 100644
--- a/tpl/collections/init.go
+++ b/tpl/collections/init.go
@@ -14,6 +14,8 @@
 package collections
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.After,
diff --git a/tpl/compare/init.go b/tpl/compare/init.go
index 98c07f41b8d..f080647b18f 100644
--- a/tpl/compare/init.go
+++ b/tpl/compare/init.go
@@ -14,6 +14,8 @@
 package compare
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/tpl/internal"
@@ -31,7 +33,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Default,
diff --git a/tpl/crypto/init.go b/tpl/crypto/init.go
index dddc5585a9e..418fbd9fb47 100644
--- a/tpl/crypto/init.go
+++ b/tpl/crypto/init.go
@@ -14,6 +14,8 @@
 package crypto
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.MD5,
diff --git a/tpl/css/css.go b/tpl/css/css.go
index e1783334ee8..5fc61301192 100644
--- a/tpl/css/css.go
+++ b/tpl/css/css.go
@@ -1,6 +1,8 @@
 package css
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/common/types/css"
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
@@ -31,7 +33,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		return ns
diff --git a/tpl/data/init.go b/tpl/data/init.go
index 22e685fc848..507e0d43e9c 100644
--- a/tpl/data/init.go
+++ b/tpl/data/init.go
@@ -14,6 +14,8 @@
 package data
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.GetCSV,
diff --git a/tpl/debug/init.go b/tpl/debug/init.go
index 12a99783d2c..796a34bfcfc 100644
--- a/tpl/debug/init.go
+++ b/tpl/debug/init.go
@@ -14,6 +14,8 @@
 package debug
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Dump,
diff --git a/tpl/diagrams/init.go b/tpl/diagrams/init.go
index 1ed308c5730..e6356ce9c45 100644
--- a/tpl/diagrams/init.go
+++ b/tpl/diagrams/init.go
@@ -15,6 +15,8 @@
 package diagrams
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -29,7 +31,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		return ns
diff --git a/tpl/encoding/init.go b/tpl/encoding/init.go
index 1d42b4e3771..1c3322d6ed5 100644
--- a/tpl/encoding/init.go
+++ b/tpl/encoding/init.go
@@ -14,6 +14,8 @@
 package encoding
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Base64Decode,
diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go
index b0683f06174..8000627e211 100644
--- a/tpl/fmt/init.go
+++ b/tpl/fmt/init.go
@@ -14,6 +14,8 @@
 package fmt
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Print,
diff --git a/tpl/hugo/init.go b/tpl/hugo/init.go
index e2b4ae7af5a..ad589722c36 100644
--- a/tpl/hugo/init.go
+++ b/tpl/hugo/init.go
@@ -15,6 +15,8 @@
 package hugo
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -27,7 +29,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return h, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return h, nil },
 		}
 
 		// We just add the Hugo struct as the namespace here. No method mappings.
diff --git a/tpl/images/init.go b/tpl/images/init.go
index d9b9af4e7db..a350d5b9d2a 100644
--- a/tpl/images/init.go
+++ b/tpl/images/init.go
@@ -14,6 +14,8 @@
 package images
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Config,
diff --git a/tpl/inflect/init.go b/tpl/inflect/init.go
index a2d28f6bfd7..736e9fbd66e 100644
--- a/tpl/inflect/init.go
+++ b/tpl/inflect/init.go
@@ -14,6 +14,8 @@
 package inflect
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Humanize,
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
index 96f526005ed..23bc18e42a0 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -60,29 +60,29 @@ func NewExecuter(helper ExecHelper) Executer {
 }
 
 type (
-	dataContextKeyType    string
+	pageContextKeyType    string
 	hasLockContextKeyType string
 	stackContextKeyType   string
 )
 
 const (
-	// The data object passed to Execute or ExecuteWithContext gets stored with this key if not already set.
-	DataContextKey = dataContextKeyType("data")
+	// The data page passed to ExecuteWithContext gets stored with this key.
+	PageContextKey = pageContextKeyType("page")
 	// Used in partialCached to signal to nested templates that a lock is already taken.
 	HasLockContextKey = hasLockContextKeyType("hasLock")
 )
 
 // Note: The context is currently not fully implemeted in Hugo. This is a work in progress.
 func (t *executer) ExecuteWithContext(ctx context.Context, p Preparer, wr io.Writer, data any) error {
+	if ctx == nil {
+		panic("nil context")
+	}
+
 	tmpl, err := p.Prepare()
 	if err != nil {
 		return err
 	}
 
-	if v := ctx.Value(DataContextKey); v == nil {
-		ctx = context.WithValue(ctx, DataContextKey, data)
-	}
-
 	value, ok := data.(reflect.Value)
 	if !ok {
 		value = reflect.ValueOf(data)
@@ -102,28 +102,6 @@ func (t *executer) ExecuteWithContext(ctx context.Context, p Preparer, wr io.Wri
 	return tmpl.executeWithState(state, value)
 }
 
-func (t *executer) Execute(p Preparer, wr io.Writer, data any) error {
-	tmpl, err := p.Prepare()
-	if err != nil {
-		return err
-	}
-
-	value, ok := data.(reflect.Value)
-	if !ok {
-		value = reflect.ValueOf(data)
-	}
-
-	state := &state{
-		helper: t.helper,
-		prep:   p,
-		tmpl:   tmpl,
-		wr:     wr,
-		vars:   []variable{{"$", value}},
-	}
-
-	return tmpl.executeWithState(state, value)
-}
-
 // Prepare returns a template ready for execution.
 func (t *Template) Prepare() (*Template, error) {
 	return t, nil
diff --git a/tpl/internal/templatefuncsRegistry.go b/tpl/internal/templatefuncsRegistry.go
index 84e0e25faf4..363f6d82f33 100644
--- a/tpl/internal/templatefuncsRegistry.go
+++ b/tpl/internal/templatefuncsRegistry.go
@@ -17,6 +17,7 @@ package internal
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"go/doc"
@@ -49,7 +50,7 @@ type TemplateFuncsNamespace struct {
 	Name string
 
 	// This is the method receiver.
-	Context func(v ...any) (any, error)
+	Context func(ctx context.Context, v ...any) (any, error)
 
 	// Additional info, aliases and examples, per method name.
 	MethodMappings map[string]TemplateFuncMethodMapping
@@ -172,7 +173,7 @@ func (namespaces TemplateFuncsNamespaces) MarshalJSON() ([]byte, error) {
 		if i != 0 {
 			buf.WriteString(",")
 		}
-		b, err := ns.toJSON()
+		b, err := ns.toJSON(context.TODO())
 		if err != nil {
 			return nil, err
 		}
@@ -188,7 +189,7 @@ var ignoreFuncs = map[string]bool{
 	"Reset": true,
 }
 
-func (t *TemplateFuncsNamespace) toJSON() ([]byte, error) {
+func (t *TemplateFuncsNamespace) toJSON(ctx context.Context) ([]byte, error) {
 	var buf bytes.Buffer
 
 	godoc := getGetTplPackagesGoDoc()[t.Name]
@@ -197,11 +198,11 @@ func (t *TemplateFuncsNamespace) toJSON() ([]byte, error) {
 
 	buf.WriteString(fmt.Sprintf(`%q: {`, t.Name))
 
-	ctx, err := t.Context()
+	tctx, err := t.Context(ctx)
 	if err != nil {
 		return nil, err
 	}
-	ctxType := reflect.TypeOf(ctx)
+	ctxType := reflect.TypeOf(tctx)
 	for i := 0; i < ctxType.NumMethod(); i++ {
 		method := ctxType.Method(i)
 		if ignoreFuncs[method.Name] {
diff --git a/tpl/js/init.go b/tpl/js/init.go
index d57e0fdcbcf..16e6c7efad1 100644
--- a/tpl/js/init.go
+++ b/tpl/js/init.go
@@ -14,6 +14,8 @@
 package js
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		return ns
diff --git a/tpl/lang/init.go b/tpl/lang/init.go
index 3f7b57ffca7..62c3a56a049 100644
--- a/tpl/lang/init.go
+++ b/tpl/lang/init.go
@@ -14,6 +14,8 @@
 package lang
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/tpl/internal"
@@ -27,7 +29,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Translate,
diff --git a/tpl/math/init.go b/tpl/math/init.go
index 19905fd3aca..67aa95f4164 100644
--- a/tpl/math/init.go
+++ b/tpl/math/init.go
@@ -14,6 +14,8 @@
 package math
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Add,
diff --git a/tpl/openapi/openapi3/init.go b/tpl/openapi/openapi3/init.go
index 8597e3294e2..4d88e148e03 100644
--- a/tpl/openapi/openapi3/init.go
+++ b/tpl/openapi/openapi3/init.go
@@ -14,6 +14,8 @@
 package openapi3
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Unmarshal,
diff --git a/tpl/os/init.go b/tpl/os/init.go
index cd9e370cd08..d7afba16fbc 100644
--- a/tpl/os/init.go
+++ b/tpl/os/init.go
@@ -14,6 +14,8 @@
 package os
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Getenv,
diff --git a/tpl/page/init.go b/tpl/page/init.go
new file mode 100644
index 00000000000..52aeaafd69e
--- /dev/null
+++ b/tpl/page/init.go
@@ -0,0 +1,49 @@
+// Copyright 2022 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 page provides template functions for accessing the current Page object,
+// the entry level context for the current template.
+package page
+
+import (
+	"context"
+
+	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/resources/page"
+	"github.com/gohugoio/hugo/tpl"
+
+	"github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "page"
+
+func init() {
+	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+		ns := &internal.TemplateFuncsNamespace{
+			Name: name,
+			Context: func(ctx context.Context, args ...interface{}) (interface{}, error) {
+				v := tpl.GetPageFromContext(ctx)
+				if v == nil {
+					// The multilingual sitemap does not have a page as its context.
+					return nil, nil
+				}
+
+				return v.(page.Page), nil
+			},
+		}
+
+		return ns
+	}
+
+	internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/partials/init.go b/tpl/partials/init.go
index 2662b8894d1..e9d901bbf63 100644
--- a/tpl/partials/init.go
+++ b/tpl/partials/init.go
@@ -14,6 +14,8 @@
 package partials
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    namespaceName,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Include,
diff --git a/tpl/path/init.go b/tpl/path/init.go
index 0d81f61819b..97a479d3f01 100644
--- a/tpl/path/init.go
+++ b/tpl/path/init.go
@@ -14,6 +14,7 @@
 package path
 
 import (
+	"context"
 	"fmt"
 	"path/filepath"
 
@@ -29,7 +30,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Split,
diff --git a/tpl/reflect/init.go b/tpl/reflect/init.go
index 3af6dfa119e..259f97323f2 100644
--- a/tpl/reflect/init.go
+++ b/tpl/reflect/init.go
@@ -15,6 +15,8 @@
 package reflect
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -27,7 +29,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.IsMap,
diff --git a/tpl/resources/init.go b/tpl/resources/init.go
index 73a7b8f42ea..db51b0287e5 100644
--- a/tpl/resources/init.go
+++ b/tpl/resources/init.go
@@ -14,6 +14,8 @@
 package resources
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -30,7 +32,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Get,
diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go
index 85323f057f6..cc5310277f7 100644
--- a/tpl/resources/resources.go
+++ b/tpl/resources/resources.go
@@ -15,11 +15,10 @@
 package resources
 
 import (
+	"context"
 	"fmt"
 	"sync"
 
-	"github.com/gohugoio/hugo/common/herrors"
-
 	"errors"
 
 	"github.com/gohugoio/hugo/common/maps"
@@ -227,7 +226,6 @@ func (ns *Namespace) ByType(typ any) resource.Resources {
 //
 // See Match for a more complete explanation about the rules used.
 func (ns *Namespace) Match(pattern any) resource.Resources {
-	defer herrors.Recover()
 	patternStr, err := cast.ToStringE(pattern)
 	if err != nil {
 		panic(err)
@@ -283,7 +281,7 @@ func (ns *Namespace) FromString(targetPathIn, contentIn any) (resource.Resource,
 
 // ExecuteAsTemplate creates a Resource from a Go template, parsed and executed with
 // the given data, and published to the relative target path.
-func (ns *Namespace) ExecuteAsTemplate(args ...any) (resource.Resource, error) {
+func (ns *Namespace) ExecuteAsTemplate(ctx context.Context, args ...any) (resource.Resource, error) {
 	if len(args) != 3 {
 		return nil, fmt.Errorf("must provide targetPath, the template data context and a Resource object")
 	}
@@ -298,7 +296,7 @@ func (ns *Namespace) ExecuteAsTemplate(args ...any) (resource.Resource, error) {
 		return nil, fmt.Errorf("type %T not supported in Resource transformations", args[2])
 	}
 
-	return ns.templatesClient.ExecuteAsTemplate(r, targetPath, data)
+	return ns.templatesClient.ExecuteAsTemplate(ctx, r, targetPath, data)
 }
 
 // Fingerprint transforms the given Resource with a MD5 hash of the content in
diff --git a/tpl/safe/init.go b/tpl/safe/init.go
index 794c9d6f0f2..8fc0e82eaa5 100644
--- a/tpl/safe/init.go
+++ b/tpl/safe/init.go
@@ -14,6 +14,8 @@
 package safe
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.CSS,
diff --git a/tpl/site/init.go b/tpl/site/init.go
index 34ea7309f70..3d293f3fe2f 100644
--- a/tpl/site/init.go
+++ b/tpl/site/init.go
@@ -15,6 +15,8 @@
 package site
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 
 	"github.com/gohugoio/hugo/tpl/internal"
@@ -27,7 +29,7 @@ func init() {
 		s := d.Site
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return s, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return s, nil },
 		}
 
 		if s == nil {
diff --git a/tpl/strings/init.go b/tpl/strings/init.go
index 503ec6a25e2..37a48912852 100644
--- a/tpl/strings/init.go
+++ b/tpl/strings/init.go
@@ -14,6 +14,8 @@
 package strings
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Chomp,
diff --git a/tpl/template.go b/tpl/template.go
index dd9249adef5..f71de8bb2a0 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -59,7 +59,6 @@ type UnusedTemplatesProvider interface {
 // TemplateHandler finds and executes templates.
 type TemplateHandler interface {
 	TemplateFinder
-	Execute(t Template, wr io.Writer, data any) error
 	ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
 	LookupLayout(d output.LayoutDescriptor, f output.Format) (Template, bool, error)
 	HasTemplate(name string) bool
@@ -153,10 +152,18 @@ type TemplateFuncGetter interface {
 	GetFunc(name string) (reflect.Value, bool)
 }
 
-// GetDataFromContext returns the template data context (usually .Page) from ctx if set.
-// NOte: This is not fully implemented yet.
-func GetDataFromContext(ctx context.Context) any {
-	return ctx.Value(texttemplate.DataContextKey)
+// GetPageFromContext returns the top level Page.
+func GetPageFromContext(ctx context.Context) any {
+	return ctx.Value(texttemplate.PageContextKey)
+}
+
+// SetPageInContext sets the top level Page.
+func SetPageInContext(ctx context.Context, p page) context.Context {
+	return context.WithValue(ctx, texttemplate.PageContextKey, p)
+}
+
+type page interface {
+	IsNode() bool
 }
 
 func GetHasLockFromContext(ctx context.Context) bool {
diff --git a/tpl/templates/init.go b/tpl/templates/init.go
index e068fca81c2..ff6acdabd0b 100644
--- a/tpl/templates/init.go
+++ b/tpl/templates/init.go
@@ -14,6 +14,8 @@
 package templates
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Exists,
diff --git a/tpl/time/init.go b/tpl/time/init.go
index 4bb2ddf67bf..583dacd4acd 100644
--- a/tpl/time/init.go
+++ b/tpl/time/init.go
@@ -14,6 +14,7 @@
 package time
 
 import (
+	"context"
 	"errors"
 
 	"github.com/gohugoio/hugo/deps"
@@ -32,7 +33,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name: name,
-			Context: func(args ...any) (any, error) {
+			Context: func(cctx context.Context, args ...any) (any, error) {
 				// Handle overlapping "time" namespace and func.
 				//
 				// If no args are passed to `time`, assume namespace usage and
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index c01863ebb4b..70541e07682 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -232,6 +232,10 @@ func (t templateExec) Clone(d *deps.Deps) *templateExec {
 }
 
 func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error {
+	// TOD1
+	if true {
+		//panic("not implemented")
+	}
 	return t.ExecuteWithContext(context.Background(), templ, wr, data)
 }
 
diff --git a/tpl/transform/init.go b/tpl/transform/init.go
index dd31446bdd2..00ae8f89d04 100644
--- a/tpl/transform/init.go
+++ b/tpl/transform/init.go
@@ -14,6 +14,8 @@
 package transform
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.Emojify,
diff --git a/tpl/urls/init.go b/tpl/urls/init.go
index 3597e87c52a..ec954640d29 100644
--- a/tpl/urls/init.go
+++ b/tpl/urls/init.go
@@ -14,6 +14,8 @@
 package urls
 
 import (
+	"context"
+
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
@@ -26,7 +28,7 @@ func init() {
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
-			Context: func(args ...any) (any, error) { return ctx, nil },
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
 		}
 
 		ns.AddMethodMapping(ctx.AbsURL,


Test patch:
diff --git a/tpl/page/integration_test.go b/tpl/page/integration_test.go
new file mode 100644
index 00000000000..30a4885e0cd
--- /dev/null
+++ b/tpl/page/integration_test.go
@@ -0,0 +1,179 @@
+// 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 page_test
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+func TestThatPageIsAvailableEverywhere(t *testing.T) {
+	t.Parallel()
+
+	filesTemplate := `
+-- config.toml --
+baseURL = 'http://example.com/'
+disableKinds = ["taxonomy", "term"]
+enableInlineShortcodes = true
+paginate = 1
+enableRobotsTXT = true
+LANG_CONFIG
+-- content/_index.md --
+---
+title: "Home"
+aliases: ["/homealias/"]
+---
+{{< shortcode "Angled Brackets" >}}
+{{% shortcode "Percentage" %}}
+
+{{< outer >}}
+{{< inner >}}
+{{< /outer >}}
+
+{{< foo.inline >}}{{ if page.IsHome }}Shortcode Inline OK.{{ end }}{{< /foo.inline >}}
+
+## Heading
+
+[I'm an inline-style link](https://www.google.com)
+
+![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1")
+
+$$$bash
+echo "hello";
+$$$
+
+-- content/p1.md --
+-- content/p2/index.md --
+-- content/p2/p2_1.md --
+---
+title: "P2_1"
+---
+{{< foo.inline >}}{{ if page.IsHome }}Shortcode in bundled page OK.{{ else}}Failed.{{ end }}{{< /foo.inline >}}
+-- content/p3.md --
+-- layouts/_default/_markup/render-heading.html --
+{{ if page.IsHome }}
+Heading OK.
+{{ end }}
+-- layouts/_default/_markup/render-image.html --
+{{ if page.IsHome }}
+Image OK.
+{{ end }}
+-- layouts/_default/_markup/render-link.html --
+{{ if page.IsHome }}
+Link OK.
+{{ end }}
+-- layouts/_default/myview.html
+{{ if page.IsHome }}
+Render OK.
+{{ end }}
+-- layouts/_default/_markup/render-codeblock.html --
+{{ if page.IsHome }}
+Codeblock OK.
+{{ end }}
+-- layouts/_default/single.html --
+Single.
+-- layouts/index.html --
+{{ if eq page . }}Page OK.{{ end }}
+{{ $r := "{{ if page.IsHome }}ExecuteAsTemplate OK.{{ end }}" | resources.FromString "foo.html" |  resources.ExecuteAsTemplate "foo.html" . }}
+{{ $r.Content }}
+{{ .RenderString "{{< renderstring.inline >}}{{ if page.IsHome }}RenderString OK.{{ end }}{{< /renderstring.inline >}}}}"}}
+{{ .Render "myview" }}
+{{ .Content }}
+partial: {{ partials.Include "foo.html" . }}
+{{ $pag := (.Paginate site.RegularPages) }}
+PageNumber: {{ $pag.PageNumber }}/{{ $pag.TotalPages }}|
+{{ $p2 := site.GetPage "p2" }}
+{{ $p2_1 := index $p2.Resources 0 }}
+Bundled page: {{ $p2_1.Content }}
+-- layouts/alias.html --
+{{ if eq page .Page }}Alias OK.{{ else }}Failed.{{ end }}
+-- layouts/404.html --
+{{ if eq page . }}404 Page OK.{{ else }}Failed.{{ end }}
+-- layouts/partials/foo.html --
+{{ if page.IsHome }}Partial OK.{{ else }}Failed.{{ end }}
+-- layouts/shortcodes/outer.html --
+{{ .Inner }}
+-- layouts/shortcodes/inner.html --
+{{ if page.IsHome }}Shortcode Inner OK.{{ else }}Failed.{{ end }}
+-- layouts/shortcodes/shortcode.html --
+{{ if page.IsHome }}Shortcode {{ .Get 0 }} OK.{{ else }}Failed.{{ end }}
+-- layouts/sitemap.xml --
+HRE?{{ if eq page . }}Sitemap OK.{{ else }}Failed.{{ end }}
+-- layouts/robots.txt --
+{{ if eq page . }}Robots OK.{{ else }}Failed.{{ end }}
+-- layouts/sitemapindex.xml --
+{{ if not page }}SitemapIndex OK.{{ else }}Failed.{{ end }}
+
+  `
+
+	for _, multilingual := range []bool{false, true} {
+		t.Run(fmt.Sprintf("multilingual-%t", multilingual), func(t *testing.T) {
+			// Fenced code blocks.
+			files := strings.ReplaceAll(filesTemplate, "$$$", "```")
+
+			if multilingual {
+				files = strings.ReplaceAll(files, "LANG_CONFIG", `
+[languages]
+[languages.en]
+weight = 1
+[languages.no]
+weight = 2
+`)
+			} else {
+				files = strings.ReplaceAll(files, "LANG_CONFIG", "")
+			}
+
+			b := hugolib.NewIntegrationTestBuilder(
+				hugolib.IntegrationTestConfig{
+					T:           t,
+					TxtarString: files,
+				},
+			).Build()
+
+			b.AssertFileContent("public/index.html", `
+Heading OK.
+Image OK.
+Link OK.
+Codeblock OK.
+Page OK.
+Partial OK.
+Shortcode Angled Brackets OK.
+Shortcode Percentage OK.
+Shortcode Inner OK.
+Shortcode Inline OK.
+ExecuteAsTemplate OK.
+RenderString OK.
+Render OK.
+Shortcode in bundled page OK.
+	`)
+
+			b.AssertFileContent("public/404.html", `404 Page OK.`)
+			b.AssertFileContent("public/robots.txt", `Robots OK.`)
+			b.AssertFileContent("public/homealias/index.html", `Alias OK.`)
+			b.AssertFileContent("public/page/1/index.html", `Alias OK.`)
+			b.AssertFileContent("public/page/2/index.html", `Page OK.`)
+			if multilingual {
+				b.AssertFileContent("public/sitemap.xml", `SitemapIndex OK.`)
+			} else {
+				b.AssertFileContent("public/sitemap.xml", `Sitemap OK.`)
+			}
+
+		})
+
+	}
+
+}