shithub: hugo

Download patch

ref: e625088ef5a970388ad50e464e87db56b358dac4
parent: 67f3aa72cf9aaf3d6e447fa6bc12de704d46adf7
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Wed Nov 27 08:42:36 EST 2019

Add render template hooks for links and images

This commit also

* revises the change detection for templates used by content files in server mode.
* Adds a Page.RenderString method

Fixes #6545
Fixes #4663
Closes #6043

diff: cannot open b/identity//null: file does not exist: 'b/identity//null' diff: cannot open b/markup/converter/hooks//null: file does not exist: 'b/markup/converter/hooks//null'
--- /dev/null
+++ b/docs/content/en/functions/RenderString.md
@@ -1,0 +1,37 @@
+---
+title: .RenderString
+description: "Renders markup to HTML."
+godocref:
+date: 2019-12-18
+categories: [functions]
+menu:
+  docs:
+    parent: "functions"
+keywords: [markdown,goldmark,render]
+signature: [".RenderString MARKUP"]
+---
+
+{{< new-in "0.62.0" >}} 
+
+`.RenderString` is a method on `Page` that renders some markup to HTML using the content renderer defined for that page (if not set in the options).
+
+The method takes an optional map argument with these options:
+
+display ("inline")
+: `inline` or `block`. If `inline` (default), surrounding ´<p></p>` on short snippets will be trimmed.
+
+markup (defaults to the Page's markup)
+: See identifiers in [List of content formats](/content-management/formats/#list-of-content-formats).
+
+Some examples:
+
+```go-html-template
+{{ $optBlock := dict "display" "block" }}
+{{ $optOrg := dict "markup" "org" }}
+{{ "**Bold Markdown**" | $p.RenderString }}
+{{  "**Bold Block Markdown**" | $p.RenderString  $optBlock }}
+{{  "/italic org mode/" | $p.RenderString  $optOrg }}:REND
+```
+
+
+**Note** that this method is more powerful than the similar [markdownify](functions/markdownify/) function as it also supports [Render Hooks](/getting-started/configuration-markup/#markdown-render-hooks) and it has options to render other markup formats.
\ No newline at end of file
--- a/docs/content/en/getting-started/configuration-markup.md
+++ b/docs/content/en/getting-started/configuration-markup.md
@@ -74,3 +74,62 @@
 
 ordered
 : Whether or not to generate an ordered list instead of an unordered list.
+
+
+## Markdown Render Hooks
+
+{{< new-in "0.62.0" >}}
+
+Note that this is only supported with the [Goldmark](#goldmark) renderer.
+
+These Render Hooks allow custom templates to render links and images from markdown.
+
+You can do this by creating templates with base names `render-link` and/or `render-image` inside `layouts/_default`.
+
+You can define [Output Format](/templates/output-formats) specific templates if needed.[^1] Your `layouts` folder may then look like this:
+
+```bash
+layouts
+└── _default
+    └── markup
+        ├── render-image.html
+        ├── render-image.rss.xml
+        └── render-link.html
+```
+
+Some use cases for the above:
+
+* Resolve link references using `.GetPage`. This would make links more portable as you could translate `./my-post.md` (and similar constructs that would work on GitHub) into `/blog/2019/01/01/my-post/` etc.
+* Add `target=blank` to external links.
+* Resolve (look in the page bundle, inside `/assets` etc.) and [transform](/content-management/image-processing) images.
+
+
+[^1]: It's currently only possible to have one set of render hook templates, e.g. not per `Type` or `Section`. We may consider that in a future version.
+
+### Render Hook Templates
+
+Both `render-link` and `render-image` templates will receive this context:
+
+Page
+: The [Page](/variables/page/) being rendered.
+
+Destination
+: The URL.
+
+Title
+: The title attribute.
+
+Text
+: The link text.
+
+A Markdown example for a inline-style link with title:
+
+```md
+[Text](https://www.gohugo.io "Title")
+```
+
+A very simple template example given the above:
+
+{{< code file="layouts/_default/render-link.html" >}}
+<a href="{{ .Destination | safeURL }}"{{ with .Title}}title="{{ . }}"{{ end }}>{{ .Text }}{{ with .Page }} (in page {{ .Title }}){{ end }}"</a>
+{{< /code >}}
--- a/helpers/content.go
+++ b/helpers/content.go
@@ -25,6 +25,8 @@
 
 	"github.com/gohugoio/hugo/common/loggers"
 
+	"github.com/spf13/afero"
+
 	"github.com/gohugoio/hugo/markup/converter"
 
 	"github.com/gohugoio/hugo/markup"
@@ -31,7 +33,6 @@
 
 	bp "github.com/gohugoio/hugo/bufferpool"
 	"github.com/gohugoio/hugo/config"
-	"github.com/spf13/afero"
 
 	"strings"
 )
@@ -78,6 +79,7 @@
 		ContentFs: contentFs,
 		Logger:    logger,
 	})
+
 	if err != nil {
 		return nil, err
 	}
--- /dev/null
+++ b/hugolib/content_render_hooks_test.go
@@ -1,0 +1,244 @@
+// Copyright 2019 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 requiredF 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 "testing"
+
+func TestRenderHooks(t *testing.T) {
+	config := `
+baseURL="https://example.org"
+workingDir="/mywork"
+`
+	b := newTestSitesBuilder(t).WithWorkingDir("/mywork").WithConfigFile("toml", config).Running()
+	b.WithTemplatesAdded("_default/single.html", `{{ .Content }}`)
+	b.WithTemplatesAdded("shortcodes/myshortcode1.html", `{{ partial "mypartial1" }}`)
+	b.WithTemplatesAdded("shortcodes/myshortcode2.html", `{{ partial "mypartial2" }}`)
+	b.WithTemplatesAdded("shortcodes/myshortcode3.html", `SHORT3|`)
+	b.WithTemplatesAdded("shortcodes/myshortcode4.html", `
+<div class="foo">
+{{ .Inner | markdownify }}
+</div>
+`)
+	b.WithTemplatesAdded("shortcodes/myshortcode5.html", `
+Inner Inline: {{ .Inner | .Page.RenderString }}
+Inner Block: {{ .Inner | .Page.RenderString (dict "display" "block" ) }}
+`)
+
+	b.WithTemplatesAdded("shortcodes/myshortcode6.html", `.Render: {{ .Page.Render "myrender" }}`)
+	b.WithTemplatesAdded("partials/mypartial1.html", `PARTIAL1`)
+	b.WithTemplatesAdded("partials/mypartial2.html", `PARTIAL2  {{ partial "mypartial3.html" }}`)
+	b.WithTemplatesAdded("partials/mypartial3.html", `PARTIAL3`)
+	b.WithTemplatesAdded("partials/mypartial4.html", `PARTIAL4`)
+	b.WithTemplatesAdded("customview/myrender.html", `myrender: {{ .Title }}|P4: {{ partial "mypartial4" }}`)
+	b.WithTemplatesAdded("_default/_markup/render-link.html", `{{ with .Page }}{{ .Title }}{{ end }}|{{ .Destination | safeURL }}|Title: {{ .Title | safeHTML }}|Text: {{ .Text | safeHTML }}|END`)
+	b.WithTemplatesAdded("docs/_markup/render-link.html", `Link docs section: {{ .Text | safeHTML }}|END`)
+	b.WithTemplatesAdded("_default/_markup/render-image.html", `IMAGE: {{ .Page.Title }}||{{ .Destination | safeURL }}|Title: {{ .Title | safeHTML }}|Text: {{ .Text | safeHTML }}|END`)
+
+	b.WithContent("customview/p1.md", `---
+title: Custom View
+---
+
+{{< myshortcode6 >}}
+
+	`, "blog/p1.md", `---
+title: Cool Page
+---
+
+[First Link](https://www.google.com "Google's Homepage")
+
+{{< myshortcode3 >}}
+
+[Second Link](https://www.google.com "Google's Homepage")
+
+Image:
+
+![Drag Racing](/images/Dragster.jpg "image title")
+
+
+`, "blog/p2.md", `---
+title: Cool Page2
+layout: mylayout
+---
+
+{{< myshortcode1 >}}
+
+[Some Text](https://www.google.com "Google's Homepage")
+
+
+
+`, "blog/p3.md", `---
+title: Cool Page3
+---
+
+{{< myshortcode2 >}}
+
+
+`, "docs/docs1.md", `---
+title: Docs 1
+---
+
+
+[Docs 1](https://www.google.com "Google's Homepage")
+
+
+`, "blog/p4.md", `---
+title: Cool Page With Image
+---
+
+Image:
+
+![Drag Racing](/images/Dragster.jpg "image title")
+
+
+`, "blog/p5.md", `---
+title: Cool Page With Markdownify
+---
+
+{{< myshortcode4 >}}
+Inner Link: [Inner Link](https://www.google.com "Google's Homepage")
+{{< /myshortcode4 >}}
+
+`, "blog/p6.md", `---
+title: With RenderString
+---
+
+{{< myshortcode5 >}}Inner Link: [Inner Link](https://www.gohugo.io "Hugo's Homepage"){{< /myshortcode5 >}}
+
+`)
+	b.Build(BuildCfg{})
+	b.AssertFileContent("public/blog/p1/index.html", `
+<p>Cool Page|https://www.google.com|Title: Google's Homepage|Text: First Link|END</p>
+Text: Second
+SHORT3|
+<p>IMAGE: Cool Page||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END</p>
+`)
+
+	b.AssertFileContent("public/customview/p1/index.html", `.Render: myrender: Custom View|P4: PARTIAL4`)
+	b.AssertFileContent("public/blog/p2/index.html", `PARTIAL`)
+	b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3`)
+	// We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `Link docs section: Docs 1|END`)
+	b.AssertFileContent("public/blog/p4/index.html", `<p>IMAGE: Cool Page With Image||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END</p>`)
+	// The regular markdownify func currently gets regular links.
+	b.AssertFileContent("public/blog/p5/index.html", "Inner Link: <a href=\"https://www.google.com\" title=\"Google's Homepage\">Inner Link</a>\n</div>")
+
+	b.AssertFileContent("public/blog/p6/index.html",
+		"Inner Inline: Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END",
+		"Inner Block: <p>Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END</p>",
+	)
+
+	b.EditFiles(
+		"layouts/_default/_markup/render-link.html", `EDITED: {{ .Destination | safeURL }}|`,
+		"layouts/_default/_markup/render-image.html", `IMAGE EDITED: {{ .Destination | safeURL }}|`,
+		"layouts/docs/_markup/render-link.html", `DOCS EDITED: {{ .Destination | safeURL }}|`,
+		"layouts/partials/mypartial1.html", `PARTIAL1_EDITED`,
+		"layouts/partials/mypartial3.html", `PARTIAL3_EDITED`,
+		"layouts/partials/mypartial4.html", `PARTIAL4_EDITED`,
+		"layouts/shortcodes/myshortcode3.html", `SHORT3_EDITED|`,
+	)
+
+	b.Build(BuildCfg{})
+	b.AssertFileContent("public/customview/p1/index.html", `.Render: myrender: Custom View|P4: PARTIAL4_EDITED`)
+	b.AssertFileContent("public/blog/p1/index.html", `<p>EDITED: https://www.google.com|</p>`, "SHORT3_EDITED|")
+	b.AssertFileContent("public/blog/p2/index.html", `PARTIAL1_EDITED`)
+	b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3_EDITED`)
+	// We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `DOCS EDITED: https://www.google.com|</p>`)
+	b.AssertFileContent("public/blog/p4/index.html", `IMAGE EDITED: /images/Dragster.jpg|`)
+	b.AssertFileContent("public/blog/p6/index.html", "<p>Inner Link: EDITED: https://www.gohugo.io|</p>")
+
+}
+
+func TestRenderHooksRSS(t *testing.T) {
+
+	b := newTestSitesBuilder(t)
+
+	b.WithTemplates("index.html", `
+{{ $p := site.GetPage "p1.md" }}
+
+P1: {{ $p.Content }}
+	
+	`, "index.xml", `
+
+{{ $p2 := site.GetPage "p2.md" }}
+{{ $p3 := site.GetPage "p3.md" }}
+
+P2: {{ $p2.Content }}
+P3: {{ $p3.Content }}
+
+	
+	`,
+		"_default/_markup/render-link.html", `html-link: {{ .Destination | safeURL }}|`,
+		"_default/_markup/render-link.rss.xml", `xml-link: {{ .Destination | safeURL }}|`,
+	)
+
+	b.WithContent("p1.md", `---
+title: "p1"
+---
+P1. [I'm an inline-style link](https://www.gohugo.io)
+
+
+`, "p2.md", `---
+title: "p2"
+---
+P1. [I'm an inline-style link](https://www.bep.is)
+
+
+`,
+		"p3.md", `---
+title: "p2"
+outputs: ["rss"]
+---
+P3. [I'm an inline-style link](https://www.example.org)
+
+`,
+	)
+
+	b.Build(BuildCfg{})
+
+	b.AssertFileContent("public/index.html", "P1: <p>P1. html-link: https://www.gohugo.io|</p>")
+	b.AssertFileContent("public/index.xml", `
+P2: <p>P1. xml-link: https://www.bep.is|</p>
+P3: <p>P3. xml-link: https://www.example.org|</p>
+`)
+
+}
+
+func TestRenderString(t *testing.T) {
+
+	b := newTestSitesBuilder(t)
+
+	b.WithTemplates("index.html", `
+{{ $p := site.GetPage "p1.md" }}
+{{ $optBlock := dict "display" "block" }}
+{{ $optOrg := dict "markup" "org" }}
+RSTART:{{ "**Bold Markdown**" | $p.RenderString }}:REND
+RSTART:{{  "**Bold Block Markdown**" | $p.RenderString  $optBlock }}:REND
+RSTART:{{  "/italic org mode/" | $p.RenderString  $optOrg }}:REND
+
+`)
+
+	b.WithContent("p1.md", `---
+title: "p1"
+---
+`,
+	)
+
+	b.Build(BuildCfg{})
+
+	b.AssertFileContent("public/index.html", `
+RSTART:<strong>Bold Markdown</strong>:REND
+RSTART:<p><strong>Bold Block Markdown</strong></p>
+RSTART:<em>italic org mode</em>:REND
+`)
+
+}
--- a/hugolib/filesystems/basefs.go
+++ b/hugolib/filesystems/basefs.go
@@ -126,10 +126,28 @@
 	StaticDirs []hugofs.FileMetaInfo
 }
 
+// FileSystems returns the FileSystems relevant for the change detection
+// in server mode.
+// Note: This does currently not return any static fs.
+func (s *SourceFilesystems) FileSystems() []*SourceFilesystem {
+	return []*SourceFilesystem{
+		s.Content,
+		s.Data,
+		s.I18n,
+		s.Layouts,
+		s.Archetypes,
+		// TODO(bep) static
+	}
+
+}
+
 // A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
 // i18n, layouts, static) and additional metadata to be able to use that filesystem
 // in server mode.
 type SourceFilesystem struct {
+	// Name matches one in files.ComponentFolders
+	Name string
+
 	// This is a virtual composite filesystem. It expects path relative to a context.
 	Fs afero.Fs
 
@@ -275,6 +293,19 @@
 	return false
 }
 
+// Path returns the relative path to the given filename if it is a member of
+// of the current filesystem, an empty string if not.
+func (d *SourceFilesystem) Path(filename string) string {
+	for _, dir := range d.Dirs {
+		meta := dir.Meta()
+		if strings.HasPrefix(filename, meta.Filename()) {
+			p := strings.TrimPrefix(strings.TrimPrefix(filename, meta.Filename()), filePathSeparator)
+			return p
+		}
+	}
+	return ""
+}
+
 // RealDirs gets a list of absolute paths to directories starting from the given
 // path.
 func (d *SourceFilesystem) RealDirs(from string) []string {
@@ -349,12 +380,14 @@
 	return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}}
 }
 
-func (b *sourceFilesystemsBuilder) newSourceFilesystem(fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem {
+func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem {
 	return &SourceFilesystem{
+		Name: name,
 		Fs:   fs,
 		Dirs: dirs,
 	}
 }
+
 func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
 
 	if b.theBigFs == nil {
@@ -369,12 +402,12 @@
 
 	createView := func(componentID string) *SourceFilesystem {
 		if b.theBigFs == nil || b.theBigFs.overlayMounts == nil {
-			return b.newSourceFilesystem(hugofs.NoOpFs, nil)
+			return b.newSourceFilesystem(componentID, hugofs.NoOpFs, nil)
 		}
 
 		dirs := b.theBigFs.overlayDirs[componentID]
 
-		return b.newSourceFilesystem(afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
+		return b.newSourceFilesystem(componentID, afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
 
 	}
 
@@ -392,7 +425,7 @@
 		return nil, err
 	}
 
-	b.result.Data = b.newSourceFilesystem(dataFs, dataDirs)
+	b.result.Data = b.newSourceFilesystem(files.ComponentFolderData, dataFs, dataDirs)
 
 	i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n]
 	i18nFs, err := hugofs.NewSliceFs(i18nDirs...)
@@ -399,7 +432,7 @@
 	if err != nil {
 		return nil, err
 	}
-	b.result.I18n = b.newSourceFilesystem(i18nFs, i18nDirs)
+	b.result.I18n = b.newSourceFilesystem(files.ComponentFolderI18n, i18nFs, i18nDirs)
 
 	contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent]
 	contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent)
@@ -409,7 +442,7 @@
 		return nil, errors.Wrap(err, "create content filesystem")
 	}
 
-	b.result.Content = b.newSourceFilesystem(contentFs, contentDirs)
+	b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs, contentDirs)
 
 	b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull)
 
@@ -421,13 +454,13 @@
 	if b.theBigFs.staticPerLanguage != nil {
 		// Multihost mode
 		for k, v := range b.theBigFs.staticPerLanguage {
-			sfs := b.newSourceFilesystem(v, b.result.StaticDirs)
+			sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v, b.result.StaticDirs)
 			sfs.PublishFolder = k
 			ms[k] = sfs
 		}
 	} else {
 		bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic)
-		ms[""] = b.newSourceFilesystem(bfs, b.result.StaticDirs)
+		ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs, b.result.StaticDirs)
 	}
 
 	return b.result, nil
--- a/hugolib/hugo_modules_test.go
+++ b/hugolib/hugo_modules_test.go
@@ -40,6 +40,9 @@
 
 // TODO(bep) this fails when testmodBuilder is also building ...
 func TestHugoModules(t *testing.T) {
+	if !isCI() {
+		t.Skip("skip (relative) long running modules test when running locally")
+	}
 	t.Parallel()
 
 	if !isCI() || hugo.GoMinorVersion() < 12 {
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -20,6 +20,8 @@
 	"strings"
 	"sync"
 
+	"github.com/gohugoio/hugo/identity"
+
 	radix "github.com/armon/go-radix"
 
 	"github.com/gohugoio/hugo/output"
@@ -411,7 +413,6 @@
 			}
 			d.OutputFormatsConfig = s.outputFormatsConfig
 		}
-
 	}
 
 	return nil
@@ -806,12 +807,40 @@
 	return h.Sites[0].findPagesByKindIn(kind, inPages)
 }
 
-func (h *HugoSites) findPagesByShortcode(shortcode string) page.Pages {
-	var pages page.Pages
+func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
+
 	for _, s := range h.Sites {
-		pages = append(pages, s.findPagesByShortcode(shortcode)...)
+	PAGES:
+		for _, p := range s.rawAllPages {
+		OUTPUTS:
+			for _, po := range p.pageOutputs {
+				if po.cp == nil {
+					continue
+				}
+				for id, _ := range idset {
+					if po.cp.dependencyTracker.Search(id) != nil {
+						po.cp.Reset()
+						p.forceRender = true
+						continue OUTPUTS
+					}
+				}
+			}
+
+			for _, s := range p.shortcodeState.shortcodes {
+				for id, _ := range idset {
+					if idm, ok := s.info.(identity.Manager); ok && idm.Search(id) != nil {
+						for _, po := range p.pageOutputs {
+							if po.cp != nil {
+								po.cp.Reset()
+							}
+						}
+						p.forceRender = true
+						continue PAGES
+					}
+				}
+			}
+		}
 	}
-	return pages
 }
 
 // Used in partial reloading to determine if the change is in a bundle.
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -71,7 +71,7 @@
 
 	if conf.whatChanged == nil {
 		// Assume everything has changed
-		conf.whatChanged = &whatChanged{source: true, other: true}
+		conf.whatChanged = &whatChanged{source: true}
 	}
 
 	var prepareErr error
--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -1459,3 +1459,19 @@
 
 	return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData}
 }
+
+func TestRebuildOnAssetChange(t *testing.T) {
+	b := newTestSitesBuilder(t).Running()
+	b.WithTemplatesAdded("index.html", `
+{{ (resources.Get "data.json").Content }}
+`)
+	b.WithSourceFile("assets/data.json", "orig data")
+
+	b.Build(BuildCfg{})
+	b.AssertFileContent("public/index.html", `orig data`)
+
+	b.EditFiles("assets/data.json", "changed data")
+
+	b.Build(BuildCfg{})
+	b.AssertFileContent("public/index.html", `changed data`)
+}
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -23,6 +23,12 @@
 	"sort"
 	"strings"
 
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/gohugoio/hugo/tpl"
+
+	"github.com/gohugoio/hugo/identity"
+
 	"github.com/gohugoio/hugo/markup/converter"
 
 	"github.com/gohugoio/hugo/common/maps"
@@ -43,9 +49,11 @@
 
 	"github.com/gohugoio/hugo/media"
 	"github.com/gohugoio/hugo/source"
+	"github.com/spf13/cast"
 
 	"github.com/gohugoio/hugo/common/collections"
 	"github.com/gohugoio/hugo/common/text"
+	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/gohugoio/hugo/resources"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/resources/resource"
@@ -59,7 +67,11 @@
 
 var (
 	pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType)
-	nopPageOutput     = &pageOutput{pagePerOutputProviders: nopPagePerOutput}
+	nopPageOutput     = &pageOutput{
+		pagePerOutputProviders:  nopPagePerOutput,
+		ContentProvider:         page.NopPage,
+		TableOfContentsProvider: page.NopPage,
+	}
 )
 
 // pageContext provides contextual information about this page, for error
@@ -317,6 +329,54 @@
 	return nil
 }
 
+func (p *pageState) createRenderHooks(f output.Format) (*hooks.Render, error) {
+
+	layoutDescriptor := p.getLayoutDescriptor()
+	layoutDescriptor.RenderingHook = true
+	layoutDescriptor.LayoutOverride = false
+	layoutDescriptor.Layout = ""
+
+	layoutDescriptor.Kind = "render-link"
+	linkLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
+	if err != nil {
+		return nil, err
+	}
+
+	layoutDescriptor.Kind = "render-image"
+	imageLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
+	if err != nil {
+		return nil, err
+	}
+
+	if linkLayouts == nil && imageLayouts == nil {
+		return nil, nil
+	}
+
+	var linkRenderer hooks.LinkRenderer
+	var imageRenderer hooks.LinkRenderer
+
+	if templ, found := p.s.lookupTemplate(linkLayouts...); found {
+		linkRenderer = contentLinkRenderer{
+			templateHandler: p.s.Tmpl,
+			Provider:        templ.(tpl.Info),
+			templ:           templ,
+		}
+	}
+
+	if templ, found := p.s.lookupTemplate(imageLayouts...); found {
+		imageRenderer = contentLinkRenderer{
+			templateHandler: p.s.Tmpl,
+			Provider:        templ.(tpl.Info),
+			templ:           templ,
+		}
+	}
+
+	return &hooks.Render{
+		LinkRenderer:  linkRenderer,
+		ImageRenderer: imageRenderer,
+	}, nil
+}
+
 func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
 	p.layoutDescriptorInit.Do(func() {
 		var section string
@@ -464,11 +524,86 @@
 	return o
 }
 
-func (p *pageState) Render(layout ...string) template.HTML {
+type renderStringOpts struct {
+	Display string
+	Markup  string
+}
+
+var defualtRenderStringOpts = renderStringOpts{
+	Display: "inline",
+	Markup:  "", // Will inherit the page's value when not set.
+}
+
+func (p *pageState) RenderString(args ...interface{}) (template.HTML, error) {
+	if len(args) < 1 || len(args) > 2 {
+		return "", errors.New("want 1 or 2 arguments")
+	}
+
+	var s string
+	opts := defualtRenderStringOpts
+	sidx := 1
+
+	if len(args) == 1 {
+		sidx = 0
+	} else {
+		m, ok := args[0].(map[string]interface{})
+		if !ok {
+			return "", errors.New("first argument must be a map")
+		}
+
+		if err := mapstructure.WeakDecode(m, &opts); err != nil {
+			return "", errors.WithMessage(err, "failed to decode options")
+		}
+	}
+
+	var err error
+	s, err = cast.ToStringE(args[sidx])
+	if err != nil {
+		return "", err
+	}
+
+	conv := p.getContentConverter()
+	if opts.Markup != "" && opts.Markup != p.m.markup {
+		var err error
+		// TODO(bep) consider cache
+		conv, err = p.m.newContentConverter(p, opts.Markup, nil)
+		if err != nil {
+			return "", p.wrapError(err)
+		}
+	}
+
+	c, err := p.pageOutput.cp.renderContentWithConverter(conv, []byte(s), false)
+	if err != nil {
+		return "", p.wrapError(err)
+	}
+
+	b := c.Bytes()
+
+	if opts.Display == "inline" {
+		// We may have to rethink this in the future when we get other
+		// renderers.
+		b = p.s.ContentSpec.TrimShortHTML(b)
+	}
+
+	return template.HTML(string(b)), nil
+}
+
+func (p *pageState) addDependency(dep identity.Provider) {
+	if !p.s.running() || p.pageOutput.cp == nil {
+		return
+	}
+	p.pageOutput.cp.dependencyTracker.Add(dep)
+}
+
+func (p *pageState) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
+	p.addDependency(info)
+	return p.Render(layout...)
+}
+
+func (p *pageState) Render(layout ...string) (template.HTML, error) {
 	l, err := p.getLayouts(layout...)
 	if err != nil {
-		p.s.SendError(p.wrapError(errors.Errorf(".Render: failed to resolve layout %v", layout)))
-		return ""
+		return "", p.wrapError(errors.Errorf("failed to resolve layout %v", layout))
 	}
 
 	for _, layout := range l {
@@ -479,17 +614,18 @@
 			// We default to good old HTML.
 			templ, _ = p.s.Tmpl.Lookup(layout + ".html")
 		}
+
 		if templ != nil {
+			p.addDependency(templ.(tpl.Info))
 			res, err := executeToString(p.s.Tmpl, templ, p)
 			if err != nil {
-				p.s.SendError(p.wrapError(errors.Wrapf(err, ".Render: failed to execute template %q v", layout)))
-				return ""
+				return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
 			}
-			return template.HTML(res)
+			return template.HTML(res), nil
 		}
 	}
 
-	return ""
+	return "", nil
 
 }
 
@@ -745,15 +881,33 @@
 		p.pageOutput.paginator.reset()
 	}
 
-	if idx > 0 {
-		// Check if we can reuse content from one of the previous formats.
-		for i := idx - 1; i >= 0; i-- {
-			po := p.pageOutputs[i]
-			if po.cp != nil && po.cp.reuse {
-				p.pageOutput.cp = po.cp
-				break
+	if isRenderingSite {
+		cp := p.pageOutput.cp
+		if cp == nil {
+
+			// Look for content to reuse.
+			for i := 0; i < len(p.pageOutputs); i++ {
+				if i == idx {
+					continue
+				}
+				po := p.pageOutputs[i]
+
+				if po.cp != nil && po.cp.reuse {
+					cp = po.cp
+					break
+				}
 			}
 		}
+
+		if cp == nil {
+			var err error
+			cp, err = newPageContentOutput(p, p.pageOutput)
+			if err != nil {
+				return err
+			}
+		}
+		p.pageOutput.initContentProvider(cp)
+		p.pageOutput.cp = cp
 	}
 
 	for _, r := range p.Resources().ByType(pageResourceType) {
--- a/hugolib/page__content.go
+++ b/hugolib/page__content.go
@@ -30,8 +30,7 @@
 type pageContent struct {
 	renderable bool
 	selfLayout string
-
-	truncated bool
+	truncated  bool
 
 	cmap *pageContentMap
 
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -592,7 +592,7 @@
 	return nil
 }
 
-func (p *pageMeta) applyDefaultValues() error {
+func (p *pageMeta) applyDefaultValues(ps *pageState) error {
 	if p.markup == "" {
 		if !p.File().IsZero() {
 			// Fall back to file extension
@@ -651,25 +651,37 @@
 			markup = "markdown"
 		}
 
-		cp := p.s.ContentSpec.Converters.Get(markup)
-		if cp == nil {
-			return errors.Errorf("no content renderer found for markup %q", p.markup)
+		cp, err := p.newContentConverter(ps, markup, renderingConfigOverrides)
+		if err != nil {
+			return err
 		}
+		p.contentConverter = cp
+	}
 
-		cpp, err := cp.New(converter.DocumentContext{
+	return nil
+
+}
+
+func (p *pageMeta) newContentConverter(ps *pageState, markup string, renderingConfigOverrides map[string]interface{}) (converter.Converter, error) {
+	cp := p.s.ContentSpec.Converters.Get(markup)
+	if cp == nil {
+		return nil, errors.Errorf("no content renderer found for markup %q", p.markup)
+	}
+
+	cpp, err := cp.New(
+		converter.DocumentContext{
+			Document:        newPageForRenderHook(ps),
 			DocumentID:      p.f.UniqueID(),
 			DocumentName:    p.f.Path(),
 			ConfigOverrides: renderingConfigOverrides,
-		})
+		},
+	)
 
-		if err != nil {
-			return err
-		}
-		p.contentConverter = cpp
+	if err != nil {
+		return nil, err
 	}
 
-	return nil
-
+	return cpp, nil
 }
 
 // The output formats this page will be rendered to.
--- a/hugolib/page__new.go
+++ b/hugolib/page__new.go
@@ -112,7 +112,7 @@
 			}
 		}
 
-		if err := metaProvider.applyDefaultValues(); err != nil {
+		if err := metaProvider.applyDefaultValues(ps); err != nil {
 			return err
 		}
 
@@ -134,7 +134,7 @@
 		}
 
 		makeOut := func(f output.Format, render bool) *pageOutput {
-			return newPageOutput(nil, ps, pp, f, render)
+			return newPageOutput(ps, pp, f, render)
 		}
 
 		if ps.m.standalone {
@@ -234,7 +234,7 @@
 			return ps.wrapError(err)
 		}
 
-		if err := metaProvider.applyDefaultValues(); err != nil {
+		if err := metaProvider.applyDefaultValues(ps); err != nil {
 			return err
 		}
 
@@ -242,11 +242,7 @@
 	}
 
 	ps.init.Add(func() (interface{}, error) {
-		reuseContent := ps.renderable && !ps.shortcodeState.hasShortcodes()
 
-		// Creates what's needed for each output format.
-		contentPerOutput := newPageContentOutput(ps)
-
 		pp, err := newPagePaths(s, ps, metaProvider)
 		if err != nil {
 			return nil, err
@@ -264,18 +260,18 @@
 			}
 
 			_, render := outputFormatsForPage.GetByName(f.Name)
-			var contentProvider *pageContentOutput
-			if reuseContent && i > 0 {
-				contentProvider = ps.pageOutputs[0].cp
-			} else {
-				var err error
-				contentProvider, err = contentPerOutput(f)
+			po := newPageOutput(ps, pp, f, render)
+
+			// Create a content provider for the first,
+			// we may be able to reuse it.
+			if i == 0 {
+				contentProvider, err := newPageContentOutput(ps, po)
 				if err != nil {
 					return nil, err
 				}
+				po.initContentProvider(contentProvider)
 			}
 
-			po := newPageOutput(contentProvider, ps, pp, f, render)
 			ps.pageOutputs[i] = po
 			created[f.Name] = po
 		}
--- a/hugolib/page__output.go
+++ b/hugolib/page__output.go
@@ -14,6 +14,7 @@
 package hugolib
 
 import (
+	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/gohugoio/hugo/output"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/resources/resource"
@@ -20,7 +21,6 @@
 )
 
 func newPageOutput(
-	cp *pageContentOutput, // may be nil
 	ps *pageState,
 	pp pagePaths,
 	f output.Format,
@@ -45,25 +45,11 @@
 		paginatorProvider = pag
 	}
 
-	var (
-		contentProvider         page.ContentProvider         = page.NopPage
-		tableOfContentsProvider page.TableOfContentsProvider = page.NopPage
-	)
-
-	if cp != nil {
-		contentProvider = cp
-		tableOfContentsProvider = cp
-	}
-
 	providers := struct {
-		page.ContentProvider
-		page.TableOfContentsProvider
 		page.PaginatorProvider
 		resource.ResourceLinksProvider
 		targetPather
 	}{
-		contentProvider,
-		tableOfContentsProvider,
 		paginatorProvider,
 		linksProvider,
 		targetPathsProvider,
@@ -70,11 +56,12 @@
 	}
 
 	po := &pageOutput{
-		f:                      f,
-		cp:                     cp,
-		pagePerOutputProviders: providers,
-		render:                 render,
-		paginator:              pag,
+		f:                       f,
+		pagePerOutputProviders:  providers,
+		ContentProvider:         page.NopPage,
+		TableOfContentsProvider: page.NopPage,
+		render:                  render,
+		paginator:               pag,
 	}
 
 	return po
@@ -94,16 +81,54 @@
 	// used in template(s).
 	paginator *pagePaginator
 
-	// This interface provides the functionality that is specific for this
+	// These interface provides the functionality that is specific for this
 	// output format.
 	pagePerOutputProviders
+	page.ContentProvider
+	page.TableOfContentsProvider
 
-	// This may be nil.
+	// May be nil.
 	cp *pageContentOutput
 }
 
+func (o *pageOutput) initRenderHooks() error {
+	if o.cp == nil {
+		return nil
+	}
+
+	ps := o.cp.p
+
+	c := ps.getContentConverter()
+	if c == nil || !c.Supports(converter.FeatureRenderHooks) {
+		return nil
+	}
+
+	h, err := ps.createRenderHooks(o.f)
+	if err != nil {
+		return err
+	}
+	if h == nil {
+		return nil
+	}
+
+	o.cp.renderHooks = h
+
+	return nil
+
+}
+
+func (p *pageOutput) initContentProvider(cp *pageContentOutput) {
+	if cp == nil {
+		return
+	}
+	p.ContentProvider = cp
+	p.TableOfContentsProvider = cp
+	p.cp = cp
+}
+
 func (p *pageOutput) enablePlaceholders() {
 	if p.cp != nil {
 		p.cp.enablePlaceholders()
 	}
+
 }
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -23,6 +23,10 @@
 	"sync"
 	"unicode/utf8"
 
+	"github.com/gohugoio/hugo/identity"
+
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+
 	"github.com/gohugoio/hugo/markup/converter"
 
 	"github.com/gohugoio/hugo/lazy"
@@ -58,153 +62,175 @@
 	}
 )
 
-func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) {
+var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"}
 
+func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) {
+
 	parent := p.init
 
-	return func(f output.Format) (*pageContentOutput, error) {
-		cp := &pageContentOutput{
-			p: p,
-			f: f,
-		}
+	var dependencyTracker identity.Manager
+	if p.s.running() {
+		dependencyTracker = identity.NewManager(pageContentOutputDependenciesID)
+	}
 
-		initContent := func() (err error) {
-			if p.cmap == nil {
-				// Nothing to do.
-				return nil
+	cp := &pageContentOutput{
+		dependencyTracker: dependencyTracker,
+		p:                 p,
+		f:                 po.f,
+	}
+
+	initContent := func() (err error) {
+		if p.cmap == nil {
+			// Nothing to do.
+			return nil
+		}
+		defer func() {
+			// See https://github.com/gohugoio/hugo/issues/6210
+			if r := recover(); r != nil {
+				err = fmt.Errorf("%s", r)
+				p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
 			}
-			defer func() {
-				// See https://github.com/gohugoio/hugo/issues/6210
-				if r := recover(); r != nil {
-					err = fmt.Errorf("%s", r)
-					p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
-				}
-			}()
+		}()
 
-			var hasVariants bool
+		if err := po.initRenderHooks(); err != nil {
+			return err
+		}
 
-			cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
-			if err != nil {
-				return err
-			}
+		var hasShortcodeVariants bool
 
-			if p.render && !hasVariants {
-				// We can reuse this for the other output formats
-				cp.enableReuse()
-			}
+		f := po.f
+		cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
+		if err != nil {
+			return err
+		}
 
-			cp.workContent = p.contentToRender(cp.contentPlaceholders)
+		enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
 
-			isHTML := cp.p.m.markup == "html"
+		if enableReuse {
+			// Reuse this for the other output formats.
+			// We may improve on this, but we really want to avoid re-rendering the content
+			// to all output formats.
+			// The current rule is that if you need output format-aware shortcodes or
+			// content rendering hooks, create a output format-specific template, e.g.
+			// myshortcode.amp.html.
+			cp.enableReuse()
+		}
 
-			if p.renderable {
-				if !isHTML {
-					r, err := cp.renderContent(cp.workContent)
-					if err != nil {
-						return err
-					}
-					cp.convertedResult = r
-					cp.workContent = r.Bytes()
+		cp.workContent = p.contentToRender(cp.contentPlaceholders)
 
-					if _, ok := r.(converter.TableOfContentsProvider); !ok {
-						tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
-						cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
-						cp.workContent = tmpContent
-					}
-				}
+		isHTML := cp.p.m.markup == "html"
 
-				if cp.placeholdersEnabled {
-					// ToC was accessed via .Page.TableOfContents in the shortcode,
-					// at a time when the ToC wasn't ready.
-					cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
+		if p.renderable {
+			if !isHTML {
+				r, err := cp.renderContent(cp.workContent, true)
+				if err != nil {
+					return err
 				}
 
-				if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
-					// There are one or more replacement tokens to be replaced.
-					cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
-					if err != nil {
-						return err
-					}
+				cp.workContent = r.Bytes()
+
+				if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
+					cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
+					cp.tableOfContents = template.HTML(
+						tocProvider.TableOfContents().ToHTML(
+							cfg.TableOfContents.StartLevel,
+							cfg.TableOfContents.EndLevel,
+							cfg.TableOfContents.Ordered,
+						),
+					)
+				} else {
+					tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
+					cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
+					cp.workContent = tmpContent
 				}
+			}
 
-				if cp.p.source.hasSummaryDivider {
-					if isHTML {
-						src := p.source.parsed.Input()
+			if cp.placeholdersEnabled {
+				// ToC was accessed via .Page.TableOfContents in the shortcode,
+				// at a time when the ToC wasn't ready.
+				cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
+			}
 
-						// Use the summary sections as they are provided by the user.
-						if p.source.posSummaryEnd != -1 {
-							cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
-						}
+			if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
+				// There are one or more replacement tokens to be replaced.
+				cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
+				if err != nil {
+					return err
+				}
+			}
 
-						if cp.p.source.posBodyStart != -1 {
-							cp.workContent = src[cp.p.source.posBodyStart:]
-						}
+			if cp.p.source.hasSummaryDivider {
+				if isHTML {
+					src := p.source.parsed.Input()
 
-					} else {
-						summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
-						if err != nil {
-							cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
-						} else {
-							cp.workContent = content
-							cp.summary = helpers.BytesToHTML(summary)
-						}
+					// Use the summary sections as they are provided by the user.
+					if p.source.posSummaryEnd != -1 {
+						cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
 					}
-				} else if cp.p.m.summary != "" {
-					b, err := cp.p.getContentConverter().Convert(
-						converter.RenderContext{
-							Src: []byte(cp.p.m.summary),
-						},
-					)
 
+					if cp.p.source.posBodyStart != -1 {
+						cp.workContent = src[cp.p.source.posBodyStart:]
+					}
+
+				} else {
+					summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
 					if err != nil {
-						return err
+						cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
+					} else {
+						cp.workContent = content
+						cp.summary = helpers.BytesToHTML(summary)
 					}
-					html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
-					cp.summary = helpers.BytesToHTML(html)
 				}
+			} else if cp.p.m.summary != "" {
+				b, err := cp.renderContent([]byte(cp.p.m.summary), false)
+				if err != nil {
+					return err
+				}
+				html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
+				cp.summary = helpers.BytesToHTML(html)
 			}
-
-			cp.content = helpers.BytesToHTML(cp.workContent)
-
-			if !p.renderable {
-				err := cp.addSelfTemplate()
-				return err
-			}
-
-			return nil
-
 		}
 
-		// Recursive loops can only happen in content files with template code (shortcodes etc.)
-		// Avoid creating new goroutines if we don't have to.
-		needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
+		cp.content = helpers.BytesToHTML(cp.workContent)
 
-		if needTimeout {
-			cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
-				return nil, initContent()
-			})
-		} else {
-			cp.initMain = parent.Branch(func() (interface{}, error) {
-				return nil, initContent()
-			})
+		if !p.renderable {
+			err := cp.addSelfTemplate()
+			return err
 		}
 
-		cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
-			cp.plain = helpers.StripHTML(string(cp.content))
-			cp.plainWords = strings.Fields(cp.plain)
-			cp.setWordCounts(p.m.isCJKLanguage)
+		return nil
 
-			if err := cp.setAutoSummary(); err != nil {
-				return err, nil
-			}
+	}
 
-			return nil, nil
+	// Recursive loops can only happen in content files with template code (shortcodes etc.)
+	// Avoid creating new goroutines if we don't have to.
+	needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
+	needTimeout = needTimeout || cp.renderHooks != nil
+
+	if needTimeout {
+		cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
+			return nil, initContent()
 		})
+	} else {
+		cp.initMain = parent.Branch(func() (interface{}, error) {
+			return nil, initContent()
+		})
+	}
 
-		return cp, nil
+	cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
+		cp.plain = helpers.StripHTML(string(cp.content))
+		cp.plainWords = strings.Fields(cp.plain)
+		cp.setWordCounts(p.m.isCJKLanguage)
 
-	}
+		if err := cp.setAutoSummary(); err != nil {
+			return err, nil
+		}
 
+		return nil, nil
+	})
+
+	return cp, nil
+
 }
 
 // pageContentOutput represents the Page content for a given output format.
@@ -211,7 +237,7 @@
 type pageContentOutput struct {
 	f output.Format
 
-	// If we can safely reuse this for other output formats.
+	// If we can reuse this for other output formats.
 	reuse     bool
 	reuseInit sync.Once
 
@@ -224,10 +250,15 @@
 	placeholdersEnabled     bool
 	placeholdersEnabledInit sync.Once
 
+	// May be nil.
+	renderHooks *hooks.Render
+	// Set if there are more than one output format variant
+	renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
+
 	// Content state
 
-	workContent     []byte
-	convertedResult converter.Result
+	workContent       []byte
+	dependencyTracker identity.Manager // Set in server mode.
 
 	// Temporary storage of placeholders mapped to their content.
 	// These are shortcodes etc. Some of these will need to be replaced
@@ -248,6 +279,20 @@
 	readingTime    int
 }
 
+func (p *pageContentOutput) trackDependency(id identity.Provider) {
+	if p.dependencyTracker != nil {
+		p.dependencyTracker.Add(id)
+	}
+}
+
+func (p *pageContentOutput) Reset() {
+	if p.dependencyTracker != nil {
+		p.dependencyTracker.Reset()
+	}
+	p.initMain.Reset()
+	p.initPlain.Reset()
+}
+
 func (p *pageContentOutput) Content() (interface{}, error) {
 	if p.p.s.initInit(p.initMain, p.p) {
 		return p.content, nil
@@ -290,10 +335,6 @@
 
 func (p *pageContentOutput) TableOfContents() template.HTML {
 	p.p.s.initInit(p.initMain, p.p)
-	if tocProvider, ok := p.convertedResult.(converter.TableOfContentsProvider); ok {
-		cfg := p.p.s.ContentSpec.Converters.GetMarkupConfig()
-		return template.HTML(tocProvider.TableOfContents().ToHTML(cfg.TableOfContents.StartLevel, cfg.TableOfContents.EndLevel, cfg.TableOfContents.Ordered))
-	}
 	return p.tableOfContents
 }
 
@@ -331,12 +372,30 @@
 
 }
 
-func (cp *pageContentOutput) renderContent(content []byte) (converter.Result, error) {
-	return cp.p.getContentConverter().Convert(
+func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
+	c := cp.p.getContentConverter()
+	return cp.renderContentWithConverter(c, content, renderTOC)
+}
+
+func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) {
+
+	r, err := c.Convert(
 		converter.RenderContext{
-			Src:       content,
-			RenderTOC: true,
+			Src:         content,
+			RenderTOC:   renderTOC,
+			RenderHooks: cp.renderHooks,
 		})
+
+	if err == nil {
+		if ids, ok := r.(identity.IdentitiesProvider); ok {
+			for _, v := range ids.GetIdentities() {
+				cp.trackDependency(v)
+			}
+		}
+	}
+
+	return r, err
+
 }
 
 func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
@@ -392,9 +451,7 @@
 // these will be shifted out when rendering a given output format.
 type pagePerOutputProviders interface {
 	targetPather
-	page.ContentProvider
 	page.PaginatorProvider
-	page.TableOfContentsProvider
 	resource.ResourceLinksProvider
 }
 
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -95,12 +95,6 @@
 Some more text
 `
 
-	simplePageWithEmbeddedScript = `---
-title: Simple
----
-<script type='text/javascript'>alert('the script tags are still there, right?');</script>
-`
-
 	simplePageWithSummaryDelimiterSameLine = `---
 title: Simple
 ---
@@ -325,6 +319,7 @@
 }
 
 func checkPageTOC(t *testing.T, page page.Page, toc string) {
+	t.Helper()
 	if page.TableOfContents() != template.HTML(toc) {
 		t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc)
 	}
--- a/hugolib/page_unwrap_test.go
+++ b/hugolib/page_unwrap_test.go
@@ -26,6 +26,7 @@
 	p := &pageState{}
 
 	c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p)
+	c.Assert(mustUnwrap(newPageForRenderHook(p)), qt.Equals, p)
 }
 
 func mustUnwrap(v interface{}) page.Page {
--- a/hugolib/pagebundler_test.go
+++ b/hugolib/pagebundler_test.go
@@ -811,6 +811,7 @@
 	writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout)
 	writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), listLayout)
 	writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), myShort)
+	writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.customo"), myShort)
 
 	writeSource(t, fs, filepath.Join(workDir, "base", "_index.md"), pageContent)
 	writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent)
--- a/hugolib/pagecollections.go
+++ b/hugolib/pagecollections.go
@@ -358,16 +358,6 @@
 	}
 }
 
-func (c *PageCollections) findPagesByShortcode(shortcode string) page.Pages {
-	var pages page.Pages
-	for _, p := range c.rawAllPages {
-		if p.HasShortcode(shortcode) {
-			pages = append(pages, p)
-		}
-	}
-	return pages
-}
-
 func (c *PageCollections) replacePage(page *pageState) {
 	// will find existing page that matches filepath and remove it
 	c.removePage(page)
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -23,8 +23,6 @@
 	"html/template"
 	"path"
 
-	"github.com/gohugoio/hugo/markup/converter"
-
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/pkg/errors"
 
@@ -198,7 +196,7 @@
 }
 
 func (s shortcode) insertPlaceholder() bool {
-	return !s.doMarkup || s.info.Config.Version == 1
+	return !s.doMarkup || s.info.ParseInfo().Config.Version == 1
 }
 
 func (s shortcode) innerString() string {
@@ -349,15 +347,10 @@
 
 		// Pre Hugo 0.55 this was the behaviour even for the outer-most
 		// shortcode.
-		if sc.doMarkup && (level > 0 || sc.info.Config.Version == 1) {
+		if sc.doMarkup && (level > 0 || sc.info.ParseInfo().Config.Version == 1) {
 			var err error
+			b, err := p.pageOutput.cp.renderContent([]byte(inner), false)
 
-			b, err := p.getContentConverter().Convert(
-				converter.RenderContext{
-					Src: []byte(inner),
-				},
-			)
-
 			if err != nil {
 				return "", false, err
 			}
@@ -494,13 +487,13 @@
 		case currItem.IsRightShortcodeDelim():
 			// we trust the template on this:
 			// if there's no inner, we're done
-			if !sc.isInline && !sc.info.IsInner {
+			if !sc.isInline && !sc.info.ParseInfo().IsInner {
 				return sc, nil
 			}
 
 		case currItem.IsShortcodeClose():
 			next := pt.Peek()
-			if !sc.isInline && !sc.info.IsInner {
+			if !sc.isInline && !sc.info.ParseInfo().IsInner {
 				if next.IsError() {
 					// return that error, more specific
 					continue
@@ -540,7 +533,7 @@
 				return nil, _errors.Errorf("template for shortcode %q not found", sc.name)
 			}
 
-			sc.info = tmpl.(tpl.TemplateInfoProvider).TemplateInfo()
+			sc.info = tmpl.(tpl.Info)
 		case currItem.IsInlineShortcodeName():
 			sc.name = currItem.ValStr()
 			sc.isInline = true
--- a/hugolib/shortcode_page.go
+++ b/hugolib/shortcode_page.go
@@ -54,3 +54,22 @@
 	p.p.enablePlaceholders()
 	return p.toc
 }
+
+// This is what is sent into the content render hooks (link, image).
+type pageForRenderHooks struct {
+	page.PageWithoutContent
+	page.TableOfContentsProvider
+	page.ContentProvider
+}
+
+func newPageForRenderHook(p *pageState) page.Page {
+	return &pageForRenderHooks{
+		PageWithoutContent:      p,
+		ContentProvider:         page.NopPage,
+		TableOfContentsProvider: page.NopPage,
+	}
+}
+
+func (p *pageForRenderHooks) page() page.Page {
+	return p.PageWithoutContent.(page.Page)
+}
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -379,8 +379,13 @@
 		if s == nil {
 			return "<nil>"
 		}
+
+		var version int
+		if s.info != nil {
+			version = s.info.ParseInfo().Config.Version
+		}
 		return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
-			s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, s.info.Config.Version, s.pos))
+			s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
 	}
 
 	regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -28,6 +28,12 @@
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/resources"
+
+	"github.com/gohugoio/hugo/identity"
+
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+
 	"github.com/gohugoio/hugo/resources/resource"
 
 	"github.com/gohugoio/hugo/markup/converter"
@@ -60,7 +66,6 @@
 	"github.com/gohugoio/hugo/navigation"
 	"github.com/gohugoio/hugo/output"
 	"github.com/gohugoio/hugo/related"
-	"github.com/gohugoio/hugo/resources"
 	"github.com/gohugoio/hugo/resources/page/pagemeta"
 	"github.com/gohugoio/hugo/source"
 	"github.com/gohugoio/hugo/tpl"
@@ -801,7 +806,6 @@
 
 type whatChanged struct {
 	source bool
-	other  bool
 	files  map[string]bool
 }
 
@@ -888,10 +892,11 @@
 // It returns whetever the content source was changed.
 // TODO(bep) clean up/rewrite this method.
 func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
-
 	events = s.filterFileEvents(events)
 	events = s.translateFileEvents(events)
 
+	changeIdentities := make(identity.Identities)
+
 	s.Log.DEBUG.Printf("Rebuild for events %q", events)
 
 	h := s.h
@@ -902,12 +907,13 @@
 		sourceChanged       = []fsnotify.Event{}
 		sourceReallyChanged = []fsnotify.Event{}
 		contentFilesChanged []string
-		tmplChanged         = []fsnotify.Event{}
-		dataChanged         = []fsnotify.Event{}
-		i18nChanged         = []fsnotify.Event{}
-		shortcodesChanged   = make(map[string]bool)
-		sourceFilesChanged  = make(map[string]bool)
 
+		tmplChanged bool
+		dataChanged bool
+		i18nChanged bool
+
+		sourceFilesChanged = make(map[string]bool)
+
 		// prevent spamming the log on changes
 		logger = helpers.NewDistinctFeedbackLogger()
 	)
@@ -919,33 +925,30 @@
 			cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
 		}
 
-		if s.isContentDirEvent(ev) {
-			logger.Println("Source changed", ev)
-			sourceChanged = append(sourceChanged, ev)
-		}
-		if s.isLayoutDirEvent(ev) {
-			logger.Println("Template changed", ev)
-			tmplChanged = append(tmplChanged, ev)
+		id, found := s.eventToIdentity(ev)
+		if found {
+			changeIdentities[id] = id
 
-			if strings.Contains(ev.Name, "shortcodes") {
-				shortcode := filepath.Base(ev.Name)
-				shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode))
-				shortcodesChanged[shortcode] = true
+			switch id.Type {
+			case files.ComponentFolderContent:
+				logger.Println("Source changed", ev)
+				sourceChanged = append(sourceChanged, ev)
+			case files.ComponentFolderLayouts:
+				logger.Println("Template changed", ev)
+				tmplChanged = true
+			case files.ComponentFolderData:
+				logger.Println("Data changed", ev)
+				dataChanged = true
+			case files.ComponentFolderI18n:
+				logger.Println("i18n changed", ev)
+				i18nChanged = true
+
 			}
 		}
-		if s.isDataDirEvent(ev) {
-			logger.Println("Data changed", ev)
-			dataChanged = append(dataChanged, ev)
-		}
-		if s.isI18nEvent(ev) {
-			logger.Println("i18n changed", ev)
-			i18nChanged = append(dataChanged, ev)
-		}
 	}
 
 	changed := &whatChanged{
-		source: len(sourceChanged) > 0 || len(shortcodesChanged) > 0,
-		other:  len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0,
+		source: len(sourceChanged) > 0,
 		files:  sourceFilesChanged,
 	}
 
@@ -960,7 +963,7 @@
 		s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
 	}
 
-	if len(tmplChanged) > 0 || len(i18nChanged) > 0 {
+	if tmplChanged || i18nChanged {
 		sites := s.h.Sites
 		first := sites[0]
 
@@ -989,7 +992,7 @@
 		}
 	}
 
-	if len(dataChanged) > 0 {
+	if dataChanged {
 		s.h.init.data.Reset()
 	}
 
@@ -1018,18 +1021,7 @@
 		sourceFilesChanged[ev.Name] = true
 	}
 
-	for shortcode := range shortcodesChanged {
-		// There are certain scenarios that, when a shortcode changes,
-		// it isn't sufficient to just rerender the already parsed shortcode.
-		// One example is if the user adds a new shortcode to the content file first,
-		// and then creates the shortcode on the file system.
-		// To handle these scenarios, we must do a full reprocessing of the
-		// pages that keeps a reference to the changed shortcode.
-		pagesWithShortcode := h.findPagesByShortcode(shortcode)
-		for _, p := range pagesWithShortcode {
-			contentFilesChanged = append(contentFilesChanged, p.File().Filename())
-		}
-	}
+	h.resetPageStateFromEvents(changeIdentities)
 
 	if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
 		var filenamesChanged []string
@@ -1218,22 +1210,16 @@
 	return nil
 }
 
-func (s *Site) isI18nEvent(e fsnotify.Event) bool {
-	return s.BaseFs.SourceFilesystems.IsI18n(e.Name)
-}
+func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) {
+	for _, fs := range s.BaseFs.SourceFilesystems.FileSystems() {
+		if p := fs.Path(e.Name); p != "" {
+			return identity.NewPathIdentity(fs.Name, p), true
+		}
+	}
 
-func (s *Site) isDataDirEvent(e fsnotify.Event) bool {
-	return s.BaseFs.SourceFilesystems.IsData(e.Name)
+	return identity.PathIdentity{}, false
 }
 
-func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool {
-	return s.BaseFs.SourceFilesystems.IsLayout(e.Name)
-}
-
-func (s *Site) isContentDirEvent(e fsnotify.Event) bool {
-	return s.BaseFs.IsContent(e.Name)
-}
-
 func (s *Site) readAndProcessContent(filenames ...string) error {
 	sourceSpec := source.NewSourceSpec(s.PathSpec, s.BaseFs.Content.Fs)
 
@@ -1560,6 +1546,26 @@
 var infoOnMissingLayout = map[string]bool{
 	// The 404 layout is very much optional in Hugo, but we do look for it.
 	"404": true,
+}
+
+type contentLinkRenderer struct {
+	templateHandler tpl.TemplateHandler
+	identity.Provider
+	templ tpl.Template
+}
+
+func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error {
+	return r.templateHandler.Execute(r.templ, w, ctx)
+}
+
+func (s *Site) lookupTemplate(layouts ...string) (tpl.Template, bool) {
+	for _, l := range layouts {
+		if templ, found := s.Tmpl.Lookup(l); found {
+			return templ, true
+		}
+	}
+
+	return nil, false
 }
 
 func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) {
--- a/hugolib/site_benchmark_new_test.go
+++ b/hugolib/site_benchmark_new_test.go
@@ -127,6 +127,36 @@
 baseURL = "https://example.com"
 
 `)
+
+			data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
+			sb.Assert(err, qt.IsNil)
+			datastr := string(data)
+			getContent := func(i int) string {
+				return fmt.Sprintf(`---
+title: "Page %d"
+---
+
+`, i) + datastr
+
+			}
+			for i := 1; i <= 100; i++ {
+				sb.WithContent(fmt.Sprintf("content/page%d.md", i), getContent(i))
+			}
+
+			return sb
+		},
+			func(s *sitesBuilder) {
+				s.Assert(s.CheckExists("public/page8/index.html"), qt.Equals, true)
+			},
+		},
+		{"Markdown with custom link handler", func(b testing.TB) *sitesBuilder {
+			sb := newTestSitesBuilder(b).WithConfigFile("toml", `
+title = "What is Markdown"
+baseURL = "https://example.com"
+
+`)
+
+			sb.WithTemplatesAdded("_default/_markup/render-link.html", `<a href="{{ .Destination | safeURL }}#custom">CUSTOM LINK</a>`)
 			data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
 			sb.Assert(err, qt.IsNil)
 			datastr := string(data)
--- a/hugolib/template_test.go
+++ b/hugolib/template_test.go
@@ -18,8 +18,12 @@
 	"path/filepath"
 	"testing"
 
+	"github.com/gohugoio/hugo/identity"
+
+	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/hugofs"
+	"github.com/gohugoio/hugo/tpl"
 
 	"github.com/spf13/viper"
 )
@@ -320,6 +324,7 @@
 Partial cached2: {{ partialCached "p1" "input2" $key1 }}
 Partial cached3: {{ partialCached "p1" "input3" $key2 }}
 `,
+
 		"partials/p1.html", `partial: {{ . }}`,
 	)
 
@@ -330,4 +335,86 @@
  Partial cached2: partial: input1
  Partial cached3: partial: input3
 `)
+}
+
+func TestTemplateDependencies(t *testing.T) {
+	b := newTestSitesBuilder(t).Running()
+
+	b.WithTemplates("index.html", `
+{{ $p := site.GetPage "p1" }}
+{{ partial "p1.html"  $p }}
+{{ partialCached "p2.html" "foo" }}
+{{ partials.Include "p3.html" "data" }}
+{{ partials.IncludeCached "p4.html" "foo" }}
+{{ $p := partial "p5" }}
+{{ partial "sub/p6.html" }}
+{{ partial "P7.html" }}
+{{ template "_default/foo.html" }}
+Partial nested: {{ partial "p10" }}
+
+`,
+		"partials/p1.html", `ps: {{ .Render "li" }}`,
+		"partials/p2.html", `p2`,
+		"partials/p3.html", `p3`,
+		"partials/p4.html", `p4`,
+		"partials/p5.html", `p5`,
+		"partials/sub/p6.html", `p6`,
+		"partials/P7.html", `p7`,
+		"partials/p8.html", `p8 {{ partial "p9.html" }}`,
+		"partials/p9.html", `p9`,
+		"partials/p10.html", `p10 {{ partial "p11.html" }}`,
+		"partials/p11.html", `p11`,
+		"_default/foo.html", `foo`,
+		"_default/li.html", `li {{ partial "p8.html" }}`,
+	)
+
+	b.WithContent("p1.md", `---
+title: P1
+---
+
+
+`)
+
+	b.Build(BuildCfg{})
+
+	s := b.H.Sites[0]
+
+	templ, found := s.lookupTemplate("index.html")
+	b.Assert(found, qt.Equals, true)
+
+	idset := make(map[identity.Identity]bool)
+	collectIdentities(idset, templ.(tpl.Info))
+	b.Assert(idset, qt.HasLen, 10)
+
+}
+
+func collectIdentities(set map[identity.Identity]bool, provider identity.Provider) {
+	if ids, ok := provider.(identity.IdentitiesProvider); ok {
+		for _, id := range ids.GetIdentities() {
+			collectIdentities(set, id)
+		}
+	} else {
+		set[provider.GetIdentity()] = true
+	}
+}
+
+func printRecursiveIdentities(level int, id identity.Provider) {
+	if level == 0 {
+		fmt.Println(id.GetIdentity(), "===>")
+	}
+	if ids, ok := id.(identity.IdentitiesProvider); ok {
+		level++
+		for _, id := range ids.GetIdentities() {
+			printRecursiveIdentities(level, id)
+		}
+	} else {
+		ident(level)
+		fmt.Println("ID", id)
+	}
+}
+
+func ident(n int) {
+	for i := 0; i < n; i++ {
+		fmt.Print("  ")
+	}
 }
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -389,8 +389,9 @@
 	var changedFiles []string
 	for i := 0; i < len(filenameContent); i += 2 {
 		filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
-		changedFiles = append(changedFiles, filename)
-		writeSource(s.T, s.Fs, s.absFilename(filename), content)
+		absFilename := s.absFilename(filename)
+		changedFiles = append(changedFiles, absFilename)
+		writeSource(s.T, s.Fs, absFilename, content)
 
 	}
 	s.changedFiles = changedFiles
@@ -961,10 +962,6 @@
 
 func isCI() bool {
 	return os.Getenv("CI") != ""
-}
-
-func isGo111() bool {
-	return strings.Contains(runtime.Version(), "1.11")
 }
 
 // See https://github.com/golang/go/issues/19280
--- /dev/null
+++ b/identity/identity.go
@@ -1,0 +1,131 @@
+package identity
+
+import (
+	"path/filepath"
+	"strings"
+	"sync"
+)
+
+// NewIdentityManager creates a new Manager starting at id.
+func NewManager(id Provider) Manager {
+	return &identityManager{
+		Provider: id,
+		ids:      Identities{id.GetIdentity(): id},
+	}
+}
+
+// NewPathIdentity creates a new Identity with the two identifiers
+// type and path.
+func NewPathIdentity(typ, pat string) PathIdentity {
+	pat = strings.ToLower(strings.TrimPrefix(filepath.ToSlash(pat), "/"))
+	return PathIdentity{Type: typ, Path: pat}
+}
+
+// Identities stores identity providers.
+type Identities map[Identity]Provider
+
+func (ids Identities) search(id Identity) Provider {
+	if v, found := ids[id]; found {
+		return v
+	}
+	for _, v := range ids {
+		switch t := v.(type) {
+		case IdentitiesProvider:
+			if nested := t.GetIdentities().search(id); nested != nil {
+				return nested
+			}
+		}
+	}
+	return nil
+}
+
+// IdentitiesProvider provides all Identities.
+type IdentitiesProvider interface {
+	GetIdentities() Identities
+}
+
+// Identity represents an thing that can provide an identify. This can be
+// any Go type, but the Identity returned by GetIdentify must be hashable.
+type Identity interface {
+	Provider
+	Name() string
+}
+
+// Manager manages identities, and is itself a Provider of Identity.
+type Manager interface {
+	IdentitiesProvider
+	Provider
+	Add(ids ...Provider)
+	Search(id Identity) Provider
+	Reset()
+}
+
+// A PathIdentity is a common identity identified by a type and a path, e.g. "layouts" and "_default/single.html".
+type PathIdentity struct {
+	Type string
+	Path string
+}
+
+// GetIdentity returns itself.
+func (id PathIdentity) GetIdentity() Identity {
+	return id
+}
+
+// Name returns the Path.
+func (id PathIdentity) Name() string {
+	return id.Path
+}
+
+// A KeyValueIdentity a general purpose identity.
+type KeyValueIdentity struct {
+	Key   string
+	Value string
+}
+
+// GetIdentity returns itself.
+func (id KeyValueIdentity) GetIdentity() Identity {
+	return id
+}
+
+// Name returns the Key.
+func (id KeyValueIdentity) Name() string {
+	return id.Key
+}
+
+// Provider provides the hashable Identity.
+type Provider interface {
+	GetIdentity() Identity
+}
+
+type identityManager struct {
+	sync.Mutex
+	Provider
+	ids Identities
+}
+
+func (im *identityManager) Add(ids ...Provider) {
+	im.Lock()
+	for _, id := range ids {
+		im.ids[id.GetIdentity()] = id
+	}
+	im.Unlock()
+}
+
+func (im *identityManager) Reset() {
+	im.Lock()
+	id := im.GetIdentity()
+	im.ids = Identities{id.GetIdentity(): id}
+	im.Unlock()
+}
+
+func (im *identityManager) GetIdentities() Identities {
+	im.Lock()
+	defer im.Unlock()
+	return im.ids
+}
+
+func (im *identityManager) Search(id Identity) Provider {
+	im.Lock()
+	defer im.Unlock()
+	return im.ids.search(id.GetIdentity())
+}
--- /dev/null
+++ b/identity/identity_test.go
@@ -1,0 +1,42 @@
+// Copyright 2019 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 identity
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestIdentityManager(t *testing.T) {
+	c := qt.New(t)
+
+	id1 := testIdentity{name: "id1"}
+	im := NewManager(id1)
+
+	c.Assert(im.Search(id1).GetIdentity(), qt.Equals, id1)
+	c.Assert(im.Search(testIdentity{name: "notfound"}), qt.Equals, nil)
+}
+
+type testIdentity struct {
+	name string
+}
+
+func (id testIdentity) GetIdentity() Identity {
+	return id
+}
+
+func (id testIdentity) Name() string {
+	return id.name
+}
--- a/markup/asciidoc/convert.go
+++ b/markup/asciidoc/convert.go
@@ -18,6 +18,7 @@
 import (
 	"os/exec"
 
+	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup/internal"
 
 	"github.com/gohugoio/hugo/markup/converter"
@@ -45,6 +46,10 @@
 
 func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
 	return converter.Bytes(a.getAsciidocContent(ctx.Src, a.ctx)), nil
+}
+
+func (c *asciidocConverter) Supports(feature identity.Identity) bool {
+	return false
 }
 
 // getAsciidocContent calls asciidoctor or asciidoc as an external helper
--- a/markup/blackfriday/convert.go
+++ b/markup/blackfriday/convert.go
@@ -15,6 +15,7 @@
 package blackfriday
 
 import (
+	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/russross/blackfriday"
@@ -70,6 +71,10 @@
 	r := c.getHTMLRenderer(ctx.RenderTOC)
 
 	return converter.Bytes(blackfriday.Markdown(ctx.Src, r, c.extensions)), nil
+}
+
+func (c *blackfridayConverter) Supports(feature identity.Identity) bool {
+	return false
 }
 
 func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Renderer {
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -16,6 +16,8 @@
 import (
 	"github.com/gohugoio/hugo/common/loggers"
 	"github.com/gohugoio/hugo/config"
+	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/gohugoio/hugo/markup/markup_config"
 	"github.com/gohugoio/hugo/markup/tableofcontents"
 	"github.com/spf13/afero"
@@ -67,6 +69,7 @@
 // another format, e.g. Markdown to HTML.
 type Converter interface {
 	Convert(ctx RenderContext) (Result, error)
+	Supports(feature identity.Identity) bool
 }
 
 // Result represents the minimum returned from Convert.
@@ -94,6 +97,7 @@
 
 // DocumentContext holds contextual information about the document to convert.
 type DocumentContext struct {
+	Document        interface{} // May be nil. Usually a page.Page
 	DocumentID      string
 	DocumentName    string
 	ConfigOverrides map[string]interface{}
@@ -101,6 +105,11 @@
 
 // RenderContext holds contextual information about the content to render.
 type RenderContext struct {
-	Src       []byte
-	RenderTOC bool
+	Src         []byte
+	RenderTOC   bool
+	RenderHooks *hooks.Render
 }
+
+var (
+	FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks")
+)
--- /dev/null
+++ b/markup/converter/hooks/hooks.go
@@ -1,0 +1,57 @@
+// Copyright 2019 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 hooks
+
+import (
+	"io"
+
+	"github.com/gohugoio/hugo/identity"
+)
+
+type LinkContext interface {
+	Page() interface{}
+	Destination() string
+	Title() string
+	Text() string
+}
+
+type Render struct {
+	LinkRenderer  LinkRenderer
+	ImageRenderer LinkRenderer
+}
+
+func (r *Render) Eq(other interface{}) bool {
+	ro, ok := other.(*Render)
+	if !ok {
+		return false
+	}
+	if r == nil || ro == nil {
+		return r == nil
+	}
+
+	if r.ImageRenderer.GetIdentity() != ro.ImageRenderer.GetIdentity() {
+		return false
+	}
+
+	if r.LinkRenderer.GetIdentity() != ro.LinkRenderer.GetIdentity() {
+		return false
+	}
+
+	return true
+}
+
+type LinkRenderer interface {
+	Render(w io.Writer, ctx LinkContext) error
+	identity.Provider
+}
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -15,11 +15,14 @@
 package goldmark
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
 	"path/filepath"
 	"runtime/debug"
 
+	"github.com/gohugoio/hugo/identity"
+
 	"github.com/pkg/errors"
 
 	"github.com/spf13/afero"
@@ -26,10 +29,8 @@
 
 	"github.com/gohugoio/hugo/hugofs"
 
-	"github.com/alecthomas/chroma/styles"
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/gohugoio/hugo/markup/highlight"
-	"github.com/gohugoio/hugo/markup/markup_config"
 	"github.com/gohugoio/hugo/markup/tableofcontents"
 	"github.com/yuin/goldmark"
 	hl "github.com/yuin/goldmark-highlighting"
@@ -48,7 +49,7 @@
 }
 
 func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
-	md := newMarkdown(cfg.MarkupConfig)
+	md := newMarkdown(cfg)
 	return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) {
 		return &goldmarkConverter{
 			ctx: ctx,
@@ -64,11 +65,13 @@
 	cfg converter.ProviderConfig
 }
 
-func newMarkdown(mcfg markup_config.Config) goldmark.Markdown {
-	cfg := mcfg.Goldmark
+func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
+	mcfg := pcfg.MarkupConfig
+	cfg := pcfg.MarkupConfig.Goldmark
 
 	var (
 		extensions = []goldmark.Extender{
+			newLinks(),
 			newTocExtension(),
 		}
 		rendererOptions []renderer.Option
@@ -143,9 +146,12 @@
 
 }
 
+var _ identity.IdentitiesProvider = (*converterResult)(nil)
+
 type converterResult struct {
 	converter.Result
 	toc tableofcontents.Root
+	ids identity.Identities
 }
 
 func (c converterResult) TableOfContents() tableofcontents.Root {
@@ -152,6 +158,41 @@
 	return c.toc
 }
 
+func (c converterResult) GetIdentities() identity.Identities {
+	return c.ids
+}
+
+type renderContext struct {
+	util.BufWriter
+	renderContextData
+}
+
+type renderContextData interface {
+	RenderContext() converter.RenderContext
+	DocumentContext() converter.DocumentContext
+	AddIdentity(id identity.Identity)
+}
+
+type renderContextDataHolder struct {
+	rctx converter.RenderContext
+	dctx converter.DocumentContext
+	ids  identity.Manager
+}
+
+func (ctx *renderContextDataHolder) RenderContext() converter.RenderContext {
+	return ctx.rctx
+}
+
+func (ctx *renderContextDataHolder) DocumentContext() converter.DocumentContext {
+	return ctx.dctx
+}
+
+func (ctx *renderContextDataHolder) AddIdentity(id identity.Identity) {
+	ctx.ids.Add(id)
+}
+
+var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"}
+
 func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -166,9 +207,7 @@
 
 	buf := &bytes.Buffer{}
 	result = buf
-	pctx := parser.NewContext()
-	pctx.Set(tocEnableKey, ctx.RenderTOC)
-
+	pctx := newParserContext(ctx)
 	reader := text.NewReader(ctx.Src)
 
 	doc := c.md.Parser().Parse(
@@ -176,27 +215,58 @@
 		parser.WithContext(pctx),
 	)
 
-	if err := c.md.Renderer().Render(buf, ctx.Src, doc); err != nil {
+	rcx := &renderContextDataHolder{
+		rctx: ctx,
+		dctx: c.ctx,
+		ids:  identity.NewManager(converterIdentity),
+	}
+
+	w := renderContext{
+		BufWriter:         bufio.NewWriter(buf),
+		renderContextData: rcx,
+	}
+
+	if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil {
 		return nil, err
 	}
 
-	if toc, ok := pctx.Get(tocResultKey).(tableofcontents.Root); ok {
-		return converterResult{
-			Result: buf,
-			toc:    toc,
-		}, nil
+	return converterResult{
+		Result: buf,
+		ids:    rcx.ids.GetIdentities(),
+		toc:    pctx.TableOfContents(),
+	}, nil
+
+}
+
+var featureSet = map[identity.Identity]bool{
+	converter.FeatureRenderHooks: true,
+}
+
+func (c *goldmarkConverter) Supports(feature identity.Identity) bool {
+	return featureSet[feature.GetIdentity()]
+}
+
+func newParserContext(rctx converter.RenderContext) *parserContext {
+	ctx := parser.NewContext()
+	ctx.Set(tocEnableKey, rctx.RenderTOC)
+	return &parserContext{
+		Context: ctx,
 	}
+}
 
-	return buf, nil
+type parserContext struct {
+	parser.Context
 }
 
-func newHighlighting(cfg highlight.Config) goldmark.Extender {
-	style := styles.Get(cfg.Style)
-	if style == nil {
-		style = styles.Fallback
+func (p *parserContext) TableOfContents() tableofcontents.Root {
+	if v := p.Get(tocResultKey); v != nil {
+		return v.(tableofcontents.Root)
 	}
+	return tableofcontents.Root{}
+}
 
-	e := hl.NewHighlighting(
+func newHighlighting(cfg highlight.Config) goldmark.Extender {
+	return hl.NewHighlighting(
 		hl.WithStyle(cfg.Style),
 		hl.WithGuessLanguage(cfg.GuessSyntax),
 		hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
@@ -230,6 +300,4 @@
 
 		}),
 	)
-
-	return e
 }
--- a/markup/goldmark/convert_test.go
+++ b/markup/goldmark/convert_test.go
@@ -38,6 +38,9 @@
 https://github.com/gohugoio/hugo/issues/6528
 [Live Demo here!](https://docuapi.netlify.com/)
 
+[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
+
+
 ## Code Fences
 
 §§§bash
@@ -98,6 +101,7 @@
 
 	mconf := markup_config.Default
 	mconf.Highlight.NoClasses = false
+	mconf.Goldmark.Renderer.Unsafe = true
 
 	p, err := Provider.New(
 		converter.ProviderConfig{
@@ -106,15 +110,15 @@
 		},
 	)
 	c.Assert(err, qt.IsNil)
-	conv, err := p.New(converter.DocumentContext{})
+	conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"})
 	c.Assert(err, qt.IsNil)
-	b, err := conv.Convert(converter.RenderContext{Src: []byte(content)})
+	b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content)})
 	c.Assert(err, qt.IsNil)
 
 	got := string(b.Bytes())
 
 	// Links
-	c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
+	//	c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
 
 	// Header IDs
 	c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got))
@@ -136,6 +140,11 @@
 	c.Assert(got, qt.Contains, `footnote.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>`)
 	c.Assert(got, qt.Contains, `<section class="footnotes" role="doc-endnotes">`)
 	c.Assert(got, qt.Contains, `<dt>date</dt>`)
+
+	toc, ok := b.(converter.TableOfContentsProvider)
+	c.Assert(ok, qt.Equals, true)
+	tocHTML := toc.TableOfContents().ToHTML(1, 2, false)
+	c.Assert(tocHTML, qt.Contains, "TableOfContents")
 
 }
 
--- /dev/null
+++ b/markup/goldmark/render_link.go
@@ -1,0 +1,208 @@
+// Copyright 2019 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 goldmark
+
+import (
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+
+	"github.com/yuin/goldmark"
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/renderer"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/util"
+)
+
+var _ renderer.SetOptioner = (*linkRenderer)(nil)
+
+func newLinkRenderer() renderer.NodeRenderer {
+	r := &linkRenderer{
+		Config: html.Config{
+			Writer: html.DefaultWriter,
+		},
+	}
+	return r
+}
+
+func newLinks() goldmark.Extender {
+	return &links{}
+}
+
+type linkContext struct {
+	page        interface{}
+	destination string
+	title       string
+	text        string
+}
+
+func (ctx linkContext) Destination() string {
+	return ctx.destination
+}
+
+func (ctx linkContext) Resolved() bool {
+	return false
+}
+
+func (ctx linkContext) Page() interface{} {
+	return ctx.page
+}
+
+func (ctx linkContext) Text() string {
+	return ctx.text
+}
+
+func (ctx linkContext) Title() string {
+	return ctx.title
+}
+
+type linkRenderer struct {
+	html.Config
+}
+
+func (r *linkRenderer) SetOption(name renderer.OptionName, value interface{}) {
+	r.Config.SetOption(name, value)
+}
+
+// RegisterFuncs implements NodeRenderer.RegisterFuncs.
+func (r *linkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+	reg.Register(ast.KindLink, r.renderLink)
+	reg.Register(ast.KindImage, r.renderImage)
+}
+
+// Fall back to the default Goldmark render funcs. Method below borrowed from:
+// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
+func (r *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	if !entering {
+		return ast.WalkContinue, nil
+	}
+	n := node.(*ast.Image)
+	_, _ = w.WriteString("<img src=\"")
+	if r.Unsafe || !html.IsDangerousURL(n.Destination) {
+		_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
+	}
+	_, _ = w.WriteString(`" alt="`)
+	_, _ = w.Write(n.Text(source))
+	_ = w.WriteByte('"')
+	if n.Title != nil {
+		_, _ = w.WriteString(` title="`)
+		r.Writer.Write(w, n.Title)
+		_ = w.WriteByte('"')
+	}
+	if r.XHTML {
+		_, _ = w.WriteString(" />")
+	} else {
+		_, _ = w.WriteString(">")
+	}
+	return ast.WalkSkipChildren, nil
+}
+
+// Fall back to the default Goldmark render funcs. Method below borrowed from:
+// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
+func (r *linkRenderer) renderDefaultLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Link)
+	if entering {
+		_, _ = w.WriteString("<a href=\"")
+		if r.Unsafe || !html.IsDangerousURL(n.Destination) {
+			_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
+		}
+		_ = w.WriteByte('"')
+		if n.Title != nil {
+			_, _ = w.WriteString(` title="`)
+			r.Writer.Write(w, n.Title)
+			_ = w.WriteByte('"')
+		}
+		_ = w.WriteByte('>')
+	} else {
+		_, _ = w.WriteString("</a>")
+	}
+	return ast.WalkContinue, nil
+}
+
+func (r *linkRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Image)
+	var h *hooks.Render
+
+	ctx, ok := w.(renderContextData)
+	if ok {
+		h = ctx.RenderContext().RenderHooks
+		ok = h != nil && h.ImageRenderer != nil
+	}
+
+	if !ok {
+		return r.renderDefaultImage(w, source, node, entering)
+	}
+
+	if !entering {
+		return ast.WalkContinue, nil
+	}
+
+	err := h.ImageRenderer.Render(
+		w,
+		linkContext{
+			page:        ctx.DocumentContext().Document,
+			destination: string(n.Destination),
+			title:       string(n.Title),
+			text:        string(n.Text(source)),
+		},
+	)
+
+	ctx.AddIdentity(h.ImageRenderer.GetIdentity())
+
+	return ast.WalkSkipChildren, err
+
+}
+
+func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Link)
+	var h *hooks.Render
+
+	ctx, ok := w.(renderContextData)
+	if ok {
+		h = ctx.RenderContext().RenderHooks
+		ok = h != nil && h.LinkRenderer != nil
+	}
+
+	if !ok {
+		return r.renderDefaultLink(w, source, node, entering)
+	}
+
+	if !entering {
+		return ast.WalkContinue, nil
+	}
+
+	err := h.LinkRenderer.Render(
+		w,
+		linkContext{
+			page:        ctx.DocumentContext().Document,
+			destination: string(n.Destination),
+			title:       string(n.Title),
+			text:        string(n.Text(source)),
+		},
+	)
+
+	ctx.AddIdentity(h.LinkRenderer.GetIdentity())
+
+	// Do not render the inner text.
+	return ast.WalkSkipChildren, err
+
+}
+
+type links struct {
+}
+
+// Extend implements goldmark.Extender.
+func (e *links) Extend(m goldmark.Markdown) {
+	m.Renderer().AddOptions(renderer.WithNodeRenderers(
+		util.Prioritized(newLinkRenderer(), 100),
+	))
+}
--- a/markup/mmark/convert.go
+++ b/markup/mmark/convert.go
@@ -15,6 +15,7 @@
 package mmark
 
 import (
+	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/miekg/mmark"
@@ -63,6 +64,10 @@
 func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
 	r := getHTMLRenderer(c.ctx, c.b, c.cfg)
 	return mmark.Parse(ctx.Src, r, c.extensions), nil
+}
+
+func (c *mmarkConverter) Supports(feature identity.Identity) bool {
+	return false
 }
 
 func getHTMLRenderer(
--- a/markup/org/convert.go
+++ b/markup/org/convert.go
@@ -17,6 +17,8 @@
 import (
 	"bytes"
 
+	"github.com/gohugoio/hugo/identity"
+
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/niklasfasching/go-org/org"
 	"github.com/spf13/afero"
@@ -65,4 +67,8 @@
 		return converter.Bytes(ctx.Src), nil
 	}
 	return converter.Bytes([]byte(html)), nil
+}
+
+func (c *orgConverter) Supports(feature identity.Identity) bool {
+	return false
 }
--- a/markup/pandoc/convert.go
+++ b/markup/pandoc/convert.go
@@ -17,6 +17,7 @@
 import (
 	"os/exec"
 
+	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup/internal"
 
 	"github.com/gohugoio/hugo/markup/converter"
@@ -45,6 +46,10 @@
 
 func (c *pandocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
 	return converter.Bytes(c.getPandocContent(ctx.Src, c.ctx)), nil
+}
+
+func (c *pandocConverter) Supports(feature identity.Identity) bool {
+	return false
 }
 
 // getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
--- a/markup/rst/convert.go
+++ b/markup/rst/convert.go
@@ -19,6 +19,7 @@
 	"os/exec"
 	"runtime"
 
+	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup/internal"
 
 	"github.com/gohugoio/hugo/markup/converter"
@@ -46,6 +47,10 @@
 
 func (c *rstConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
 	return converter.Bytes(c.getRstContent(ctx.Src, c.ctx)), nil
+}
+
+func (c *rstConverter) Supports(feature identity.Identity) bool {
+	return false
 }
 
 // getRstContent calls the Python script rst2html as an external helper
--- a/output/layout.go
+++ b/output/layout.go
@@ -37,8 +37,14 @@
 	Layout  string
 	// LayoutOverride indicates what we should only look for the above layout.
 	LayoutOverride bool
+
+	RenderingHook bool
 }
 
+func (d LayoutDescriptor) isList() bool {
+	return !d.RenderingHook && d.Kind != "page"
+}
+
 // LayoutHandler calculates the layout template to use to render a given output type.
 type LayoutHandler struct {
 	mu    sync.RWMutex
@@ -89,7 +95,7 @@
 
 func (l *layoutBuilder) addLayoutVariations(vars ...string) {
 	for _, layoutVar := range vars {
-		if l.d.LayoutOverride && layoutVar != l.d.Layout {
+		if !l.d.RenderingHook && l.d.LayoutOverride && layoutVar != l.d.Layout {
 			continue
 		}
 		l.layoutVariations = append(l.layoutVariations, layoutVar)
@@ -99,6 +105,9 @@
 func (l *layoutBuilder) addTypeVariations(vars ...string) {
 	for _, typeVar := range vars {
 		if !reservedSections[typeVar] {
+			if l.d.RenderingHook {
+				typeVar = typeVar + renderingHookRoot
+			}
 			l.typeVariations = append(l.typeVariations, typeVar)
 		}
 	}
@@ -115,18 +124,23 @@
 	l.addTypeVariations(l.d.Kind)
 }
 
+const renderingHookRoot = "/_markup"
+
 func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
 
 	b := &layoutBuilder{d: d, f: f}
 
-	if d.Layout != "" {
-		b.addLayoutVariations(d.Layout)
+	if d.RenderingHook {
+		b.addLayoutVariations(d.Kind)
+	} else {
+		if d.Layout != "" {
+			b.addLayoutVariations(d.Layout)
+		}
+		if d.Type != "" {
+			b.addTypeVariations(d.Type)
+		}
 	}
 
-	if d.Type != "" {
-		b.addTypeVariations(d.Type)
-	}
-
 	switch d.Kind {
 	case "page":
 		b.addLayoutVariations("single")
@@ -159,7 +173,7 @@
 	}
 
 	isRSS := f.Name == RSSFormat.Name
-	if isRSS {
+	if !d.RenderingHook && isRSS {
 		// The historic and common rss.xml case
 		b.addLayoutVariations("")
 	}
@@ -167,7 +181,7 @@
 	// All have _default in their lookup path
 	b.addTypeVariations("_default")
 
-	if d.Kind != "page" {
+	if d.isList() {
 		// Add the common list type
 		b.addLayoutVariations("list")
 	}
@@ -174,7 +188,7 @@
 
 	layouts := b.resolveVariations()
 
-	if isRSS {
+	if !d.RenderingHook && isRSS {
 		layouts = append(layouts, "_internal/_default/rss.xml")
 	}
 
--- a/output/layout_test.go
+++ b/output/layout_test.go
@@ -111,6 +111,9 @@
 			[]string{"section/shortcodes.amp.html"}, 12},
 		{"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType,
 			[]string{"section/partials.amp.html"}, 12},
+		// We may add type support ... later.
+		{"Content hook", LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog"}, "", ampType,
+			[]string{"_default/_markup/render-link.amp.html", "_default/_markup/render-link.html"}, 2},
 	} {
 		c.Run(this.name, func(c *qt.C) {
 			l := NewLayoutHandler()
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -201,9 +201,10 @@
 	Weight() int
 }
 
-// PageRenderProvider provides a way for a Page to render itself.
+// PageRenderProvider provides a way for a Page to render content.
 type PageRenderProvider interface {
-	Render(layout ...string) template.HTML
+	Render(layout ...string) (template.HTML, error)
+	RenderString(args ...interface{}) (template.HTML, error)
 }
 
 // PageWithoutContent is the Page without any of the content methods.
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -371,8 +371,12 @@
 	return "", nil
 }
 
-func (p *nopPage) Render(layout ...string) template.HTML {
-	return ""
+func (p *nopPage) Render(layout ...string) (template.HTML, error) {
+	return "", nil
+}
+
+func (p *nopPage) RenderString(args ...interface{}) (template.HTML, error) {
+	return "", nil
 }
 
 func (p *nopPage) ResourceType() string {
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -446,7 +446,11 @@
 	return "", nil
 }
 
-func (p *testPage) Render(layout ...string) template.HTML {
+func (p *testPage) Render(layout ...string) (template.HTML, error) {
+	panic("not implemented")
+}
+
+func (p *testPage) RenderString(args ...interface{}) (template.HTML, error) {
 	panic("not implemented")
 }
 
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -59,6 +59,7 @@
 		"type state struct", "type stateOld struct",
 		"func (s *state) evalFunction", "func (s *state) evalFunctionOld",
 		"func (s *state) evalField(", "func (s *state) evalFieldOld(",
+		"func (s *state) evalCall(", "func (s *state) evalCallOld(",
 	)
 
 	htmlTemplateReplacers = strings.NewReplacer(
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -658,7 +658,7 @@
 // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
 // it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
 // as the function itself.
-func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
+func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
 	if args != nil {
 		args = args[1:] // Zeroth arg is function name/node; not passed to function.
 	}
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -34,8 +34,9 @@
 
 // ExecHelper allows some custom eval hooks.
 type ExecHelper interface {
-	GetFunc(name string) (reflect.Value, bool)
-	GetMapValue(receiver, key reflect.Value) (reflect.Value, bool)
+	GetFunc(tmpl Preparer, name string) (reflect.Value, bool)
+	GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value)
+	GetMapValue(tmpl Preparer, receiver, key reflect.Value) (reflect.Value, bool)
 }
 
 // Executer executes a given template.
@@ -64,6 +65,7 @@
 
 	state := &state{
 		helper: t.helper,
+		prep:   p,
 		tmpl:   tmpl,
 		wr:     wr,
 		vars:   []variable{{"$", value}},
@@ -75,7 +77,6 @@
 
 // Prepare returns a template ready for execution.
 func (t *Template) Prepare() (*Template, error) {
-
 	return t, nil
 }
 
@@ -95,6 +96,7 @@
 // can execute in parallel.
 type state struct {
 	tmpl   *Template
+	prep   Preparer   // Added for Hugo.
 	helper ExecHelper // Added for Hugo.
 	wr     io.Writer
 	node   parse.Node // current node, for errors
@@ -110,7 +112,7 @@
 	var ok bool
 	if s.helper != nil {
 		// Added for Hugo.
-		function, ok = s.helper.GetFunc(name)
+		function, ok = s.helper.GetFunc(s.prep, name)
 	}
 
 	if !ok {
@@ -148,9 +150,23 @@
 	if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
 		ptr = ptr.Addr()
 	}
-	if method := ptr.MethodByName(fieldName); method.IsValid() {
+	// Added for Hugo.
+	var first reflect.Value
+	var method reflect.Value
+	if s.helper != nil {
+		method, first = s.helper.GetMethod(s.prep, ptr, fieldName)
+	} else {
+		method = ptr.MethodByName(fieldName)
+	}
+
+	if method.IsValid() {
+		if first != zero {
+			return s.evalCall(dot, method, node, fieldName, args, final, first)
+		}
+
 		return s.evalCall(dot, method, node, fieldName, args, final)
 	}
+
 	hasArgs := len(args) > 1 || final != missingVal
 	// It's not a method; must be a field of a struct or an element of a map.
 	switch receiver.Kind() {
@@ -177,7 +193,7 @@
 			var result reflect.Value
 			if s.helper != nil {
 				// Added for Hugo.
-				result, _ = s.helper.GetMapValue(receiver, nameVal)
+				result, _ = s.helper.GetMapValue(s.prep, receiver, nameVal)
 			} else {
 				result = receiver.MapIndex(nameVal)
 			}
@@ -208,4 +224,80 @@
 	}
 	s.errorf("can't evaluate field %s in type %s", fieldName, typ)
 	panic("not reached")
+}
+
+// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
+// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
+// as the function itself.
+func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value {
+	if args != nil {
+		args = args[1:] // Zeroth arg is function name/node; not passed to function.
+	}
+	typ := fun.Type()
+	numFirst := len(first)
+	numIn := len(args) + numFirst // // Added for Hugo
+	if final != missingVal {
+		numIn++
+	}
+	numFixed := len(args) + len(first)
+	if typ.IsVariadic() {
+		numFixed = typ.NumIn() - 1 // last arg is the variadic one.
+		if numIn < numFixed {
+			s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args))
+		}
+	} else if numIn != typ.NumIn() {
+		s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
+	}
+	if !goodFunc(typ) {
+		// TODO: This could still be a confusing error; maybe goodFunc should provide info.
+		s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
+	}
+	// Build the arg list.
+	argv := make([]reflect.Value, numIn)
+	// Args must be evaluated. Fixed args first.
+	i := len(first)
+	for ; i < numFixed && i < len(args)+numFirst; i++ {
+		argv[i] = s.evalArg(dot, typ.In(i), args[i-numFirst])
+	}
+	// Now the ... args.
+	if typ.IsVariadic() {
+		argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
+		for ; i < len(args)+numFirst; i++ {
+			argv[i] = s.evalArg(dot, argType, args[i-numFirst])
+		}
+
+	}
+	// Add final value if necessary.
+	if final != missingVal {
+		t := typ.In(typ.NumIn() - 1)
+		if typ.IsVariadic() {
+			if numIn-1 < numFixed {
+				// The added final argument corresponds to a fixed parameter of the function.
+				// Validate against the type of the actual parameter.
+				t = typ.In(numIn - 1)
+			} else {
+				// The added final argument corresponds to the variadic part.
+				// Validate against the type of the elements of the variadic slice.
+				t = t.Elem()
+			}
+		}
+		argv[i] = s.validateType(final, t)
+	}
+
+	// Added for Hugo
+	for i := 0; i < len(first); i++ {
+		argv[i] = s.validateType(first[i], typ.In(i))
+	}
+
+	v, err := safeCall(fun, argv)
+	// If we have an error that is not nil, stop execution and return that
+	// error to the caller.
+	if err != nil {
+		s.at(node)
+		s.errorf("error calling %s: %v", name, err)
+	}
+	if v.Type() == reflectValueType {
+		v = v.Interface().(reflect.Value)
+	}
+	return v
 }
--- a/tpl/internal/go_templates/texttemplate/hugo_template_test.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template_test.go
@@ -27,10 +27,18 @@
 	M map[string]string
 }
 
+func (t TestStruct) Hello1(arg string) string {
+	return arg
+}
+
+func (t TestStruct) Hello2(arg1, arg2 string) string {
+	return arg1 + " " + arg2
+}
+
 type execHelper struct {
 }
 
-func (e *execHelper) GetFunc(name string) (reflect.Value, bool) {
+func (e *execHelper) GetFunc(tmpl Preparer, name string) (reflect.Value, bool) {
 	if name == "print" {
 		return zero, false
 	}
@@ -39,11 +47,19 @@
 	}), true
 }
 
-func (e *execHelper) GetMapValue(m, key reflect.Value) (reflect.Value, bool) {
+func (e *execHelper) GetMapValue(tmpl Preparer, m, key reflect.Value) (reflect.Value, bool) {
 	key = reflect.ValueOf(strings.ToLower(key.String()))
 	return m.MapIndex(key), true
 }
 
+func (e *execHelper) GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
+	if name != "Hello1" {
+		return zero, zero
+	}
+	m := receiver.MethodByName("Hello2")
+	return m, reflect.ValueOf("v2")
+}
+
 func TestTemplateExecutor(t *testing.T) {
 	c := qt.New(t)
 
@@ -51,6 +67,7 @@
 {{ print "foo" }}
 {{ printf "hugo" }}
 Map: {{ .M.A }}
+Method: {{ .Hello1 "v1" }}
 
 `)
 
@@ -67,5 +84,6 @@
 	c.Assert(got, qt.Contains, "foo")
 	c.Assert(got, qt.Contains, "hello hugo")
 	c.Assert(got, qt.Contains, "Map: av")
+	c.Assert(got, qt.Contains, "Method: v2 v1")
 
 }
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -116,9 +116,9 @@
 		return "", fmt.Errorf("partial %q not found", name)
 	}
 
-	var info tpl.Info
-	if ip, ok := templ.(tpl.TemplateInfoProvider); ok {
-		info = ip.TemplateInfo()
+	var info tpl.ParseInfo
+	if ip, ok := templ.(tpl.Info); ok {
+		info = ip.ParseInfo()
 	}
 
 	var w io.Writer
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -24,8 +24,6 @@
 	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 )
 
-var _ TemplateInfoProvider = (*TemplateInfo)(nil)
-
 // TemplateManager manages the collection of templates.
 type TemplateManager interface {
 	TemplateHandler
@@ -34,7 +32,6 @@
 	AddLateTemplate(name, tpl string) error
 	LoadTemplates(prefix string) error
 
-	MarkReady() error
 	RebuildClone()
 }
 
@@ -80,11 +77,6 @@
 	Prepare() (*texttemplate.Template, error)
 }
 
-// TemplateInfoProvider provides some contextual information about a template.
-type TemplateInfoProvider interface {
-	TemplateInfo() Info
-}
-
 // TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
 type TemplateParser interface {
 	Parse(name, tpl string) (Template, error)
@@ -101,12 +93,33 @@
 	Debug()
 }
 
-// TemplateInfo wraps a Template with some additional information.
-type TemplateInfo struct {
+// templateInfo wraps a Template with some additional information.
+type templateInfo struct {
 	Template
-	Info Info
+	Info
 }
 
+// templateInfo wraps a Template with some additional information.
+type templateInfoManager struct {
+	Template
+	InfoManager
+}
+
+// WithInfo wraps the info in a template.
+func WithInfo(templ Template, info Info) Template {
+	if manager, ok := info.(InfoManager); ok {
+		return &templateInfoManager{
+			Template:    templ,
+			InfoManager: manager,
+		}
+	}
+
+	return &templateInfo{
+		Template: templ,
+		Info:     info,
+	}
+}
+
 var baseOfRe = regexp.MustCompile("template: (.*?):")
 
 func extractBaseOf(err string) string {
@@ -115,10 +128,6 @@
 		return m[1]
 	}
 	return ""
-}
-
-func (t *TemplateInfo) TemplateInfo() Info {
-	return t.Info
 }
 
 // TemplateFuncGetter allows to find a template func by name.
--- a/tpl/template_info.go
+++ b/tpl/template_info.go
@@ -13,12 +13,44 @@
 
 package tpl
 
+import (
+	"github.com/gohugoio/hugo/identity"
+)
+
 // Increments on breaking changes.
 const TemplateVersion = 2
 
-// Info holds some info extracted from a parsed template.
-type Info struct {
+type Info interface {
+	ParseInfo() ParseInfo
 
+	// Identifies this template and its dependencies.
+	identity.Provider
+}
+
+type InfoManager interface {
+	ParseInfo() ParseInfo
+
+	// Identifies and manages this template and its dependencies.
+	identity.Manager
+}
+
+type defaultInfo struct {
+	identity.Manager
+	parseInfo ParseInfo
+}
+
+func NewInfo(id identity.Manager, parseInfo ParseInfo) Info {
+	return &defaultInfo{
+		Manager:   id,
+		parseInfo: parseInfo,
+	}
+}
+
+func (info *defaultInfo) ParseInfo() ParseInfo {
+	return info.parseInfo
+}
+
+type ParseInfo struct {
 	// Set for shortcode templates with any {{ .Inner }}
 	IsInner bool
 
@@ -26,17 +58,25 @@
 	HasReturn bool
 
 	// Config extracted from template.
-	Config Config
+	Config ParseConfig
 }
 
-func (info Info) IsZero() bool {
+func (info ParseInfo) IsZero() bool {
 	return info.Config.Version == 0
 }
 
-type Config struct {
+// Info holds some info extracted from a parsed template.
+type Info1 struct {
+}
+
+type ParseConfig struct {
 	Version int
 }
 
-var DefaultConfig = Config{
+var DefaultParseConfig = ParseConfig{
 	Version: TemplateVersion,
+}
+
+var DefaultParseInfo = ParseInfo{
+	Config: DefaultParseConfig,
 }
--- a/tpl/tplimpl/shortcodes.go
+++ b/tpl/tplimpl/shortcodes.go
@@ -83,10 +83,12 @@
 func (s *shortcodeTemplates) compareVariants(a, b []string) int {
 
 	weight := 0
+	k := len(a)
 	for i, av := range a {
 		bv := b[i]
 		if av == bv {
-			weight++
+			// Add more weight to the left side (language...).
+			weight = weight + k - i
 		} else {
 			weight--
 		}
--- a/tpl/tplimpl/shortcodes_test.go
+++ b/tpl/tplimpl/shortcodes_test.go
@@ -53,10 +53,10 @@
 			name2    string
 			expected int
 		}{
-			{"Same suffix", "figure.html", "figure.html", 3},
-			{"Same suffix and output format", "figure.html.html", "figure.html.html", 3},
-			{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 3},
-			{"No suffix", "figure", "figure", 3},
+			{"Same suffix", "figure.html", "figure.html", 6},
+			{"Same suffix and output format", "figure.html.html", "figure.html.html", 6},
+			{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
+			{"No suffix", "figure", "figure", 6},
 			{"Different output format", "figure.amp.html", "figure.html.html", -1},
 			{"One with output format, one without", "figure.amp.html", "figure.html", -1},
 		}
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -20,6 +20,10 @@
 	"regexp"
 	"time"
 
+	"github.com/gohugoio/hugo/hugofs/files"
+
+	"github.com/gohugoio/hugo/identity"
+
 	"github.com/gohugoio/hugo/common/herrors"
 
 	"strings"
@@ -27,7 +31,6 @@
 	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
 
 	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/tpl/tplimpl/embedded"
@@ -81,6 +84,7 @@
 	common := &templatesCommon{
 		nameBaseTemplateName: make(map[string]string),
 		transformNotFound:    make(map[string]bool),
+		identityNotFound:     make(map[string][]identity.Manager),
 	}
 
 	htmlT := &htmlTemplates{
@@ -100,13 +104,16 @@
 		Deps:      deps,
 		layoutsFs: deps.BaseFs.Layouts.Fs,
 		templateHandlerCommon: &templateHandlerCommon{
-			shortcodes:   make(map[string]*shortcodeTemplates),
-			templateInfo: make(map[string]tpl.Info),
-			html:         htmlT,
-			text:         textT,
+			shortcodes:       make(map[string]*shortcodeTemplates),
+			templateInfo:     make(map[string]tpl.Info),
+			templateInfoTree: make(map[string]*templateInfoTree),
+			html:             htmlT,
+			text:             textT,
 		},
 	}
 
+	textT.textTemplate.templates = textT
+	textT.standalone.templates = textT
 	common.handler = h
 
 	return h
@@ -152,8 +159,8 @@
 	return t.addTemplateIn(t.t, name, tpl)
 }
 
-func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) (*templateContext, error) {
-	templ, err := tt.New(name).Parse(tpl)
+func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, templstr string) (*templateContext, error) {
+	templ, err := tt.New(name).Parse(templstr)
 	if err != nil {
 		return nil, err
 	}
@@ -160,19 +167,18 @@
 
 	typ := resolveTemplateType(name)
 
-	c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
+	c, err := t.handler.applyTemplateTransformersToHMLTTemplate(typ, templ)
 	if err != nil {
 		return nil, err
 	}
 
-	for k := range c.notFound {
+	for k := range c.templateNotFound {
 		t.transformNotFound[k] = true
+		t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
 	}
 
-	if typ == templateShortcode {
-		t.handler.addShortcodeVariant(name, c.Info, templ)
-	} else {
-		t.handler.templateInfo[name] = c.Info
+	for k := range c.identityNotFound {
+		t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
 	}
 
 	return c, nil
@@ -208,7 +214,7 @@
 	// * https://github.com/golang/go/issues/16101
 	// * https://github.com/gohugoio/hugo/issues/2549
 	overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
-	if _, err := applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil {
+	if _, err := t.handler.applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil {
 		return err
 	}
 
@@ -253,6 +259,8 @@
 // It implements the templateLoader and tpl.TemplateHandler interfaces.
 // There is one templateHandler created per Site.
 type templateHandler struct {
+	ready bool
+
 	executor texttemplate.Executer
 	funcs    map[string]reflect.Value
 
@@ -324,6 +332,7 @@
 // Lookup tries to find a template with the given name in both template
 // collections: First HTML, then the plain text template collection.
 func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
+
 	if strings.HasPrefix(name, textTmplNamePrefix) {
 		// The caller has explicitly asked for a text template, so only look
 		// in the text template collection.
@@ -345,6 +354,9 @@
 // This currently only applies to shortcodes and what we get here is the
 // shortcode name.
 func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
+	if !t.ready {
+		panic("handler not ready")
+	}
 	name = templateBaseName(templateShortcode, name)
 	s, found := t.shortcodes[name]
 	if !found {
@@ -358,18 +370,17 @@
 
 	more := len(s.variants) > 1
 
-	return &tpl.TemplateInfo{
-		Template: sv.templ,
-		Info:     sv.info,
-	}, true, more
+	return tpl.WithInfo(sv.templ, sv.info), true, more
 
 }
 
-// MarkReady marks the templates as "ready for execution". No changes allowed
+// markReady marks the templates as "ready for execution". No changes allowed
 // after this is set.
-// TODO(bep) if this proves to be resource heavy, we could detect
-// earlier if we really need this, or make it lazy.
-func (t *templateHandler) MarkReady() error {
+func (t *templateHandler) markReady() error {
+	defer func() {
+		t.ready = true
+	}()
+
 	if err := t.postTransform(); err != nil {
 		return err
 	}
@@ -483,6 +494,7 @@
 }
 
 func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
+
 	base := templateBaseName(templateShortcode, name)
 
 	shortcodename, variants := templateNameAndVariants(base)
@@ -561,18 +573,9 @@
 }
 
 func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) {
-	if adapter, ok := templ.(*tpl.TemplateInfo); ok {
-		if adapter.Info.IsZero() {
-			if info, found := t.templateInfo[templ.Name()]; found {
-				adapter.Info = info
-			}
-		}
-	} else if templ != nil {
+	if templ != nil {
 		if info, found := t.templateInfo[templ.Name()]; found {
-			return &tpl.TemplateInfo{
-				Template: templ,
-				Info:     info,
-			}, true
+			return tpl.WithInfo(templ, info), true
 		}
 	}
 
@@ -586,7 +589,11 @@
 }
 
 func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
+	if !t.ready {
+		panic("invalid state")
+	}
 	c := &templateHandler{
+		ready:     true,
 		Deps:      d,
 		layoutsFs: d.BaseFs.Layouts.Fs,
 	}
@@ -703,36 +710,69 @@
 
 }
 
-func (t *templateHandler) postTransform() error {
-	if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 {
-		return nil
+func (t *templateHandler) getOrCreateTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
+	info, found := t.templateInfo[name]
+	if found {
+		return info.(identity.Manager), info.ParseInfo()
 	}
+	return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
+}
 
-	defer func() {
-		t.text.transformNotFound = make(map[string]bool)
-		t.html.transformNotFound = make(map[string]bool)
-	}()
+func (t *templateHandler) createTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
+	_, found := t.templateInfo[name]
+	if found {
+		panic("already created: " + name)
+	}
 
+	return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
+}
+
+func (t *templateHandler) postTransform() error {
+	for k, v := range t.templateInfoTree {
+		if v.id != nil {
+			info := tpl.NewInfo(
+				v.id,
+				v.info,
+			)
+			t.templateInfo[k] = info
+
+			if v.typ == templateShortcode {
+				t.addShortcodeVariant(k, info, v.templ)
+			}
+		}
+	}
+
 	for _, s := range []struct {
-		lookup            func(name string) *parse.Tree
+		lookup            func(name string) *templateInfoTree
 		transformNotFound map[string]bool
+		identityNotFound  map[string][]identity.Manager
 	}{
 		// html templates
-		{func(name string) *parse.Tree {
+		{func(name string) *templateInfoTree {
 			templ := t.html.lookup(name)
 			if templ == nil {
 				return nil
 			}
-			return templ.Tree
-		}, t.html.transformNotFound},
+			id, info := t.getOrCreateTemplateInfo(name)
+			return &templateInfoTree{
+				id:   id,
+				info: info,
+				tree: templ.Tree,
+			}
+		}, t.html.transformNotFound, t.html.identityNotFound},
 		// text templates
-		{func(name string) *parse.Tree {
+		{func(name string) *templateInfoTree {
 			templT := t.text.lookup(name)
 			if templT == nil {
 				return nil
 			}
-			return templT.Tree
-		}, t.text.transformNotFound},
+			id, info := t.getOrCreateTemplateInfo(name)
+			return &templateInfoTree{
+				id:   id,
+				info: info,
+				tree: templT.Tree,
+			}
+		}, t.text.transformNotFound, t.text.identityNotFound},
 	} {
 		for name := range s.transformNotFound {
 			templ := s.lookup(name)
@@ -743,6 +783,15 @@
 				}
 			}
 		}
+
+		for k, v := range s.identityNotFound {
+			tmpl := s.lookup(k)
+			if tmpl != nil {
+				for _, im := range v {
+					im.Add(tmpl.id)
+				}
+			}
+		}
 	}
 
 	return nil
@@ -758,7 +807,6 @@
 		tt,
 		new(nopLookupVariant),
 	}
-
 }
 
 type templateHandlerCommon struct {
@@ -771,6 +819,9 @@
 	// shortcodeTemplates type.
 	templateInfo map[string]tpl.Info
 
+	// Used to track templates during the AST transformations.
+	templateInfoTree map[string]*templateInfoTree
+
 	// text holds all the pure text templates.
 	text *textTemplates
 	html *htmlTemplates
@@ -795,9 +846,12 @@
 	// Used to get proper filenames in errors
 	nameBaseTemplateName map[string]string
 
-	// Holds names of the templates not found during the first AST transformation
+	// Holds names of the template definitions not found during the first AST transformation
 	// pass.
 	transformNotFound map[string]bool
+
+	// Holds identities of templates not found during first pass.
+	identityNotFound map[string][]identity.Manager
 }
 
 func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
@@ -806,8 +860,9 @@
 }
 
 type textTemplate struct {
-	mu sync.RWMutex
-	t  *texttemplate.Template
+	mu        sync.RWMutex
+	t         *texttemplate.Template
+	templates *textTemplates
 }
 
 func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
@@ -831,7 +886,7 @@
 		return nil, err
 	}
 
-	if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
+	if _, err := t.templates.handler.applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
 		return nil, err
 	}
 	return templ, nil
@@ -868,9 +923,9 @@
 	return t.addTemplateIn(t.t, name, tpl)
 }
 
-func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) (*templateContext, error) {
+func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tplstr string) (*templateContext, error) {
 	name = strings.TrimPrefix(name, textTmplNamePrefix)
-	templ, err := t.parseIn(tt, name, tpl)
+	templ, err := t.parseIn(tt, name, tplstr)
 	if err != nil {
 		return nil, err
 	}
@@ -877,21 +932,15 @@
 
 	typ := resolveTemplateType(name)
 
-	c, err := applyTemplateTransformersToTextTemplate(typ, templ)
+	c, err := t.handler.applyTemplateTransformersToTextTemplate(typ, templ)
 	if err != nil {
 		return nil, err
 	}
 
-	for k := range c.notFound {
+	for k := range c.templateNotFound {
 		t.transformNotFound[k] = true
 	}
 
-	if typ == templateShortcode {
-		t.handler.addShortcodeVariant(name, c.Info, templ)
-	} else {
-		t.handler.templateInfo[name] = c.Info
-	}
-
 	return c, nil
 }
 
@@ -924,7 +973,7 @@
 	}
 
 	overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
-	if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
+	if _, err := t.handler.applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
 		return err
 	}
 	t.overlays[name] = overlayTpl
--- a/tpl/tplimpl/templateProvider.go
+++ b/tpl/tplimpl/templateProvider.go
@@ -44,16 +44,13 @@
 
 	}
 
-	return newTmpl.MarkReady()
+	return newTmpl.markReady()
 
 }
 
 // Clone clones.
 func (*TemplateProvider) Clone(d *deps.Deps) error {
-
 	t := d.Tmpl.(*templateHandler)
-	clone := t.clone(d)
-
-	return clone.MarkReady()
-
+	t.clone(d)
+	return nil
 }
--- a/tpl/tplimpl/template_ast_transformers.go
+++ b/tpl/tplimpl/template_ast_transformers.go
@@ -14,8 +14,12 @@
 package tplimpl
 
 import (
-	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+	"regexp"
+	"strings"
 
+	"github.com/gohugoio/hugo/identity"
+
+	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
 	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 
@@ -34,9 +38,10 @@
 )
 
 type templateContext struct {
-	visited  map[string]bool
-	notFound map[string]bool
-	lookupFn func(name string) *parse.Tree
+	visited          map[string]bool
+	templateNotFound map[string]bool
+	identityNotFound map[string]bool
+	lookupFn         func(name string) *templateInfoTree
 
 	// The last error encountered.
 	err error
@@ -47,13 +52,14 @@
 	configChecked bool
 
 	// Contains some info about the template
-	tpl.Info
+	parseInfo *tpl.ParseInfo
+	id        identity.Manager
 
 	// Store away the return node in partials.
 	returnNode *parse.CommandNode
 }
 
-func (c templateContext) getIfNotVisited(name string) *parse.Tree {
+func (c templateContext) getIfNotVisited(name string) *templateInfoTree {
 	if c.visited[name] {
 		return nil
 	}
@@ -63,59 +69,95 @@
 		// This may be a inline template defined outside of this file
 		// and not yet parsed. Unusual, but it happens.
 		// Store the name to try again later.
-		c.notFound[name] = true
+		c.templateNotFound[name] = true
 	}
 
 	return templ
 }
 
-func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
+func newTemplateContext(
+	id identity.Manager,
+	info *tpl.ParseInfo,
+	lookupFn func(name string) *templateInfoTree) *templateContext {
+
 	return &templateContext{
-		Info:     tpl.Info{Config: tpl.DefaultConfig},
-		lookupFn: lookupFn,
-		visited:  make(map[string]bool),
-		notFound: make(map[string]bool)}
+		id:               id,
+		parseInfo:        info,
+		lookupFn:         lookupFn,
+		visited:          make(map[string]bool),
+		templateNotFound: make(map[string]bool),
+		identityNotFound: make(map[string]bool),
+	}
 }
 
-func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree {
-	return func(nn string) *parse.Tree {
-		tt := templ.Lookup(nn)
-		if tt != nil {
-			return tt.Tree
-		}
-		return nil
+func createGetTemplateInfoTreeFor(getID func(name string) *templateInfoTree) func(nn string) *templateInfoTree {
+	return func(nn string) *templateInfoTree {
+		return getID(nn)
 	}
 }
 
-func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
-	return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ))
+func (t *templateHandler) applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
+	id, info := t.createTemplateInfo(templ.Name())
+	ti := &templateInfoTree{
+		tree:  templ.Tree,
+		templ: templ,
+		typ:   typ,
+		id:    id,
+		info:  info,
+	}
+	t.templateInfoTree[templ.Name()] = ti
+	getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
+		return t.templateInfoTree[name]
+	})
+
+	return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
 }
 
-func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
-	return applyTemplateTransformers(typ, templ.Tree,
-		func(nn string) *parse.Tree {
-			tt := templ.Lookup(nn)
-			if tt != nil {
-				return tt.Tree
-			}
-			return nil
-		})
+func (t *templateHandler) applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
+	id, info := t.createTemplateInfo(templ.Name())
+	ti := &templateInfoTree{
+		tree:  templ.Tree,
+		templ: templ,
+		typ:   typ,
+		id:    id,
+		info:  info,
+	}
+
+	t.templateInfoTree[templ.Name()] = ti
+	getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
+		return t.templateInfoTree[name]
+	})
+
+	return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
+
 }
 
-func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) {
+type templateInfoTree struct {
+	info  tpl.ParseInfo
+	typ   templateType
+	id    identity.Manager
+	templ tpl.Template
+	tree  *parse.Tree
+}
+
+func applyTemplateTransformers(
+	typ templateType,
+	templ *templateInfoTree,
+	lookupFn func(name string) *templateInfoTree) (*templateContext, error) {
+
 	if templ == nil {
 		return nil, errors.New("expected template, but none provided")
 	}
 
-	c := newTemplateContext(lookupFn)
+	c := newTemplateContext(templ.id, &templ.info, lookupFn)
 	c.typ = typ
 
-	_, err := c.applyTransformations(templ.Root)
+	_, err := c.applyTransformations(templ.tree.Root)
 
 	if err == nil && c.returnNode != nil {
 		// This is a partial with a return statement.
-		c.Info.HasReturn = true
-		templ.Root = c.wrapInPartialReturnWrapper(templ.Root)
+		c.parseInfo.HasReturn = true
+		templ.tree.Root = c.wrapInPartialReturnWrapper(templ.tree.Root)
 	}
 
 	return c, err
@@ -125,7 +167,9 @@
 	partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
 )
 
-var partialReturnWrapper *parse.ListNode
+var (
+	partialReturnWrapper *parse.ListNode
+)
 
 func init() {
 	templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
@@ -133,6 +177,7 @@
 		panic(err)
 	}
 	partialReturnWrapper = templ.Tree.Root
+
 }
 
 func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
@@ -156,6 +201,7 @@
 // getif works slightly different than the Go built-in in that it also
 // considers any IsZero methods on the values (as in time.Time).
 // See https://github.com/gohugoio/hugo/issues/5738
+// TODO(bep) get rid of this.
 func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
 	if len(p.Cmds) == 0 {
 		return
@@ -176,9 +222,9 @@
 }
 
 // applyTransformations do 3 things:
-// 1) Make all .Params.CamelCase and similar into lowercase.
-// 2) Wraps every with and if pipe in getif
-// 3) Collects some information about the template content.
+// 1) Wraps every with and if pipe in getif
+// 2) Parses partial return statement.
+// 3) Tracks template (partial) dependencies and some other info.
 func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
 	switch x := n.(type) {
 	case *parse.ListNode:
@@ -198,7 +244,7 @@
 	case *parse.TemplateNode:
 		subTempl := c.getIfNotVisited(x.Name)
 		if subTempl != nil {
-			c.applyTransformationsToNodes(subTempl.Root)
+			c.applyTransformationsToNodes(subTempl.tree.Root)
 		}
 	case *parse.PipeNode:
 		c.collectConfig(x)
@@ -210,6 +256,7 @@
 		}
 
 	case *parse.CommandNode:
+		c.collectPartialInfo(x)
 		c.collectInner(x)
 		keep := c.collectReturnNode(x)
 
@@ -277,11 +324,10 @@
 			c.err = errors.Wrap(err, errMsg)
 			return
 		}
-		if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil {
+		if err := mapstructure.WeakDecode(m, &c.parseInfo.Config); err != nil {
 			c.err = errors.Wrap(err, errMsg)
 		}
 	}
-
 }
 
 // collectInner determines if the given CommandNode represents a
@@ -290,7 +336,7 @@
 	if c.typ != templateShortcode {
 		return
 	}
-	if c.Info.IsInner || len(n.Args) == 0 {
+	if c.parseInfo.IsInner || len(n.Args) == 0 {
 		return
 	}
 
@@ -304,11 +350,43 @@
 		}
 
 		if c.hasIdent(idents, "Inner") {
-			c.Info.IsInner = true
+			c.parseInfo.IsInner = true
 			break
 		}
 	}
 
+}
+
+var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`)
+
+func (c *templateContext) collectPartialInfo(x *parse.CommandNode) {
+	if len(x.Args) < 2 {
+		return
+	}
+
+	first := x.Args[0]
+	var id string
+	switch v := first.(type) {
+	case *parse.IdentifierNode:
+		id = v.Ident
+	case *parse.ChainNode:
+		id = v.String()
+	}
+
+	if partialRe.MatchString(id) {
+		partialName := strings.Trim(x.Args[1].String(), "\"")
+		if !strings.Contains(partialName, ".") {
+			partialName += ".html"
+		}
+		partialName = "partials/" + partialName
+		info := c.lookupFn(partialName)
+		if info != nil {
+			c.id.Add(info.id)
+		} else {
+			// Delay for later
+			c.identityNotFound[partialName] = true
+		}
+	}
 }
 
 func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
--- a/tpl/tplimpl/template_ast_transformers_test.go
+++ b/tpl/tplimpl/template_ast_transformers_test.go
@@ -15,14 +15,17 @@
 import (
 	"strings"
 
-	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+	"github.com/gohugoio/hugo/hugofs/files"
 
 	"testing"
 	"time"
 
-	"github.com/gohugoio/hugo/tpl"
+	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 
 	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/tpl"
 )
 
 // Issue #2927
@@ -33,7 +36,7 @@
 {{ define "menu-nodes" }}
 {{ template "menu-node" }}
 {{ end }}
-{{ define "menu-node" }}
+{{ define "menu-nßode" }}
 {{ template "menu-node" }}
 {{ end }}
 {{ template "menu-nodes" }}
@@ -41,12 +44,25 @@
 
 	templ, err := template.New("foo").Parse(recursive)
 	c.Assert(err, qt.IsNil)
+	parseInfo := tpl.DefaultParseInfo
 
-	ctx := newTemplateContext(createParseTreeLookup(templ))
+	ctx := newTemplateContext(
+		newTemplateInfo("test").(identity.Manager),
+		&parseInfo,
+		createGetTemplateInfoTree(templ.Tree),
+	)
 	ctx.applyTransformations(templ.Tree.Root)
 
 }
 
+func createGetTemplateInfoTree(tree *parse.Tree) func(name string) *templateInfoTree {
+	return func(name string) *templateInfoTree {
+		return &templateInfoTree{
+			tree: tree,
+		}
+	}
+}
+
 type I interface {
 	Method0()
 }
@@ -80,13 +96,10 @@
 {{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }}
 {{ template "mytemplate" . }}
 {{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }}
-
 {{ template "other-file-template" . }}
-
 {{ define "mytemplate" }}
 {{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }}
 {{ end }}
-
 `
 
 		// https://github.com/gohugoio/hugo/issues/5865
@@ -97,7 +110,7 @@
 	)
 
 	d := newD(c)
-	h := d.Tmpl.(tpl.TemplateManager)
+	h := d.Tmpl.(*templateHandler)
 
 	// HTML templates
 	c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil)
@@ -107,15 +120,13 @@
 	c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil)
 	c.Assert(h.AddTemplate("_text/myothertexttemplate.txt", templ2), qt.IsNil)
 
-	c.Assert(h.MarkReady(), qt.IsNil)
+	c.Assert(h.markReady(), qt.IsNil)
 
 	for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} {
+		var sb strings.Builder
 		tt, _ := d.Tmpl.Lookup(name)
-		sb := &strings.Builder{}
-
-		err := d.Tmpl.Execute(tt, sb, ctx)
+		err := h.Execute(tt, &sb, ctx)
 		c.Assert(err, qt.IsNil)
-
 		result := sb.String()
 
 		c.Assert(result, qt.Contains, ".True: TRUE")
@@ -138,14 +149,10 @@
 	tests := []struct {
 		name      string
 		tplString string
-		expected  tpl.Info
+		expected  tpl.ParseInfo
 	}{
-		{"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}},
-		{"Basic config map", "{{ $_hugo_config := `" + configStr + "`  }}", tpl.Info{
-			Config: tpl.Config{
-				Version: 42,
-			},
-		}},
+		{"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
+		{"Basic config map", "{{ $_hugo_config := `" + configStr + "`  }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
 	}
 
 	echo := func(in interface{}) interface{} {
@@ -162,12 +169,13 @@
 
 			templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
 			c.Assert(err, qt.IsNil)
+			parseInfo := tpl.DefaultParseInfo
 
-			ctx := newTemplateContext(createParseTreeLookup(templ))
+			ctx := newTemplateContext(
+				newTemplateInfo("test").(identity.Manager), &parseInfo, createGetTemplateInfoTree(templ.Tree))
 			ctx.typ = templateShortcode
 			ctx.applyTransformations(templ.Tree.Root)
-
-			c.Assert(ctx.Info, qt.Equals, test.expected)
+			c.Assert(ctx.parseInfo, qt.DeepEquals, &test.expected)
 		})
 	}
 
@@ -205,7 +213,10 @@
 			templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
 			c.Assert(err, qt.IsNil)
 
-			_, err = applyTemplateTransformers(templatePartial, templ.Tree, createParseTreeLookup(templ))
+			_, err = applyTemplateTransformers(
+				templatePartial,
+				&templateInfoTree{tree: templ.Tree, info: tpl.DefaultParseInfo},
+				createGetTemplateInfoTree(templ.Tree))
 
 			// Just check that it doesn't fail in this test. We have functional tests
 			// in hugoblib.
@@ -214,4 +225,11 @@
 		})
 	}
 
+}
+
+func newTemplateInfo(name string) tpl.Info {
+	return tpl.NewInfo(
+		identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)),
+		tpl.DefaultParseInfo,
+	)
 }
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -19,6 +19,8 @@
 	"reflect"
 	"strings"
 
+	"github.com/gohugoio/hugo/tpl"
+
 	"github.com/gohugoio/hugo/common/maps"
 
 	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
@@ -62,7 +64,7 @@
 	funcs map[string]reflect.Value
 }
 
-func (t *templateExecHelper) GetFunc(name string) (reflect.Value, bool) {
+func (t *templateExecHelper) GetFunc(tmpl texttemplate.Preparer, name string) (reflect.Value, bool) {
 	if fn, found := t.funcs[name]; found {
 		return fn, true
 	}
@@ -69,7 +71,7 @@
 	return zero, false
 }
 
-func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.Value, bool) {
+func (t *templateExecHelper) GetMapValue(tmpl texttemplate.Preparer, receiver, key reflect.Value) (reflect.Value, bool) {
 	if params, ok := receiver.Interface().(maps.Params); ok {
 		// Case insensitive.
 		keystr := strings.ToLower(key.String())
@@ -85,6 +87,22 @@
 	return v, v.IsValid()
 }
 
+func (t *templateExecHelper) GetMethod(tmpl texttemplate.Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
+	// This is a hot path and receiver.MethodByName really shows up in the benchmarks.
+	// Page.Render is the only method with a WithTemplateInfo as of now, so let's just
+	// check that for now.
+	// TODO(bep) find a more flexible, but still fast, way.
+	if name == "Render" {
+		if info, ok := tmpl.(tpl.Info); ok {
+			if m := receiver.MethodByName(name + "WithTemplateInfo"); m.IsValid() {
+				return m, reflect.ValueOf(info)
+			}
+		}
+	}
+
+	return receiver.MethodByName(name), zero
+}
+
 func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
 	funcs := createFuncMap(d)
 	funcsv := make(map[string]reflect.Value)
@@ -120,9 +138,7 @@
 				}
 				funcMap[alias] = mm.Method
 			}
-
 		}
-
 	}
 
 	if d.OverloadedTemplateFuncs != nil {
--- a/tpl/tplimpl/template_info_test.go
+++ b/tpl/tplimpl/template_info_test.go
@@ -24,18 +24,19 @@
 func TestTemplateInfoShortcode(t *testing.T) {
 	c := qt.New(t)
 	d := newD(c)
-	h := d.Tmpl.(tpl.TemplateManager)
+	h := d.Tmpl.(*templateHandler)
 
 	c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `
 {{ .Inner }}
 `), qt.IsNil)
 
+	c.Assert(h.markReady(), qt.IsNil)
 	tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{})
 
 	c.Assert(found, qt.Equals, true)
-	tti, ok := tt.(tpl.TemplateInfoProvider)
+	tti, ok := tt.(tpl.Info)
 	c.Assert(ok, qt.Equals, true)
-	c.Assert(tti.TemplateInfo().IsInner, qt.Equals, true)
+	c.Assert(tti.ParseInfo().IsInner, qt.Equals, true)
 
 }