Repository: gohugoio/hugo (language: go)

Issue:
All values after the first dot in content/layout/etc. base filenames as identifiers (e.g. language, ext)
I'm doing some major spring cleaning by re-designing/implementing all template handling in Hugo (with goals of making it more powerful but also much easier to understand). We have lots of integration tests, which in this case is both a curse and a blessing.

One of my now failing tests is basically this, in a site with 1 language (en):

```
content/doc1.en.md
content/doc1.fr.md
content/doc1.sv.md
```

By just looking at the above, it is obvious that these 3 are translations of the same document, but since we only have English defined, we get 3 pages (all with language set to `en`) with the following paths:

```
/doc1
/doc1.fr
/doc.sv
```

Which is obviously wrong. In my new setup, the last two will be thrown out as duplicates. 

Note that there may be some examples in the wild somehow broken by this (e.g. `/dot.net.md` vs `/dot.foo.md`), but I'm not sure I'm prepared to go the extra mile to complicate this further. **Those who think this is a considerable problem needs to raise an convincing argument in this issue.**.
Layout lookup should consider root and current section's type if set
You can currently set

```bash
type: pretty
layout: two-colums
```

In every content page, list and single pages. Which makes it flexible. But if you take the base layouts into account, it is hard to create a couple of reusable templates that can be applied to new sections on demand (without creating duplicates):

If I set this in `news/_index.md`:

```bash
type: "3-column"
```

Then I want that type to be in the template lookup order (before the most general ones) for both that section and other pages in that section.

This means that if I create `news/article.md` I don't have to do something extra to get that "3-column" layout.

This behaviour matches what we do with the `section` today, but this will make it more flexible.

So we add

* The current section's type if set
* The root section's type if set

In that order.



Discussion / hints:
I know I've seen this somewhere in the wild, but I have no idea how prevalent it is:

```
hugo new releases/v1.234.5.md
```

The alternative would be something like...

```text
hugo new releases/v1-234-5.md
```

...then modify the `slug` and `title`.

We're using a content adapter on the docs site that sets the path to news/vN.N.N, but that is easily changed if needed.
>hugo new releases/v1.234.5.md

In the above example, I would have internally classified 234 and 5 as "unrecognized identifiers", so it would be possible to add a `warnidf` or something about it.


I need to reconsider/refine this one, so I'll reopen to remind me.
I have enabled this for page + section right now. Will have to think about this a little bit. Testing it and it wasn't that powerful as I hoped -- but that is because of the baseof resolution. I will create another issue for that. 

Note to self: For taxonomies, make sure this does not resolve to list.html in the type override folder.
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.


Fix patch:
diff --git a/commands/server.go b/commands/server.go
index d3a72ec9aa4..11b43a80fa4 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -23,6 +23,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"maps"
 	"net"
 	"net/http"
 	_ "net/http/pprof"
@@ -48,6 +49,7 @@ import (
 	"github.com/fsnotify/fsnotify"
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/hugo"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/common/urls"
@@ -57,7 +59,6 @@ import (
 	"github.com/gohugoio/hugo/hugolib"
 	"github.com/gohugoio/hugo/hugolib/filesystems"
 	"github.com/gohugoio/hugo/livereload"
-	"github.com/gohugoio/hugo/tpl"
 	"github.com/gohugoio/hugo/transform"
 	"github.com/gohugoio/hugo/transform/livereloadinject"
 	"github.com/spf13/afero"
@@ -65,7 +66,6 @@ import (
 	"github.com/spf13/fsync"
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/sync/semaphore"
-	"maps"
 )
 
 var (
@@ -897,16 +897,16 @@ func (c *serverCommand) serve() error {
 	// To allow the en user to change the error template while the server is running, we use
 	// the freshest template we can provide.
 	var (
-		errTempl     tpl.Template
-		templHandler tpl.TemplateHandler
+		errTempl     *tplimpl.TemplInfo
+		templHandler *tplimpl.TemplateStore
 	)
-	getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
+	getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
 		if h == nil {
 			return errTempl, templHandler
 		}
-		templHandler := h.Tmpl()
-		errTempl, found := templHandler.Lookup("_server/error.html")
-		if !found {
+		templHandler := h.GetTemplateStore()
+		errTempl := templHandler.LookupByPath("/_server/error.html")
+		if errTempl == nil {
 			panic("template server/error.html not found")
 		}
 		return errTempl, templHandler
diff --git a/common/constants/constants.go b/common/constants/constants.go
index b2cb4fb7395..3f48f3e4abb 100644
--- a/common/constants/constants.go
+++ b/common/constants/constants.go
@@ -23,6 +23,7 @@ const (
 	WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
 	WarnRenderShortcodesInHTML     = "warning-rendershortcodes-in-html"
 	WarnGoldmarkRawHTML            = "warning-goldmark-raw-html"
+	WarnPartialSuperfluousPrefix   = "warning-partial-superfluous-prefix"
 )
 
 // Field/method names with special meaning.
diff --git a/common/hstrings/strings.go b/common/hstrings/strings.go
index 2df3486fc85..1de38678f3f 100644
--- a/common/hstrings/strings.go
+++ b/common/hstrings/strings.go
@@ -16,11 +16,11 @@ package hstrings
 import (
 	"fmt"
 	"regexp"
+	"slices"
 	"strings"
 	"sync"
 
 	"github.com/gohugoio/hugo/compare"
-	"slices"
 )
 
 var _ compare.Eqer = StringEqualFold("")
@@ -128,7 +128,7 @@ func ToString(v any) (string, bool) {
 	return "", false
 }
 
-type Tuple struct {
-	First  string
-	Second string
-}
+type (
+	Strings2 [2]string
+	Strings3 [3]string
+)
diff --git a/common/maps/cache.go b/common/maps/cache.go
index 72c07e7cad9..7304a7a6f4e 100644
--- a/common/maps/cache.go
+++ b/common/maps/cache.go
@@ -69,6 +69,14 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
 	return v, nil
 }
 
+// Contains returns whether the given key exists in the cache.
+func (c *Cache[K, T]) Contains(key K) bool {
+	c.RLock()
+	_, found := c.m[key]
+	c.RUnlock()
+	return found
+}
+
 // InitAndGet initializes the cache if not already done and returns the value for the given key.
 // The init state will be reset on Reset or Drain.
 func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
@@ -108,6 +116,17 @@ func (c *Cache[K, T]) Set(key K, value T) {
 	c.Unlock()
 }
 
+// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
+func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
+	c.RLock()
+	if _, found := c.get(key); !found {
+		c.RUnlock()
+		c.Set(key, value)
+	} else {
+		c.RUnlock()
+	}
+}
+
 func (c *Cache[K, T]) set(key K, value T) {
 	c.m[key] = value
 }
diff --git a/common/maps/ordered.go b/common/maps/ordered.go
index 08dd7791968..0da9d239d10 100644
--- a/common/maps/ordered.go
+++ b/common/maps/ordered.go
@@ -14,8 +14,9 @@
 package maps
 
 import (
-	"github.com/gohugoio/hugo/common/hashing"
 	"slices"
+
+	"github.com/gohugoio/hugo/common/hashing"
 )
 
 // Ordered is a map that can be iterated in the order of insertion.
@@ -57,6 +58,15 @@ func (m *Ordered[K, T]) Get(key K) (T, bool) {
 	return value, found
 }
 
+// Has returns whether the given key exists in the map.
+func (m *Ordered[K, T]) Has(key K) bool {
+	if m == nil {
+		return false
+	}
+	_, found := m.values[key]
+	return found
+}
+
 // Delete deletes the value for the given key.
 func (m *Ordered[K, T]) Delete(key K) {
 	if m == nil {
diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go
index 94329fe7a67..4b3feaa1492 100644
--- a/common/paths/pathparser.go
+++ b/common/paths/pathparser.go
@@ -23,6 +23,11 @@ import (
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/hugofs/files"
 	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/resources/kinds"
+)
+
+const (
+	identifierBaseof = "baseof"
 )
 
 // PathParser parses a path into a Path.
@@ -33,6 +38,10 @@ type PathParser struct {
 	// Reports whether the given language is disabled.
 	IsLangDisabled func(string) bool
 
+	// IsOutputFormat reports whether the given name is a valid output format.
+	// The second argument is optional.
+	IsOutputFormat func(name, ext string) bool
+
 	// Reports whether the given ext is a content file.
 	IsContentExt func(string) bool
 }
@@ -83,13 +92,10 @@ func (pp *PathParser) Parse(c, s string) *Path {
 }
 
 func (pp *PathParser) newPath(component string) *Path {
-	return &Path{
-		component:             component,
-		posContainerLow:       -1,
-		posContainerHigh:      -1,
-		posSectionHigh:        -1,
-		posIdentifierLanguage: -1,
-	}
+	p := &Path{}
+	p.reset()
+	p.component = component
+	return p
 }
 
 func (pp *PathParser) parse(component, s string) (*Path, error) {
@@ -114,10 +120,91 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
 	return p, nil
 }
 
-func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
-	hasLang := pp.LanguageIndex != nil
-	hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
+func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot int) {
+	if p.posContainerHigh != -1 {
+		return
+	}
+	mayHaveLang := pp.LanguageIndex != nil
+	mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
+	mayHaveOutputFormat := component == files.ComponentFolderLayouts
+	mayHaveKind := mayHaveOutputFormat
+
+	var found bool
+	var high int
+	if len(p.identifiers) > 0 {
+		high = lastDot
+	} else {
+		high = len(p.s)
+	}
+	id := types.LowHigh[string]{Low: i + 1, High: high}
+	sid := p.s[id.Low:id.High]
+
+	if len(p.identifiers) == 0 {
+		// The first is always the extension.
+		p.identifiers = append(p.identifiers, id)
+		found = true
+
+		// May also be the output format.
+		if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
+			p.posIdentifierOutputFormat = 0
+		}
+	} else {
+
+		var langFound bool
+
+		if mayHaveLang {
+			var disabled bool
+			_, langFound = pp.LanguageIndex[sid]
+			if !langFound {
+				disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
+				if disabled {
+					p.disabled = true
+					langFound = true
+				}
+			}
+			found = langFound
+			if langFound {
+				p.identifiers = append(p.identifiers, id)
+				p.posIdentifierLanguage = len(p.identifiers) - 1
+
+			}
+		}
+
+		if !found && mayHaveOutputFormat {
+			// At this point we may already have resolved an output format,
+			// but we need to keep looking for a more specific one, e.g. amp before html.
+			// Use both name and extension to prevent
+			// false positives on the form css.html.
+			if pp.IsOutputFormat(sid, p.Ext()) {
+				found = true
+				p.identifiers = append(p.identifiers, id)
+				p.posIdentifierOutputFormat = len(p.identifiers) - 1
+			}
+		}
+
+		if !found && mayHaveKind {
+			if kinds.GetKindMain(sid) != "" {
+				found = true
+				p.identifiers = append(p.identifiers, id)
+				p.posIdentifierKind = len(p.identifiers) - 1
+			}
+		}
+
+		if !found && sid == identifierBaseof {
+			found = true
+			p.identifiers = append(p.identifiers, id)
+			p.posIdentifierBaseof = len(p.identifiers) - 1
+		}
+
+		if !found {
+			p.identifiers = append(p.identifiers, id)
+			p.identifiersUnknown = append(p.identifiersUnknown, len(p.identifiers)-1)
+		}
+
+	}
+}
 
+func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
 	if runtime.GOOS == "windows" {
 		s = path.Clean(filepath.ToSlash(s))
 		if s == "." {
@@ -140,46 +227,21 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
 
 	p.s = s
 	slashCount := 0
+	lastDot := 0
 
 	for i := len(s) - 1; i >= 0; i-- {
 		c := s[i]
 
 		switch c {
 		case '.':
-			if p.posContainerHigh == -1 {
-				var high int
-				if len(p.identifiers) > 0 {
-					high = p.identifiers[len(p.identifiers)-1].Low - 1
-				} else {
-					high = len(p.s)
-				}
-				id := types.LowHigh[string]{Low: i + 1, High: high}
-				if len(p.identifiers) == 0 {
-					p.identifiers = append(p.identifiers, id)
-				} else if len(p.identifiers) == 1 {
-					// Check for a valid language.
-					s := p.s[id.Low:id.High]
-
-					if hasLang {
-						var disabled bool
-						_, langFound := pp.LanguageIndex[s]
-						if !langFound {
-							disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
-							if disabled {
-								p.disabled = true
-								langFound = true
-							}
-						}
-						if langFound {
-							p.posIdentifierLanguage = 1
-							p.identifiers = append(p.identifiers, id)
-						}
-					}
-				}
-			}
+			pp.parseIdentifier(component, s, p, i, lastDot)
+			lastDot = i
 		case '/':
 			slashCount++
 			if p.posContainerHigh == -1 {
+				if lastDot > 0 {
+					pp.parseIdentifier(component, s, p, i, lastDot)
+				}
 				p.posContainerHigh = i + 1
 			} else if p.posContainerLow == -1 {
 				p.posContainerLow = i + 1
@@ -194,22 +256,41 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
 		isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
 		isContent := isContentComponent && pp.IsContentExt(p.Ext())
 		id := p.identifiers[len(p.identifiers)-1]
-		b := p.s[p.posContainerHigh : id.Low-1]
-		if isContent {
-			switch b {
-			case "index":
-				p.bundleType = PathTypeLeaf
-			case "_index":
-				p.bundleType = PathTypeBranch
-			default:
-				p.bundleType = PathTypeContentSingle
+
+		if id.High > p.posContainerHigh {
+			b := p.s[p.posContainerHigh:id.High]
+			if isContent {
+				switch b {
+				case "index":
+					p.pathType = TypeLeaf
+				case "_index":
+					p.pathType = TypeBranch
+				default:
+					p.pathType = TypeContentSingle
+				}
+
+				if slashCount == 2 && p.IsLeafBundle() {
+					p.posSectionHigh = 0
+				}
+			} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
+				p.pathType = TypeContentData
 			}
+		}
+
+	}
 
-			if slashCount == 2 && p.IsLeafBundle() {
-				p.posSectionHigh = 0
+	if component == files.ComponentFolderLayouts {
+		if p.posIdentifierBaseof != -1 {
+			p.pathType = TypeBaseof
+		} else {
+			pth := p.Path()
+			if strings.Contains(pth, "/_shortcodes/") {
+				p.pathType = TypeShortcode
+			} else if strings.Contains(pth, "/_markup/") {
+				p.pathType = TypeMarkup
+			} else if strings.HasPrefix(pth, "/_partials/") {
+				p.pathType = TypePartial
 			}
-		} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
-			p.bundleType = PathTypeContentData
 		}
 	}
 
@@ -218,35 +299,44 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
 
 func ModifyPathBundleTypeResource(p *Path) {
 	if p.IsContent() {
-		p.bundleType = PathTypeContentResource
+		p.pathType = TypeContentResource
 	} else {
-		p.bundleType = PathTypeFile
+		p.pathType = TypeFile
 	}
 }
 
-type PathType int
+//go:generate stringer -type Type
+
+type Type int
 
 const (
+
 	// A generic resource, e.g. a JSON file.
-	PathTypeFile PathType = iota
+	TypeFile Type = iota
 
 	// All below are content files.
 	// A resource of a content type with front matter.
-	PathTypeContentResource
+	TypeContentResource
 
 	// E.g. /blog/my-post.md
-	PathTypeContentSingle
+	TypeContentSingle
 
 	// All below are bundled content files.
 
 	// Leaf bundles, e.g. /blog/my-post/index.md
-	PathTypeLeaf
+	TypeLeaf
 
 	// Branch bundles, e.g. /blog/_index.md
-	PathTypeBranch
+	TypeBranch
 
 	// Content data file, _content.gotmpl.
-	PathTypeContentData
+	TypeContentData
+
+	// Layout types.
+	TypeMarkup
+	TypeShortcode
+	TypePartial
+	TypeBaseof
 )
 
 type Path struct {
@@ -257,13 +347,17 @@ type Path struct {
 	posContainerHigh int
 	posSectionHigh   int
 
-	component  string
-	bundleType PathType
+	component string
+	pathType  Type
 
 	identifiers []types.LowHigh[string]
 
-	posIdentifierLanguage int
-	disabled              bool
+	posIdentifierLanguage     int
+	posIdentifierOutputFormat int
+	posIdentifierKind         int
+	posIdentifierBaseof       int
+	identifiersUnknown        []int
+	disabled                  bool
 
 	trimLeadingSlash bool
 
@@ -293,9 +387,12 @@ func (p *Path) reset() {
 	p.posContainerHigh = -1
 	p.posSectionHigh = -1
 	p.component = ""
-	p.bundleType = 0
+	p.pathType = 0
 	p.identifiers = p.identifiers[:0]
 	p.posIdentifierLanguage = -1
+	p.posIdentifierOutputFormat = -1
+	p.posIdentifierKind = -1
+	p.posIdentifierBaseof = -1
 	p.disabled = false
 	p.trimLeadingSlash = false
 	p.unnormalized = nil
@@ -316,6 +413,9 @@ func (p *Path) norm(s string) string {
 
 // IdentifierBase satisfies identity.Identity.
 func (p *Path) IdentifierBase() string {
+	if p.Component() == files.ComponentFolderLayouts {
+		return p.Path()
+	}
 	return p.Base()
 }
 
@@ -332,6 +432,13 @@ func (p *Path) Container() string {
 	return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
 }
 
+func (p *Path) String() string {
+	if p == nil {
+		return "<nil>"
+	}
+	return p.Path()
+}
+
 // ContainerDir returns the container directory for this path.
 // For content bundles this will be the parent directory.
 func (p *Path) ContainerDir() string {
@@ -352,13 +459,13 @@ func (p *Path) Section() string {
 // IsContent returns true if the path is a content file (e.g. mypost.md).
 // Note that this will also return true for content files in a bundle.
 func (p *Path) IsContent() bool {
-	return p.BundleType() >= PathTypeContentResource
+	return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
 }
 
 // isContentPage returns true if the path is a content file (e.g. mypost.md),
 // but nof if inside a leaf bundle.
 func (p *Path) isContentPage() bool {
-	return p.BundleType() >= PathTypeContentSingle
+	return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
 }
 
 // Name returns the last element of path.
@@ -398,10 +505,26 @@ func (p *Path) BaseNameNoIdentifier() string {
 
 // NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
 func (p *Path) NameNoIdentifier() string {
+	lowHigh := p.nameLowHigh()
+	return p.s[lowHigh.Low:lowHigh.High]
+}
+
+func (p *Path) nameLowHigh() types.LowHigh[string] {
 	if len(p.identifiers) > 0 {
-		return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
+		lastID := p.identifiers[len(p.identifiers)-1]
+		if p.posContainerHigh == lastID.Low {
+			// The last identifier is the name.
+			return lastID
+		}
+		return types.LowHigh[string]{
+			Low:  p.posContainerHigh,
+			High: p.identifiers[len(p.identifiers)-1].Low - 1,
+		}
+	}
+	return types.LowHigh[string]{
+		Low:  p.posContainerHigh,
+		High: len(p.s),
 	}
-	return p.s[p.posContainerHigh:]
 }
 
 // Dir returns all but the last element of path, typically the path's directory.
@@ -421,6 +544,11 @@ func (p *Path) Path() (d string) {
 	return p.norm(p.s)
 }
 
+// PathNoLeadingSlash returns the full path without the leading slash.
+func (p *Path) PathNoLeadingSlash() string {
+	return p.Path()[1:]
+}
+
 // Unnormalized returns the Path with the original case preserved.
 func (p *Path) Unnormalized() *Path {
 	return p.unnormalized
@@ -436,6 +564,28 @@ func (p *Path) PathNoIdentifier() string {
 	return p.base(false, false)
 }
 
+// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
+func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
+	if len(p.identifiers) == 0 {
+		return p.norm(p.s)
+	}
+	i := p.identifierIndex(0)
+
+	if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
+		i = j
+	}
+	if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
+		i = j
+	}
+
+	if i == -1 {
+		return p.norm(p.s)
+	}
+
+	id := p.identifiers[i]
+	return p.norm(p.s[:id.Low-1])
+}
+
 // PathRel returns the path relative to the given owner.
 func (p *Path) PathRel(owner *Path) string {
 	ob := owner.Base()
@@ -462,6 +612,21 @@ func (p *Path) Base() string {
 	return p.base(!p.isContentPage(), p.IsBundle())
 }
 
+// Used in template lookups.
+// For pages with Type set, we treat that as the section.
+func (p *Path) BaseReTyped(typ string) (d string) {
+	base := p.Base()
+	if typ == "" || p.Section() == typ {
+		return base
+	}
+	d = "/" + typ
+	if p.posSectionHigh != -1 {
+		d += base[p.posSectionHigh:]
+	}
+	d = p.norm(d)
+	return
+}
+
 // BaseNoLeadingSlash returns the base path without the leading slash.
 func (p *Path) BaseNoLeadingSlash() string {
 	return p.Base()[1:]
@@ -477,11 +642,12 @@ func (p *Path) base(preserveExt, isBundle bool) string {
 		return p.norm(p.s)
 	}
 
-	id := p.identifiers[len(p.identifiers)-1]
-	high := id.Low - 1
+	var high int
 
 	if isBundle {
 		high = p.posContainerHigh - 1
+	} else {
+		high = p.nameLowHigh().High
 	}
 
 	if high == 0 {
@@ -493,7 +659,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
 	}
 
 	// For txt files etc. we want to preserve the extension.
-	id = p.identifiers[0]
+	id := p.identifiers[0]
 
 	return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
 }
@@ -502,8 +668,16 @@ func (p *Path) Ext() string {
 	return p.identifierAsString(0)
 }
 
+func (p *Path) OutputFormat() string {
+	return p.identifierAsString(p.posIdentifierOutputFormat)
+}
+
+func (p *Path) Kind() string {
+	return p.identifierAsString(p.posIdentifierKind)
+}
+
 func (p *Path) Lang() string {
-	return p.identifierAsString(1)
+	return p.identifierAsString(p.posIdentifierLanguage)
 }
 
 func (p *Path) Identifier(i int) string {
@@ -522,28 +696,36 @@ func (p *Path) Identifiers() []string {
 	return ids
 }
 
-func (p *Path) BundleType() PathType {
-	return p.bundleType
+func (p *Path) IdentifiersUnknown() []string {
+	ids := make([]string, len(p.identifiersUnknown))
+	for i, id := range p.identifiersUnknown {
+		ids[i] = p.s[p.identifiers[id].Low:p.identifiers[id].High]
+	}
+	return ids
+}
+
+func (p *Path) Type() Type {
+	return p.pathType
 }
 
 func (p *Path) IsBundle() bool {
-	return p.bundleType >= PathTypeLeaf
+	return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
 }
 
 func (p *Path) IsBranchBundle() bool {
-	return p.bundleType == PathTypeBranch
+	return p.pathType == TypeBranch
 }
 
 func (p *Path) IsLeafBundle() bool {
-	return p.bundleType == PathTypeLeaf
+	return p.pathType == TypeLeaf
 }
 
 func (p *Path) IsContentData() bool {
-	return p.bundleType == PathTypeContentData
+	return p.pathType == TypeContentData
 }
 
-func (p Path) ForBundleType(t PathType) *Path {
-	p.bundleType = t
+func (p Path) ForBundleType(t Type) *Path {
+	p.pathType = t
 	return &p
 }
 
diff --git a/common/paths/pathtype_string.go b/common/paths/pathtype_string.go
deleted file mode 100644
index 7a99f8a03dd..00000000000
--- a/common/paths/pathtype_string.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Code generated by "stringer -type=PathType"; DO NOT EDIT.
-
-package paths
-
-import "strconv"
-
-func _() {
-	// An "invalid array index" compiler error signifies that the constant values have changed.
-	// Re-run the stringer command to generate them again.
-	var x [1]struct{}
-	_ = x[PathTypeFile-0]
-	_ = x[PathTypeContentResource-1]
-	_ = x[PathTypeContentSingle-2]
-	_ = x[PathTypeLeaf-3]
-	_ = x[PathTypeBranch-4]
-}
-
-const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
-
-var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
-
-func (i PathType) String() string {
-	if i < 0 || i >= PathType(len(_PathType_index)-1) {
-		return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
-	}
-	return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
-}
diff --git a/common/paths/type_string.go b/common/paths/type_string.go
new file mode 100644
index 00000000000..08fbcc835e1
--- /dev/null
+++ b/common/paths/type_string.go
@@ -0,0 +1,32 @@
+// Code generated by "stringer -type Type"; DO NOT EDIT.
+
+package paths
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[TypeFile-0]
+	_ = x[TypeContentResource-1]
+	_ = x[TypeContentSingle-2]
+	_ = x[TypeLeaf-3]
+	_ = x[TypeBranch-4]
+	_ = x[TypeContentData-5]
+	_ = x[TypeMarkup-6]
+	_ = x[TypeShortcode-7]
+	_ = x[TypePartial-8]
+	_ = x[TypeBaseof-9]
+}
+
+const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
+
+var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
+
+func (i Type) String() string {
+	if i < 0 || i >= Type(len(_Type_index)-1) {
+		return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _Type_name[_Type_index[i]:_Type_index[i+1]]
+}
diff --git a/common/types/types.go b/common/types/types.go
index 082c058ffff..7e94c1eea72 100644
--- a/common/types/types.go
+++ b/common/types/types.go
@@ -28,6 +28,16 @@ type RLocker interface {
 	RUnlock()
 }
 
+type Locker interface {
+	Lock()
+	Unlock()
+}
+
+type RWLocker interface {
+	RLocker
+	Locker
+}
+
 // KeyValue is a interface{} tuple.
 type KeyValue struct {
 	Key   any
diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
index 758d7d98612..e73153a94b5 100644
--- a/config/allconfig/allconfig.go
+++ b/config/allconfig/allconfig.go
@@ -849,7 +849,24 @@ func (c *Configs) Init() error {
 	c.Languages = languages
 	c.LanguagesDefaultFirst = languagesDefaultFirst
 
-	c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
+	c.ContentPathParser = &paths.PathParser{
+		LanguageIndex:  languagesDefaultFirst.AsIndexSet(),
+		IsLangDisabled: c.Base.IsLangDisabled,
+		IsContentExt:   c.Base.ContentTypes.Config.IsContentSuffix,
+		IsOutputFormat: func(name, ext string) bool {
+			if name == "" {
+				return false
+			}
+
+			if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok {
+				if ext != "" && !of.MediaType.HasSuffix(ext) {
+					return false
+				}
+				return true
+			}
+			return false
+		},
+	}
 
 	c.configLangs = make([]config.AllProvider, len(c.Languages))
 	for i, l := range c.LanguagesDefaultFirst {
diff --git a/create/content.go b/create/content.go
index db07720ade6..a4661c1ba14 100644
--- a/create/content.go
+++ b/create/content.go
@@ -291,7 +291,7 @@ func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugo
 func (b *contentBuilder) mapArcheTypeDir() error {
 	var m archetypeMap
 
-	seen := map[hstrings.Tuple]bool{}
+	seen := map[hstrings.Strings2]bool{}
 
 	walkFn := func(path string, fim hugofs.FileMetaInfo) error {
 		if fim.IsDir() {
@@ -301,7 +301,7 @@ func (b *contentBuilder) mapArcheTypeDir() error {
 		pi := fim.Meta().PathInfo
 
 		if pi.IsContent() {
-			pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang}
+			pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang}
 			if seen[pathLang] {
 				// Duplicate content file, e.g. page.md and page.html.
 				// In the regular build, we will filter out the duplicates, but
diff --git a/deps/deps.go b/deps/deps.go
index 34c41012cf3..d0d6d95fc37 100644
--- a/deps/deps.go
+++ b/deps/deps.go
@@ -29,6 +29,7 @@ import (
 	"github.com/gohugoio/hugo/media"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/resources/postpub"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 
 	"github.com/gohugoio/hugo/metrics"
 	"github.com/gohugoio/hugo/resources"
@@ -46,12 +47,6 @@ type Deps struct {
 
 	ExecHelper *hexec.Exec
 
-	// The templates to use. This will usually implement the full tpl.TemplateManager.
-	tmplHandlers *tpl.TemplateHandlers
-
-	// The template funcs.
-	TmplFuncMap map[string]any
-
 	// The file systems to use.
 	Fs *hugofs.Fs `json:"-"`
 
@@ -79,7 +74,8 @@ type Deps struct {
 	// The site building.
 	Site page.Site
 
-	TemplateProvider ResourceProvider
+	TemplateStore *tplimpl.TemplateStore
+
 	// Used in tests
 	OverloadedTemplateFuncs map[string]any
 
@@ -102,6 +98,9 @@ type Deps struct {
 	// This is common/global for all sites.
 	BuildState *BuildState
 
+	// Misc counters.
+	Counters *Counters
+
 	// Holds RPC dispatchers for Katex etc.
 	// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
 	WasmDispatchers *warpc.Dispatchers
@@ -109,9 +108,6 @@ type Deps struct {
 	// The JS batcher client.
 	JSBatcherClient js.BatcherClient
 
-	// The JS batcher client.
-	// JSBatcherClient *esbuild.BatcherClient
-
 	isClosed bool
 
 	*globalErrHandler
@@ -130,8 +126,8 @@ func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
 	return &d, nil
 }
 
-func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
-	d.tmplHandlers = t
+func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
+	return d.TemplateStore
 }
 
 func (d *Deps) Init() error {
@@ -153,10 +149,12 @@ func (d *Deps) Init() error {
 			logger: d.Log,
 		}
 	}
-
 	if d.BuildState == nil {
 		d.BuildState = &BuildState{}
 	}
+	if d.Counters == nil {
+		d.Counters = &Counters{}
+	}
 	if d.BuildState.DeferredExecutions == nil {
 		if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
 			d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
@@ -263,22 +261,17 @@ func (d *Deps) Init() error {
 	return nil
 }
 
+// TODO(bep) rework this to get it in line with how we manage templates.
 func (d *Deps) Compile(prototype *Deps) error {
 	var err error
 	if prototype == nil {
-		if err = d.TemplateProvider.NewResource(d); err != nil {
-			return err
-		}
+
 		if err = d.TranslationProvider.NewResource(d); err != nil {
 			return err
 		}
 		return nil
 	}
 
-	if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
-		return err
-	}
-
 	if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
 		return err
 	}
@@ -378,14 +371,6 @@ type ResourceProvider interface {
 	CloneResource(dst, src *Deps) error
 }
 
-func (d *Deps) Tmpl() tpl.TemplateHandler {
-	return d.tmplHandlers.Tmpl
-}
-
-func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
-	return d.tmplHandlers.TxtTmpl
-}
-
 func (d *Deps) Close() error {
 	if d.isClosed {
 		return nil
@@ -454,6 +439,12 @@ type BuildState struct {
 	DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
 }
 
+// Misc counters.
+type Counters struct {
+	// Counter for the math.Counter function.
+	MathCounter atomic.Uint64
+}
+
 type DeferredExecutions struct {
 	// A set of filenames in /public that
 	// contains a post-processing prefix.
diff --git a/hugolib/alias.go b/hugolib/alias.go
index 08d57a8bced..0d182042a59 100644
--- a/hugolib/alias.go
+++ b/hugolib/alias.go
@@ -29,16 +29,17 @@ import (
 	"github.com/gohugoio/hugo/publisher"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/tpl"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 )
 
 type aliasHandler struct {
-	t         tpl.TemplateHandler
+	ts        *tplimpl.TemplateStore
 	log       loggers.Logger
 	allowRoot bool
 }
 
-func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler {
-	return aliasHandler{t, l, allowRoot}
+func newAliasHandler(ts *tplimpl.TemplateStore, l loggers.Logger, allowRoot bool) aliasHandler {
+	return aliasHandler{ts, l, allowRoot}
 }
 
 type aliasPage struct {
@@ -47,16 +48,24 @@ type aliasPage struct {
 }
 
 func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) {
-	var templ tpl.Template
-	var found bool
-
-	templ, found = a.t.Lookup("alias.html")
-	if !found {
-		// TODO(bep) consolidate
-		templ, found = a.t.Lookup("_internal/alias.html")
-		if !found {
-			return nil, errors.New("no alias template found")
-		}
+	var templateDesc tplimpl.TemplateDescriptor
+	var base string = ""
+	if ps, ok := p.(*pageState); ok {
+		base, templateDesc = ps.getTemplateBasePathAndDescriptor()
+	}
+	templateDesc.Layout = ""
+	templateDesc.Kind = ""
+	templateDesc.OutputFormat = output.AliasHTMLFormat.Name
+
+	q := tplimpl.TemplateQuery{
+		Path:     base,
+		Category: tplimpl.CategoryLayout,
+		Desc:     templateDesc,
+	}
+
+	t := a.ts.LookupPagesLayout(q)
+	if t == nil {
+		return nil, errors.New("no alias template found")
 	}
 
 	data := aliasPage{
@@ -67,7 +76,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err
 	ctx := tpl.Context.Page.Set(context.Background(), p)
 
 	buffer := new(bytes.Buffer)
-	err := a.t.ExecuteWithContext(ctx, templ, buffer, data)
+	err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
 	if err != nil {
 		return nil, err
 	}
@@ -79,7 +88,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format
 }
 
 func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
-	handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot)
+	handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot)
 
 	targetPath, err := handler.targetPathAlias(path)
 	if err != nil {
diff --git a/hugolib/content_factory.go b/hugolib/content_factory.go
index e6b0fb50685..109e988b1d4 100644
--- a/hugolib/content_factory.go
+++ b/hugolib/content_factory.go
@@ -72,12 +72,12 @@ func (f ContentFactory) ApplyArchetypeTemplate(w io.Writer, p page.Page, archety
 
 	templateSource = f.shortcodeReplacerPre.Replace(templateSource)
 
-	templ, err := ps.s.TextTmpl().Parse("archetype.md", string(templateSource))
+	templ, err := ps.s.TemplateStore.TextParse("archetype.md", templateSource)
 	if err != nil {
 		return fmt.Errorf("failed to parse archetype template: %s: %w", err, err)
 	}
 
-	result, err := executeToString(context.Background(), ps.s.Tmpl(), templ, d)
+	result, err := executeToString(context.Background(), ps.s.GetTemplateStore(), templ, d)
 	if err != nil {
 		return fmt.Errorf("failed to execute archetype template: %s: %w", err, err)
 	}
diff --git a/hugolib/content_map.go b/hugolib/content_map.go
index 55c96c9a053..56a602f540f 100644
--- a/hugolib/content_map.go
+++ b/hugolib/content_map.go
@@ -264,8 +264,8 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
 	meta := fi.Meta()
 	pi := meta.PathInfo
 
-	switch pi.BundleType() {
-	case paths.PathTypeFile, paths.PathTypeContentResource:
+	switch pi.Type() {
+	case paths.TypeFile, paths.TypeContentResource:
 		m.s.Log.Trace(logg.StringFunc(
 			func() string {
 				return fmt.Sprintf("insert resource: %q", fi.Meta().Filename)
@@ -275,7 +275,7 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
 			addErr = err
 			return
 		}
-	case paths.PathTypeContentData:
+	case paths.TypeContentData:
 		pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
 		pageCount += pc
 		resourceCount += rc
@@ -349,8 +349,7 @@ func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *Buil
 				DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
 					ss := s.(*Site)
 					return pagesfromdata.PagesFromTemplateDeps{
-						TmplFinder: ss.TextTmpl(),
-						TmplExec:   ss.Tmpl(),
+						TemplateStore: ss.GetTemplateStore(),
 					}
 				},
 				DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"),
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
index fcc650c39b3..b501cd9ea94 100644
--- a/hugolib/content_map_page.go
+++ b/hugolib/content_map_page.go
@@ -180,7 +180,7 @@ func (t *pageTrees) collectAndMarkStaleIdentities(p *paths.Path) []identity.Iden
 
 	if p.Component() == files.ComponentFolderContent {
 		// It may also be a bundled content resource.
-		key := p.ForBundleType(paths.PathTypeContentResource).Base()
+		key := p.ForBundleType(paths.TypeContentResource).Base()
 		tree = t.treeResources
 		nCount = 0
 		tree.ForEeachInDimension(key, doctree.DimensionLanguage.Index(),
@@ -1304,14 +1304,14 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
 		checkedCounter atomic.Int64
 	)
 
-	resetPo := func(po *pageOutput, r identity.FinderResult) {
-		if po.pco != nil {
+	resetPo := func(po *pageOutput, rebuildContent bool, r identity.FinderResult) {
+		if rebuildContent && po.pco != nil {
 			po.pco.Reset() // Will invalidate content cache.
 		}
 
 		po.renderState = 0
 		po.p.resourcesPublishInit = &sync.Once{}
-		if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatusHTMLFormat.Name {
+		if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatus404HTMLFormat.Name {
 			// Will force a re-render even in fast render mode.
 			po.renderOnce = false
 		}
@@ -1323,7 +1323,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
 	}
 
 	// This can be a relativeley expensive operations, so we do it in parallel.
-	g := rungroup.Run[*pageState](ctx, rungroup.Config[*pageState]{
+	g := rungroup.Run(ctx, rungroup.Config[*pageState]{
 		NumWorkers: h.numWorkers,
 		Handle: func(ctx context.Context, p *pageState) error {
 			if !p.isRenderedAny() {
@@ -1335,7 +1335,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
 				checkedCounter.Add(1)
 				if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition {
 					for _, po := range p.pageOutputs {
-						resetPo(po, r)
+						// Note that p.dependencyManager is used when rendering content, so reset that.
+						resetPo(po, true, r)
 					}
 					// Done.
 					return nil
@@ -1351,7 +1352,8 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
 				for _, id := range changes {
 					checkedCounter.Add(1)
 					if r := depsFinder.Contains(id, po.dependencyManagerOutput, 50); r > identity.FinderFoundOneOfManyRepetition {
-						resetPo(po, r)
+						// Note that dependencyManagerOutput is not used when rendering content, so don't reset that.
+						resetPo(po, false, r)
 						continue OUTPUTS
 					}
 				}
@@ -1954,7 +1956,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
 		tree.InsertIntoValuesDimension(key, p)
 	}
 
-	addStandalone("/404", kinds.KindStatus404, output.HTTPStatusHTMLFormat)
+	addStandalone("/404", kinds.KindStatus404, output.HTTPStatus404HTMLFormat)
 
 	if s.conf.EnableRobotsTXT {
 		if m.i == 0 || s.Conf.IsMultihost() {
diff --git a/hugolib/doctree/simpletree.go b/hugolib/doctree/simpletree.go
index 2193c08f62a..b79ef3f2f43 100644
--- a/hugolib/doctree/simpletree.go
+++ b/hugolib/doctree/simpletree.go
@@ -14,35 +14,46 @@
 package doctree
 
 import (
+	"iter"
 	"sync"
 
 	radix "github.com/armon/go-radix"
 )
 
-// Tree is a radix tree that holds T.
+// Tree is a non thread safe radix tree that holds T.
 type Tree[T any] interface {
+	TreeCommon[T]
+	WalkPrefix(s string, f func(s string, v T) (bool, error)) error
+	WalkPath(s string, f func(s string, v T) (bool, error)) error
+	All() iter.Seq2[string, T]
+}
+
+// TreeThreadSafe is a thread safe radix tree that holds T.
+type TreeThreadSafe[T any] interface {
+	TreeCommon[T]
+	WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
+	WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error
+	All(lockType LockType) iter.Seq2[string, T]
+}
+
+type TreeCommon[T any] interface {
 	Get(s string) T
 	LongestPrefix(s string) (string, T)
 	Insert(s string, v T) T
-	WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error
 }
 
-// NewSimpleTree creates a new SimpleTree.
-func NewSimpleTree[T comparable]() *SimpleTree[T] {
+func NewSimpleTree[T any]() *SimpleTree[T] {
 	return &SimpleTree[T]{tree: radix.New()}
 }
 
-// SimpleTree is a thread safe radix tree that holds T.
-type SimpleTree[T comparable] struct {
-	mu   sync.RWMutex
+// SimpleTree is a radix tree that holds T.
+// This tree is not thread safe.
+type SimpleTree[T any] struct {
 	tree *radix.Tree
 	zero T
 }
 
 func (tree *SimpleTree[T]) Get(s string) T {
-	tree.mu.RLock()
-	defer tree.mu.RUnlock()
-
 	if v, ok := tree.tree.Get(s); ok {
 		return v.(T)
 	}
@@ -50,9 +61,6 @@ func (tree *SimpleTree[T]) Get(s string) T {
 }
 
 func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
-	tree.mu.RLock()
-	defer tree.mu.RUnlock()
-
 	if s, v, ok := tree.tree.LongestPrefix(s); ok {
 		return s, v.(T)
 	}
@@ -60,17 +68,121 @@ func (tree *SimpleTree[T]) LongestPrefix(s string) (string, T) {
 }
 
 func (tree *SimpleTree[T]) Insert(s string, v T) T {
+	tree.tree.Insert(s, v)
+	return v
+}
+
+func (tree *SimpleTree[T]) Walk(f func(s string, v T) (bool, error)) error {
+	var err error
+	tree.tree.Walk(func(s string, v any) bool {
+		var b bool
+		b, err = f(s, v.(T))
+		if err != nil {
+			return true
+		}
+		return b
+	})
+	return err
+}
+
+func (tree *SimpleTree[T]) WalkPrefix(s string, f func(s string, v T) (bool, error)) error {
+	var err error
+	tree.tree.WalkPrefix(s, func(s string, v any) bool {
+		var b bool
+		b, err = f(s, v.(T))
+		if err != nil {
+			return true
+		}
+		return b
+	})
+
+	return err
+}
+
+func (tree *SimpleTree[T]) WalkPath(s string, f func(s string, v T) (bool, error)) error {
+	var err error
+	tree.tree.WalkPath(s, func(s string, v any) bool {
+		var b bool
+		b, err = f(s, v.(T))
+		if err != nil {
+			return true
+		}
+		return b
+	})
+	return err
+}
+
+func (tree *SimpleTree[T]) All() iter.Seq2[string, T] {
+	return func(yield func(s string, v T) bool) {
+		tree.tree.Walk(func(s string, v any) bool {
+			return !yield(s, v.(T))
+		})
+	}
+}
+
+// NewSimpleThreadSafeTree creates a new SimpleTree.
+func NewSimpleThreadSafeTree[T any]() *SimpleThreadSafeTree[T] {
+	return &SimpleThreadSafeTree[T]{tree: radix.New(), mu: new(sync.RWMutex)}
+}
+
+// SimpleThreadSafeTree is a thread safe radix tree that holds T.
+type SimpleThreadSafeTree[T any] struct {
+	mu     *sync.RWMutex
+	noLock bool
+	tree   *radix.Tree
+	zero   T
+}
+
+var noopFunc = func() {}
+
+func (tree *SimpleThreadSafeTree[T]) readLock() func() {
+	if tree.noLock {
+		return noopFunc
+	}
+	tree.mu.RLock()
+	return tree.mu.RUnlock
+}
+
+func (tree *SimpleThreadSafeTree[T]) writeLock() func() {
+	if tree.noLock {
+		return noopFunc
+	}
 	tree.mu.Lock()
-	defer tree.mu.Unlock()
+	return tree.mu.Unlock
+}
+
+func (tree *SimpleThreadSafeTree[T]) Get(s string) T {
+	unlock := tree.readLock()
+	defer unlock()
+
+	if v, ok := tree.tree.Get(s); ok {
+		return v.(T)
+	}
+	return tree.zero
+}
+
+func (tree *SimpleThreadSafeTree[T]) LongestPrefix(s string) (string, T) {
+	unlock := tree.readLock()
+	defer unlock()
+
+	if s, v, ok := tree.tree.LongestPrefix(s); ok {
+		return s, v.(T)
+	}
+	return "", tree.zero
+}
+
+func (tree *SimpleThreadSafeTree[T]) Insert(s string, v T) T {
+	unlock := tree.writeLock()
+	defer unlock()
 
 	tree.tree.Insert(s, v)
 	return v
 }
 
-func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
+func (tree *SimpleThreadSafeTree[T]) Lock(lockType LockType) func() {
 	switch lockType {
 	case LockTypeNone:
-		return func() {}
+		return noopFunc
 	case LockTypeRead:
 		tree.mu.RLock()
 		return tree.mu.RUnlock
@@ -78,10 +190,16 @@ func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
 		tree.mu.Lock()
 		return tree.mu.Unlock
 	}
-	return func() {}
+	return noopFunc
 }
 
-func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+func (tree SimpleThreadSafeTree[T]) LockTree(lockType LockType) (TreeThreadSafe[T], func()) {
+	unlock := tree.Lock(lockType)
+	tree.noLock = true
+	return &tree, unlock // create a copy of tree with the noLock flag set to true.
+}
+
+func (tree *SimpleThreadSafeTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
 	commit := tree.Lock(lockType)
 	defer commit()
 	var err error
@@ -96,3 +214,31 @@ func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s stri
 
 	return err
 }
+
+func (tree *SimpleThreadSafeTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+	commit := tree.Lock(lockType)
+	defer commit()
+	var err error
+	tree.tree.WalkPath(s, func(s string, v any) bool {
+		var b bool
+		b, err = f(s, v.(T))
+		if err != nil {
+			return true
+		}
+		return b
+	})
+
+	return err
+}
+
+func (tree *SimpleThreadSafeTree[T]) All(lockType LockType) iter.Seq2[string, T] {
+	commit := tree.Lock(lockType)
+	defer commit()
+	return func(yield func(s string, v T) bool) {
+		tree.tree.Walk(func(s string, v any) bool {
+			return !yield(s, v.(T))
+		})
+	}
+}
+
+// iter.Seq[*TemplWithBaseApplied]
diff --git a/hugolib/doctree/support.go b/hugolib/doctree/support.go
index adcc3b06cdb..f1b713b312f 100644
--- a/hugolib/doctree/support.go
+++ b/hugolib/doctree/support.go
@@ -17,8 +17,6 @@ import (
 	"fmt"
 	"strings"
 	"sync"
-
-	radix "github.com/armon/go-radix"
 )
 
 var _ MutableTrees = MutableTrees{}
@@ -60,11 +58,9 @@ func (ctx *WalkContext[T]) AddPostHook(handler func() error) {
 	ctx.HooksPost = append(ctx.HooksPost, handler)
 }
 
-func (ctx *WalkContext[T]) Data() *SimpleTree[any] {
+func (ctx *WalkContext[T]) Data() *SimpleThreadSafeTree[any] {
 	ctx.dataInit.Do(func() {
-		ctx.data = &SimpleTree[any]{
-			tree: radix.New(),
-		}
+		ctx.data = NewSimpleThreadSafeTree[any]()
 	})
 	return ctx.data
 }
@@ -191,7 +187,7 @@ func (t MutableTrees) CanLock() bool {
 
 // WalkContext is passed to the Walk callback.
 type WalkContext[T any] struct {
-	data          *SimpleTree[any]
+	data          *SimpleThreadSafeTree[any]
 	dataInit      sync.Once
 	eventHandlers eventHandlers[T]
 	events        []*Event[T]
diff --git a/hugolib/doctree/treeshifttree.go b/hugolib/doctree/treeshifttree.go
index 059eaaf88b8..8f958d82842 100644
--- a/hugolib/doctree/treeshifttree.go
+++ b/hugolib/doctree/treeshifttree.go
@@ -13,7 +13,9 @@
 
 package doctree
 
-var _ Tree[string] = (*TreeShiftTree[string])(nil)
+import "iter"
+
+var _ TreeThreadSafe[string] = (*TreeShiftTree[string])(nil)
 
 type TreeShiftTree[T comparable] struct {
 	// This tree is shiftable in one dimension.
@@ -26,16 +28,16 @@ type TreeShiftTree[T comparable] struct {
 	zero T
 
 	// Will be of length equal to the length of the dimension.
-	trees []*SimpleTree[T]
+	trees []*SimpleThreadSafeTree[T]
 }
 
 func NewTreeShiftTree[T comparable](d, length int) *TreeShiftTree[T] {
 	if length <= 0 {
 		panic("length must be > 0")
 	}
-	trees := make([]*SimpleTree[T], length)
+	trees := make([]*SimpleThreadSafeTree[T], length)
 	for i := range length {
-		trees[i] = NewSimpleTree[T]()
+		trees[i] = NewSimpleThreadSafeTree[T]()
 	}
 	return &TreeShiftTree[T]{d: d, trees: trees}
 }
@@ -91,6 +93,14 @@ func (t *TreeShiftTree[T]) WalkPrefixRaw(lockType LockType, s string, f func(s s
 	return nil
 }
 
+func (t *TreeShiftTree[T]) WalkPath(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
+	return t.trees[t.v].WalkPath(lockType, s, f)
+}
+
+func (t *TreeShiftTree[T]) All(lockType LockType) iter.Seq2[string, T] {
+	return t.trees[t.v].All(lockType)
+}
+
 func (t *TreeShiftTree[T]) LenRaw() int {
 	var count int
 	for _, tt := range t.trees {
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
index 5373f9832ac..0b68af2ecdd 100644
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -134,7 +134,6 @@ func (h *HugoSites) resolveSite(lang string) *Site {
 	return nil
 }
 
-// Only used in tests.
 type buildCounters struct {
 	contentRenderCounter atomic.Uint64
 	pageRenderCounter    atomic.Uint64
@@ -557,7 +556,6 @@ func (h *HugoSites) handleDataFile(r *source.File) error {
 	higherPrecedentData := current[r.BaseFileName()]
 
 	switch data.(type) {
-	case nil:
 	case map[string]any:
 
 		switch higherPrecedentData.(type) {
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 729c17c7528..ce4ca370c21 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -494,17 +494,17 @@ func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error {
 				defer deferred.Mu.Unlock()
 
 				if !deferred.Executed {
-					tmpl := s.Deps.Tmpl()
-					templ, found := tmpl.Lookup(deferred.TemplateName)
-					if !found {
-						panic(fmt.Sprintf("template %q not found", deferred.TemplateName))
+					tmpl := s.Deps.GetTemplateStore()
+					ti := s.TemplateStore.LookupByPath(deferred.TemplatePath)
+					if ti == nil {
+						panic(fmt.Sprintf("template %q not found", deferred.TemplatePath))
 					}
 
 					if err := func() error {
 						buf := bufferpool.GetBuffer()
 						defer bufferpool.PutBuffer(buf)
 
-						err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data)
+						err = tmpl.ExecuteWithContext(deferred.Ctx, ti, buf, deferred.Data)
 						if err != nil {
 							return err
 						}
@@ -577,9 +577,13 @@ func (h *HugoSites) printUnusedTemplatesOnce() error {
 	h.printUnusedTemplatesInit.Do(func() {
 		conf := h.Configs.Base
 		if conf.PrintUnusedTemplates {
-			unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
+			unusedTemplates := h.GetTemplateStore().UnusedTemplates()
 			for _, unusedTemplate := range unusedTemplates {
-				h.Log.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
+				if unusedTemplate.Fi != nil {
+					h.Log.Warnf("Template %s is unused, source %q", unusedTemplate.PathInfo.Path(), unusedTemplate.Fi.Meta().Filename)
+				} else {
+					h.Log.Warnf("Template %s is unused", unusedTemplate.PathInfo.Path())
+				}
 			}
 		}
 	})
@@ -954,7 +958,7 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
 		case files.ComponentFolderLayouts:
 			tmplChanged = true
 			templatePath := pathInfo.Unnormalized().TrimLeadingSlash().PathNoLang()
-			if !h.Tmpl().HasTemplate(templatePath) {
+			if !h.GetTemplateStore().HasTemplate(templatePath) {
 				tmplAdded = true
 			}
 
@@ -974,8 +978,9 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
 				}
 			} else {
 				logger.Println("Template changed", pathInfo.Path())
-				if templ, found := h.Tmpl().GetIdentity(templatePath); found {
-					changes = append(changes, templ)
+				id := h.GetTemplateStore().GetIdentity(pathInfo.Path())
+				if id != nil {
+					changes = append(changes, id)
 				} else {
 					changes = append(changes, pathInfo)
 				}
@@ -1084,7 +1089,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
 
 	changed := &WhatChanged{
 		needsPagesAssembly: needsPagesAssemble,
-		identitySet:        make(identity.Identities),
 	}
 	changed.Add(changes...)
 
@@ -1106,17 +1110,39 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
 		}
 	}
 
-	h.Deps.OnChangeListeners.Notify(changed.Changes()...)
+	changes2 := changed.Changes()
+	h.Deps.OnChangeListeners.Notify(changes2...)
 
 	if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
 		return err
 	}
 
-	if tmplChanged || i18nChanged {
+	if tmplChanged {
 		if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
-			// TODO(bep) this could probably be optimized to somehow
-			// only load the changed templates and its dependencies, but that is non-trivial.
+			depsFinder := identity.NewFinder(identity.FinderConfig{})
 			ll := l.WithField("substep", "rebuild templates")
+			s := h.Sites[0]
+			if err := s.Deps.TemplateStore.RefreshFiles(func(fi hugofs.FileMetaInfo) bool {
+				pi := fi.Meta().PathInfo
+				for _, id := range changes2 {
+					if depsFinder.Contains(pi, id, -1) > 0 {
+						return true
+					}
+				}
+				return false
+			}); err != nil {
+				return ll, err
+			}
+
+			return ll, nil
+		}); err != nil {
+			return err
+		}
+	}
+
+	if i18nChanged {
+		if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
+			ll := l.WithField("substep", "rebuild i18n")
 			var prototype *deps.Deps
 			for i, s := range h.Sites {
 				if err := s.Deps.Compile(prototype); err != nil {
diff --git a/hugolib/page.go b/hugolib/page.go
index de64767df17..41519909b16 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -28,15 +28,13 @@ import (
 	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/media"
 	"github.com/gohugoio/hugo/output"
-	"github.com/gohugoio/hugo/output/layouts"
 	"github.com/gohugoio/hugo/related"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 	"github.com/spf13/afero"
 
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/gohugoio/hugo/markup/tableofcontents"
 
-	"github.com/gohugoio/hugo/tpl"
-
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/types"
 
@@ -116,6 +114,14 @@ type pageState struct {
 	resourcesPublishInit *sync.Once
 }
 
+func (p *pageState) incrPageOutputTemplateVariation() {
+	p.pageOutputTemplateVariationsState.Add(1)
+}
+
+func (p *pageState) canReusePageOutputContent() bool {
+	return p.pageOutputTemplateVariationsState.Load() == 1
+}
+
 func (p *pageState) IdentifierBase() string {
 	return p.Path()
 }
@@ -169,10 +175,6 @@ func (p *pageState) resetBuildState() {
 	// Nothing to do for now.
 }
 
-func (p *pageState) reusePageOutputContent() bool {
-	return p.pageOutputTemplateVariationsState.Load() == 1
-}
-
 func (p *pageState) skipRender() bool {
 	b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
 		segments.SegmentMatcherFields{
@@ -474,49 +476,40 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
 	return nil
 }
 
-func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor {
-	p.layoutDescriptorInit.Do(func() {
-		var section string
-		sections := p.SectionsEntries()
-
-		switch p.Kind() {
-		case kinds.KindSection:
-			if len(sections) > 0 {
-				section = sections[0]
-			}
-		case kinds.KindTaxonomy, kinds.KindTerm:
-
-			if p.m.singular != "" {
-				section = p.m.singular
-			} else if len(sections) > 0 {
-				section = sections[0]
-			}
-		default:
-		}
-
-		p.layoutDescriptor = layouts.LayoutDescriptor{
-			Kind:    p.Kind(),
-			Type:    p.Type(),
-			Lang:    p.Language().Lang,
-			Layout:  p.Layout(),
-			Section: section,
-		}
-	})
-
-	return p.layoutDescriptor
+func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
+	p := po.p
+	f := po.f
+	base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
+	return base, tplimpl.TemplateDescriptor{
+		Kind:         p.Kind(),
+		Lang:         p.Language().Lang,
+		Layout:       p.Layout(),
+		OutputFormat: f.Name,
+		MediaType:    f.MediaType.Type,
+		IsPlainText:  f.IsPlainText,
+	}
 }
 
-func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) {
-	f := p.outputFormat()
-
-	d := p.getLayoutDescriptor()
+func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
+	dir, d := p.getTemplateBasePathAndDescriptor()
 
 	if len(layouts) > 0 {
 		d.Layout = layouts[0]
-		d.LayoutOverride = true
+		d.LayoutMustMatch = true
+	}
+
+	q := tplimpl.TemplateQuery{
+		Path:     dir,
+		Category: tplimpl.CategoryLayout,
+		Desc:     d,
+	}
+
+	tinfo := p.s.TemplateStore.LookupPagesLayout(q)
+	if tinfo == nil {
+		return nil, false, nil
 	}
 
-	return p.s.Tmpl().LookupLayout(d, f)
+	return tinfo, true, nil
 }
 
 // Must be run after the site section tree etc. is built and ready.
@@ -705,7 +698,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
 
 	if isRenderingSite {
 		cp := p.pageOutput.pco
-		if cp == nil && p.reusePageOutputContent() {
+		if cp == nil && p.canReusePageOutputContent() {
 			// Look for content to reuse.
 			for i := range p.pageOutputs {
 				if i == idx {
diff --git a/hugolib/page__common.go b/hugolib/page__common.go
index 7407f7140cc..27d2c3089b2 100644
--- a/hugolib/page__common.go
+++ b/hugolib/page__common.go
@@ -21,7 +21,6 @@ import (
 	"github.com/gohugoio/hugo/lazy"
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/gohugoio/hugo/navigation"
-	"github.com/gohugoio/hugo/output/layouts"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/resources/resource"
 	"github.com/gohugoio/hugo/source"
@@ -86,9 +85,6 @@ type pageCommon struct {
 	// should look like.
 	targetPathDescriptor page.TargetPathDescriptor
 
-	layoutDescriptor     layouts.LayoutDescriptor
-	layoutDescriptorInit sync.Once
-
 	// Set if feature enabled and this is in a Git repo.
 	gitInfo    source.GitInfo
 	codeowners []string
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
index 3cfea1727be..b42c2b419d8 100644
--- a/hugolib/page__content.go
+++ b/hugolib/page__content.go
@@ -24,6 +24,8 @@ import (
 	"strings"
 	"unicode/utf8"
 
+	maps0 "maps"
+
 	"github.com/bep/logg"
 	"github.com/gohugoio/hugo/common/hcontext"
 	"github.com/gohugoio/hugo/common/herrors"
@@ -32,7 +34,6 @@ import (
 	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/common/types/hstring"
 	"github.com/gohugoio/hugo/helpers"
-	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup"
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
@@ -45,7 +46,6 @@ import (
 	"github.com/gohugoio/hugo/tpl"
 	"github.com/mitchellh/mapstructure"
 	"github.com/spf13/cast"
-	maps0 "maps"
 )
 
 const (
@@ -600,7 +600,7 @@ func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummar
 				return nil, err
 			}
 			if hasShortcodeVariants {
-				cp.po.p.pageOutputTemplateVariationsState.Add(1)
+				cp.po.p.incrPageOutputTemplateVariation()
 			}
 
 			var result contentSummary
@@ -684,10 +684,9 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
 		if err := cp.initRenderHooks(); err != nil {
 			return nil, err
 		}
-		f := cp.po.f
 		po := cp.po
 		p := po.p
-		ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, p, f, false)
+		ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false)
 		if err != nil {
 			return nil, err
 		}
@@ -701,16 +700,14 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
 
 			if p.s.conf.Internal.Watch {
 				for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
-					for _, templ := range s.templs {
-						cp.trackDependency(templ.(identity.IdentityProvider))
-					}
+					cp.trackDependency(s.templ)
 				}
 			}
 
 			// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
 			cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
 			if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
-				cp.po.p.pageOutputTemplateVariationsState.Add(1)
+				cp.po.p.incrPageOutputTemplateVariation()
 			}
 		}
 
@@ -723,7 +720,7 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
 		}
 
 		if hasVariants {
-			p.pageOutputTemplateVariationsState.Add(1)
+			p.incrPageOutputTemplateVariation()
 		}
 
 		isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
@@ -980,7 +977,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
 			return "", err
 		}
 
-		placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
+		placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true)
 		if err != nil {
 			return "", err
 		}
@@ -990,7 +987,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
 			return "", err
 		}
 		if hasVariants {
-			pco.po.p.pageOutputTemplateVariationsState.Add(1)
+			pco.po.p.incrPageOutputTemplateVariation()
 		}
 		b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
 		if err != nil {
@@ -1028,7 +1025,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
 				return "", err
 			}
 			if hasShortcodeVariants {
-				pco.po.p.pageOutputTemplateVariationsState.Add(1)
+				pco.po.p.incrPageOutputTemplateVariation()
 			}
 		}
 
@@ -1110,7 +1107,7 @@ func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTM
 	}
 
 	if hasVariants {
-		pco.po.p.pageOutputTemplateVariationsState.Add(1)
+		pco.po.p.incrPageOutputTemplateVariation()
 	}
 
 	if cb != nil {
diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
index e44224bae9d..9516c482a53 100644
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -72,8 +72,11 @@ type pageMeta struct {
 
 // Prepare for a rebuild of the data passed in from front matter.
 func (m *pageMeta) setMetaPostPrepareRebuild() {
-	params := xmaps.Clone[map[string]any](m.paramsOriginal)
+	params := xmaps.Clone(m.paramsOriginal)
 	m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
+		Kind:   m.pageConfig.Kind,
+		Lang:   m.pageConfig.Lang,
+		Path:   m.pageConfig.Path,
 		Params: params,
 	}
 	m.pageMetaFrontMatter = pageMetaFrontMatter{}
@@ -108,10 +111,10 @@ func (p *pageMeta) Aliases() []string {
 }
 
 func (p *pageMeta) BundleType() string {
-	switch p.pathInfo.BundleType() {
-	case paths.PathTypeLeaf:
+	switch p.pathInfo.Type() {
+	case paths.TypeLeaf:
 		return "leaf"
-	case paths.PathTypeBranch:
+	case paths.TypeBranch:
 		return "branch"
 	default:
 		return ""
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index 2915c6b8a5f..3add014088e 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -19,23 +19,21 @@ import (
 	"errors"
 	"fmt"
 	"html/template"
-	"strings"
 	"sync"
 	"sync/atomic"
 
 	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/common/text"
 	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 	"github.com/spf13/cast"
 
 	"github.com/gohugoio/hugo/markup/converter/hooks"
-	"github.com/gohugoio/hugo/markup/highlight/chromalexers"
 	"github.com/gohugoio/hugo/markup/tableofcontents"
 
 	"github.com/gohugoio/hugo/markup/converter"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
-	"github.com/gohugoio/hugo/tpl"
 
 	"github.com/gohugoio/hugo/output"
 	"github.com/gohugoio/hugo/resources/page"
@@ -120,9 +118,9 @@ func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (tem
 	}
 
 	// Make sure to send the *pageState and not the *pageContentOutput to the template.
-	res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p)
+	res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
 	if err != nil {
-		return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
+		return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Template.Name(), err))
 	}
 	return template.HTML(res), nil
 }
@@ -274,103 +272,100 @@ func (pco *pageContentOutput) initRenderHooks() error {
 				return r
 			}
 
-			layoutDescriptor := pco.po.p.getLayoutDescriptor()
-			layoutDescriptor.RenderingHook = true
-			layoutDescriptor.LayoutOverride = false
-			layoutDescriptor.Layout = ""
+			// Inherit the descriptor from the page/current output format.
+			// This allows for fine-grained control of the template used for
+			// rendering of e.g. links.
+			base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
 
 			switch tp {
 			case hooks.LinkRendererType:
-				layoutDescriptor.Kind = "render-link"
+				layoutDescriptor.Variant1 = "link"
 			case hooks.ImageRendererType:
-				layoutDescriptor.Kind = "render-image"
+				layoutDescriptor.Variant1 = "image"
 			case hooks.HeadingRendererType:
-				layoutDescriptor.Kind = "render-heading"
+				layoutDescriptor.Variant1 = "heading"
 			case hooks.PassthroughRendererType:
-				layoutDescriptor.Kind = "render-passthrough"
+				layoutDescriptor.Variant1 = "passthrough"
 				if id != nil {
-					layoutDescriptor.KindVariants = id.(string)
+					layoutDescriptor.Variant2 = id.(string)
 				}
 			case hooks.BlockquoteRendererType:
-				layoutDescriptor.Kind = "render-blockquote"
+				layoutDescriptor.Variant1 = "blockquote"
 				if id != nil {
-					layoutDescriptor.KindVariants = id.(string)
+					layoutDescriptor.Variant2 = id.(string)
 				}
 			case hooks.TableRendererType:
-				layoutDescriptor.Kind = "render-table"
+				layoutDescriptor.Variant1 = "table"
 			case hooks.CodeBlockRendererType:
-				layoutDescriptor.Kind = "render-codeblock"
+				layoutDescriptor.Variant1 = "codeblock"
 				if id != nil {
-					lang := id.(string)
-					lexer := chromalexers.Get(lang)
-					if lexer != nil {
-						layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
-					} else {
-						layoutDescriptor.KindVariants = lang
-					}
+					layoutDescriptor.Variant2 = id.(string)
 				}
 			}
 
-			getHookTemplate := func(f output.Format) (tpl.Template, bool) {
-				templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
-				if err != nil {
-					panic(err)
+			renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
+			var ignoreInternal bool
+			switch layoutDescriptor.Variant1 {
+			case "link":
+				ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
+			case "image":
+				ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
+			}
+
+			candidates := pco.po.p.s.renderFormats
+			var numCandidatesFound int
+			consider := func(candidate *tplimpl.TemplInfo) bool {
+				if layoutDescriptor.Variant1 != candidate.D.Variant1 {
+					return false
 				}
-				if found {
-					if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
-
-						renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
-
-						switch templ.Name() {
-						case "_default/_markup/render-link.html":
-							if !renderHookConfig.Link.IsEnableDefault() {
-								return nil, false
-							}
-						case "_default/_markup/render-image.html":
-							if !renderHookConfig.Image.IsEnableDefault() {
-								return nil, false
-							}
-						}
-					}
+
+				if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
+					return false
 				}
-				return templ, found
-			}
 
-			templ, found1 := getHookTemplate(pco.po.f)
-			if !found1 || pco.po.p.reusePageOutputContent() {
-				defaultOutputFormat := pco.po.p.s.conf.C.DefaultOutputFormat
+				if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded {
+					// Don't consider the internal hook templates.
+					return false
+				}
 
-				candidates := pco.po.p.s.renderFormats
+				if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 {
+					return true
+				}
 
-				// Some hooks may only be available in HTML, and if
-				// this site is configured to not have HTML output, we need to
-				// make sure we have a fallback. This should be very rare.
-				if pco.po.f.MediaType.FirstSuffix.Suffix != "html" {
-					if _, found := candidates.GetBySuffix("html"); !found {
-						candidates = append(candidates, output.HTMLFormat)
-					}
+				if candidate.D.OutputFormat == "" {
+					numCandidatesFound++
+				} else if _, found := candidates.GetByName(candidate.D.OutputFormat); found {
+					numCandidatesFound++
 				}
 
-				// Check if some of the other output formats would give a different template.
-				for _, f := range candidates {
-					if f.Name == pco.po.f.Name {
-						continue
-					}
-					templ2, found2 := getHookTemplate(f)
-
-					if found2 {
-						if !found1 && f.Name == defaultOutputFormat.Name {
-							templ = templ2
-							found1 = true
-							break
-						}
-
-						if templ != templ2 {
-							pco.po.p.pageOutputTemplateVariationsState.Add(1)
-							break
-						}
-					}
+				return true
+			}
+
+			getHookTemplate := func() (*tplimpl.TemplInfo, bool) {
+				q := tplimpl.TemplateQuery{
+					Path:     base,
+					Category: tplimpl.CategoryMarkup,
+					Desc:     layoutDescriptor,
+					Consider: consider,
 				}
+
+				v := pco.po.p.s.TemplateStore.LookupPagesLayout(q)
+				return v, v != nil
+			}
+
+			templ, found1 := getHookTemplate()
+			if found1 && templ == nil {
+				panic("found1 is true, but templ is nil")
+			}
+
+			if !found1 && layoutDescriptor.OutputFormat == pco.po.p.s.conf.DefaultOutputFormat {
+				numCandidatesFound++
+			}
+
+			if numCandidatesFound > 1 {
+				// More than one output format candidate found for this hook temoplate,
+				// so we cannot reuse the same rendered content.
+				pco.po.p.incrPageOutputTemplateVariation()
 			}
 
 			if !found1 {
@@ -384,7 +379,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
 			}
 
 			r := hookRendererTemplate{
-				templateHandler: pco.po.p.s.Tmpl(),
+				templateHandler: pco.po.p.s.GetTemplateStore(),
 				templ:           templ,
 				resolvePosition: resolvePosition,
 			}
@@ -488,7 +483,7 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths {
 	return t.paths
 }
 
-func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
+func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) {
 	b := bp.GetBuffer()
 	defer bp.PutBuffer(b)
 	if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {
diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go
index f175895c42d..50900e5853c 100644
--- a/hugolib/pages_capture.go
+++ b/hugolib/pages_capture.go
@@ -195,7 +195,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
 						return id.p.Dir() == fim.Meta().PathInfo.Dir()
 					}
 
-					if fim.Meta().PathInfo.IsLeafBundle() && id.p.BundleType() == paths.PathTypeContentSingle {
+					if fim.Meta().PathInfo.IsLeafBundle() && id.p.Type() == paths.TypeContentSingle {
 						return id.p.Dir() == fim.Meta().PathInfo.Dir()
 					}
 
@@ -314,7 +314,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
 			return nil, filepath.SkipDir
 		}
 
-		seen := map[hstrings.Tuple]bool{}
+		seen := map[hstrings.Strings2]hugofs.FileMetaInfo{}
 		for _, fi := range readdir {
 			if fi.IsDir() {
 				continue
@@ -327,11 +327,14 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
 			// These would eventually have been filtered out as duplicates when
 			// inserting them into the document store,
 			// but doing it here will preserve a consistent ordering.
-			baseLang := hstrings.Tuple{First: pi.Base(), Second: meta.Lang}
-			if seen[baseLang] {
+			baseLang := hstrings.Strings2{pi.Base(), meta.Lang}
+			if fi2, ok := seen[baseLang]; ok {
+				if c.h.Configs.Base.PrintPathWarnings && !c.h.isRebuild() {
+					c.logger.Warnf("Duplicate content path: %q file: %q file: %q", pi.Base(), fi2.Meta().Filename, meta.Filename)
+				}
 				continue
 			}
-			seen[baseLang] = true
+			seen[baseLang] = fi
 
 			if pi == nil {
 				panic(fmt.Sprintf("no path info for %q", meta.Filename))
@@ -374,7 +377,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
 
 func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPath string, readdir []hugofs.FileMetaInfo) error {
 	bundlePi := bundle.Meta().PathInfo
-	seen := map[hstrings.Tuple]bool{}
+	seen := map[hstrings.Strings2]bool{}
 
 	walk := func(path string, info hugofs.FileMetaInfo) error {
 		if info.IsDir() {
@@ -396,7 +399,7 @@ func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPat
 		// These would eventually have been filtered out as duplicates when
 		// inserting them into the document store,
 		// but doing it here will preserve a consistent ordering.
-		baseLang := hstrings.Tuple{First: pi.Base(), Second: info.Meta().Lang}
+		baseLang := hstrings.Strings2{pi.Base(), info.Meta().Lang}
 		if seen[baseLang] {
 			return nil
 		}
diff --git a/hugolib/pagesfromdata/pagesfromgotmpl.go b/hugolib/pagesfromdata/pagesfromgotmpl.go
index 58bb72978ac..a704b39ee68 100644
--- a/hugolib/pagesfromdata/pagesfromgotmpl.go
+++ b/hugolib/pagesfromdata/pagesfromgotmpl.go
@@ -29,6 +29,7 @@ import (
 	"github.com/gohugoio/hugo/resources/page/pagemeta"
 	"github.com/gohugoio/hugo/resources/resource"
 	"github.com/gohugoio/hugo/tpl"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 	"github.com/mitchellh/mapstructure"
 	"github.com/spf13/cast"
 )
@@ -167,8 +168,7 @@ type PagesFromTemplateOptions struct {
 }
 
 type PagesFromTemplateDeps struct {
-	TmplFinder tpl.TemplateParseFinder
-	TmplExec   tpl.TemplateExecutor
+	TemplateStore *tplimpl.TemplateStore
 }
 
 var _ resource.Staler = (*PagesFromTemplate)(nil)
@@ -303,7 +303,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
 	}
 	defer f.Close()
 
-	tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
+	tmpl, err := p.TemplateStore.TextParse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
 	if err != nil {
 		return BuildInfo{}, err
 	}
@@ -314,7 +314,7 @@ func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
 
 	ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
 
-	if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
+	if err := p.TemplateStore.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
 		return BuildInfo{}, err
 	}
 
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index 9a2bb7aeb09..7a23b15d2f2 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 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.
@@ -29,6 +29,7 @@ import (
 
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/types"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 
 	"github.com/gohugoio/hugo/parser/pageparser"
 	"github.com/gohugoio/hugo/resources/page"
@@ -36,7 +37,6 @@ import (
 	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/common/text"
 	"github.com/gohugoio/hugo/common/urls"
-	"github.com/gohugoio/hugo/output"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
 	"github.com/gohugoio/hugo/tpl"
@@ -205,8 +205,7 @@ type shortcode struct {
 
 	indentation string // indentation from source.
 
-	info   tpl.Info       // One of the output formats (arbitrary)
-	templs []tpl.Template // All output formats
+	templ *tplimpl.TemplInfo
 
 	// If set, the rendered shortcode is sent as part of the surrounding content
 	// to Goldmark and similar.
@@ -230,16 +229,15 @@ func (s shortcode) insertPlaceholder() bool {
 }
 
 func (s shortcode) needsInner() bool {
-	return s.info != nil && s.info.ParseInfo().IsInner
+	return s.templ != nil && s.templ.ParseInfo.IsInner
 }
 
 func (s shortcode) configVersion() int {
-	if s.info == nil {
+	if s.templ == nil {
 		// Not set for inline shortcodes.
 		return 2
 	}
-
-	return s.info.ParseInfo().Config.Version
+	return s.templ.ParseInfo.Config.Version
 }
 
 func (s shortcode) innerString() string {
@@ -315,12 +313,12 @@ func prepareShortcode(
 	ctx context.Context,
 	level int,
 	s *Site,
-	tplVariants tpl.TemplateVariants,
 	sc *shortcode,
 	parent *ShortcodeWithPage,
-	p *pageState,
+	po *pageOutput,
 	isRenderString bool,
 ) (shortcodeRenderer, error) {
+	p := po.p
 	toParseErr := func(err error) error {
 		source := p.m.content.mustSource()
 		return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
@@ -333,7 +331,7 @@ func prepareShortcode(
 			// parsed and rendered by Goldmark.
 			ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
 		}
-		r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
+		r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString)
 		if err != nil {
 			return nil, false, toParseErr(err)
 		}
@@ -352,30 +350,29 @@ func doRenderShortcode(
 	ctx context.Context,
 	level int,
 	s *Site,
-	tplVariants tpl.TemplateVariants,
 	sc *shortcode,
 	parent *ShortcodeWithPage,
-	p *pageState,
+	po *pageOutput,
 	isRenderString bool,
 ) (shortcodeRenderer, error) {
-	var tmpl tpl.Template
+	var tmpl *tplimpl.TemplInfo
+	p := po.p
 
 	// Tracks whether this shortcode or any of its children has template variations
 	// in other languages or output formats. We are currently only interested in
-	// the output formats, so we may get some false positives -- we
-	// should improve on that.
+	// the output formats.
 	var hasVariants bool
 
 	if sc.isInline {
 		if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
 			return zeroShortcode, nil
 		}
-		templName := path.Join("_inline_shortcode", p.Path(), sc.name)
+		templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
 		if sc.isClosing {
 			templStr := sc.innerString()
 
 			var err error
-			tmpl, err = s.TextTmpl().Parse(templName, templStr)
+			tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
 			if err != nil {
 				if isRenderString {
 					return zeroShortcode, p.wrapError(err)
@@ -389,21 +386,32 @@ func doRenderShortcode(
 
 		} else {
 			// Re-use of shortcode defined earlier in the same page.
-			var found bool
-			tmpl, found = s.TextTmpl().Lookup(templName)
-			if !found {
+			tmpl = s.TemplateStore.TextLookup(templatePath)
+			if tmpl == nil {
 				return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
 			}
 		}
-		tmpl = tpl.AddIdentity(tmpl)
 	} else {
-		var found, more bool
-		tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
-		if !found {
+		ofCount := map[string]int{}
+		include := func(match *tplimpl.TemplInfo) bool {
+			ofCount[match.D.OutputFormat]++
+			return true
+		}
+		base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
+		q := tplimpl.TemplateQuery{
+			Path:     base,
+			Name:     sc.name,
+			Category: tplimpl.CategoryShortcode,
+			Desc:     layoutDescriptor,
+			Consider: include,
+		}
+		v := s.TemplateStore.LookupShortcode(q)
+		if v == nil {
 			s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
 			return zeroShortcode, nil
 		}
-		hasVariants = hasVariants || more
+		tmpl = v
+		hasVariants = hasVariants || len(ofCount) > 1
 	}
 
 	data := &ShortcodeWithPage{
@@ -427,7 +435,7 @@ func doRenderShortcode(
 			case string:
 				inner += innerData
 			case *shortcode:
-				s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p, isRenderString)
+				s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString)
 				if err != nil {
 					return zeroShortcode, err
 				}
@@ -484,7 +492,7 @@ func doRenderShortcode(
 
 	}
 
-	result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
+	result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data)
 
 	if err != nil && sc.isInline {
 		fe := herrors.NewFileErrorFromName(err, p.File().Filename())
@@ -534,16 +542,11 @@ func (s *shortcodeHandler) hasName(name string) bool {
 	return ok
 }
 
-func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format, isRenderString bool) (map[string]shortcodeRenderer, error) {
+func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) {
 	rendered := make(map[string]shortcodeRenderer)
 
-	tplVariants := tpl.TemplateVariants{
-		Language:     p.Language().Lang,
-		OutputFormat: f,
-	}
-
 	for _, v := range s.shortcodes {
-		s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p, isRenderString)
+		s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString)
 		if err != nil {
 			return nil, err
 		}
@@ -636,7 +639,7 @@ Loop:
 			// we trust the template on this:
 			// if there's no inner, we're done
 			if !sc.isInline {
-				if !sc.info.ParseInfo().IsInner {
+				if !sc.templ.ParseInfo.IsInner {
 					return sc, nil
 				}
 			}
@@ -672,14 +675,19 @@ Loop:
 
 			sc.name = currItem.ValStr(source)
 
-			// Used to check if the template expects inner content.
-			templs := s.s.Tmpl().LookupVariants(sc.name)
-			if templs == nil {
+			// Used to check if the template expects inner content,
+			// so just pick one arbitrarily with the same name.
+			q := tplimpl.TemplateQuery{
+				Path:     "",
+				Name:     sc.name,
+				Category: tplimpl.CategoryShortcode,
+				Consider: nil,
+			}
+			templ := s.s.TemplateStore.LookupShortcode(q)
+			if templ == nil {
 				return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
 			}
-
-			sc.info = templs[0].(tpl.Info)
-			sc.templs = templs
+			sc.templ = templ
 		case currItem.IsInlineShortcodeName():
 			sc.name = currItem.ValStr(source)
 			sc.isInline = true
@@ -778,7 +786,7 @@ func expandShortcodeTokens(
 	return source, nil
 }
 
-func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
+func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) {
 	buffer := bp.GetBuffer()
 	defer bp.PutBuffer(buffer)
 
diff --git a/hugolib/site.go b/hugolib/site.go
index dab23d67061..ebf06eee392 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -47,7 +47,13 @@ import (
 	"github.com/gohugoio/hugo/langs/i18n"
 	"github.com/gohugoio/hugo/modules"
 	"github.com/gohugoio/hugo/resources"
+
 	"github.com/gohugoio/hugo/tpl/tplimpl"
+	"github.com/gohugoio/hugo/tpl/tplimplinit"
+	xmaps "golang.org/x/exp/maps"
+
+	// Loads the template funcs namespaces.
+
 	"golang.org/x/text/unicode/norm"
 
 	"github.com/gohugoio/hugo/common/paths"
@@ -188,8 +194,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
 		BuildState: &deps.BuildState{
 			OnSignalRebuild: onSignalRebuild,
 		},
+		Counters:            &deps.Counters{},
 		MemCache:            memCache,
-		TemplateProvider:    tplimpl.DefaultTemplateProvider,
 		TranslationProvider: i18n.NewTranslationProvider(),
 		WasmDispatchers: warpc.AllDispatchers(
 			warpc.Options{
@@ -385,6 +391,34 @@ func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []
 	var prototype *deps.Deps
 	for i, s := range sites {
 		s.h = h
+		// The template store needs to be initialized after the h container is set on s.
+		if i == 0 {
+			templateStore, err := tplimpl.NewStore(
+				tplimpl.StoreOptions{
+					Fs:                     s.BaseFs.Layouts.Fs,
+					DefaultContentLanguage: s.Conf.DefaultContentLanguage(),
+					Watching:               s.Conf.Watching(),
+					PathParser:             s.Conf.PathParser(),
+					Metrics:                d.Metrics,
+					OutputFormats:          s.conf.OutputFormats.Config,
+					MediaTypes:             s.conf.MediaTypes.Config,
+					DefaultOutputFormat:    s.conf.DefaultOutputFormat,
+					TaxonomySingularPlural: s.conf.Taxonomies,
+				}, tplimpl.SiteOptions{
+					Site:          s,
+					TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+				})
+			if err != nil {
+				return nil, err
+			}
+			s.Deps.TemplateStore = templateStore
+		} else {
+			s.Deps.TemplateStore = prototype.TemplateStore.WithSiteOpts(
+				tplimpl.SiteOptions{
+					Site:          s,
+					TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+				})
+		}
 		if err := s.Deps.Compile(prototype); err != nil {
 			return nil, err
 		}
@@ -464,7 +498,10 @@ func (s *Site) MainSections() []string {
 
 // Returns a struct with some information about the build.
 func (s *Site) Hugo() hugo.HugoInfo {
-	if s.h == nil || s.h.hugoInfo.Environment == "" {
+	if s.h == nil {
+		panic("site: hugo: h not initialized")
+	}
+	if s.h.hugoInfo.Environment == "" {
 		panic("site: hugo: hugoInfo not initialized")
 	}
 	return s.h.hugoInfo
@@ -797,7 +834,7 @@ func (s *Site) initRenderFormats() {
 	s.renderFormats = formats
 }
 
-func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
+func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler {
 	return s.relatedDocsHandler
 }
 
@@ -923,19 +960,24 @@ type WhatChanged struct {
 	mu sync.Mutex
 
 	needsPagesAssembly bool
-	identitySet        identity.Identities
+
+	ids map[identity.Identity]bool
+}
+
+func (w *WhatChanged) init() {
+	if w.ids == nil {
+		w.ids = make(map[identity.Identity]bool)
+	}
 }
 
 func (w *WhatChanged) Add(ids ...identity.Identity) {
 	w.mu.Lock()
 	defer w.mu.Unlock()
 
-	if w.identitySet == nil {
-		w.identitySet = make(identity.Identities)
-	}
+	w.init()
 
 	for _, id := range ids {
-		w.identitySet[id] = true
+		w.ids[id] = true
 	}
 }
 
@@ -946,20 +988,20 @@ func (w *WhatChanged) Clear() {
 }
 
 func (w *WhatChanged) clear() {
-	w.identitySet = identity.Identities{}
+	w.ids = nil
 }
 
 func (w *WhatChanged) Changes() []identity.Identity {
-	if w == nil || w.identitySet == nil {
+	if w == nil || w.ids == nil {
 		return nil
 	}
-	return w.identitySet.AsSlice()
+	return xmaps.Keys(w.ids)
 }
 
 func (w *WhatChanged) Drain() []identity.Identity {
 	w.mu.Lock()
 	defer w.mu.Unlock()
-	ids := w.identitySet.AsSlice()
+	ids := w.Changes()
 	w.clear()
 	return ids
 }
@@ -1394,7 +1436,7 @@ const (
 	pageDependencyScopeGlobal
 )
 
-func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ tpl.Template) error {
+func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
 	s.h.buildCounters.pageRenderCounter.Add(1)
 	renderBuffer := bp.GetBuffer()
 	defer bp.PutBuffer(renderBuffer)
@@ -1453,8 +1495,8 @@ var infoOnMissingLayout = map[string]bool{
 // hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
 // where ITEM is the thing being hooked.
 type hookRendererTemplate struct {
-	templateHandler tpl.TemplateHandler
-	templ           tpl.Template
+	templateHandler *tplimpl.TemplateStore
+	templ           *tplimpl.TemplInfo
 	resolvePosition func(ctx any) text.Position
 }
 
@@ -1490,7 +1532,7 @@ func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
 	return false
 }
 
-func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
+func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ *tplimpl.TemplInfo) (err error) {
 	if templ == nil {
 		s.logMissingLayout(name, "", "", outputFormat)
 		return nil
@@ -1500,7 +1542,7 @@ func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string,
 		panic("nil context")
 	}
 
-	if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
+	if err = s.GetTemplateStore().ExecuteWithContext(ctx, templ, w, d); err != nil {
 		filename := name
 		if p, ok := d.(*pageState); ok {
 			filename = p.String()
diff --git a/hugolib/site_output.go b/hugolib/site_output.go
index 47778b8b037..3438ea9f702 100644
--- a/hugolib/site_output.go
+++ b/hugolib/site_output.go
@@ -27,6 +27,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
 	htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
 	robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
 	sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
+	httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name)
 
 	defaultListTypes := output.Formats{htmlOut}
 	if rssFound {
@@ -42,7 +43,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
 		// Below are for consistency. They are currently not used during rendering.
 		kinds.KindSitemap:   {sitemapOut},
 		kinds.KindRobotsTXT: {robotsOut},
-		kinds.KindStatus404: {htmlOut},
+		kinds.KindStatus404: {httpStatus404Out},
 	}
 
 	// May be disabled
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index c88036f2656..ed22d425861 100644
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -23,9 +23,9 @@ import (
 	"github.com/bep/logg"
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/hugolib/doctree"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 
 	"github.com/gohugoio/hugo/config"
-	"github.com/gohugoio/hugo/tpl"
 
 	"github.com/gohugoio/hugo/resources/kinds"
 	"github.com/gohugoio/hugo/resources/page"
@@ -57,7 +57,7 @@ func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool {
 		return s.outIdx == 0
 	}
 
-	if kind == kinds.KindStatus404 {
+	if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
 		// 1 for all output formats
 		return s.outIdx == 0
 	}
@@ -168,7 +168,7 @@ func pageRenderer(
 
 		s.Log.Trace(
 			func() string {
-				return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath)
+				return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Template.Name(), targetPath)
 			},
 		)
 
@@ -225,7 +225,7 @@ func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) {
 }
 
 // renderPaginator must be run after the owning Page has been rendered.
-func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
+func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
 	paginatePath := s.Conf.Pagination().Path
 
 	d := p.targetPathDescriptor
diff --git a/identity/identity.go b/identity/identity.go
index 53169dfe1bd..c78ed0fdde0 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -509,6 +509,10 @@ func probablyEq(a, b Identity) bool {
 		return true
 	}
 
+	if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) {
+		return true
+	}
+
 	if a2, ok := a.(IsProbablyDependentProvider); ok {
 		return a2.IsProbablyDependent(b)
 	}
diff --git a/internal/js/esbuild/batch.go b/internal/js/esbuild/batch.go
index c5394ac0a9c..aa50cf2c1fe 100644
--- a/internal/js/esbuild/batch.go
+++ b/internal/js/esbuild/batch.go
@@ -43,7 +43,7 @@ import (
 	"github.com/gohugoio/hugo/resources"
 	"github.com/gohugoio/hugo/resources/resource"
 	"github.com/gohugoio/hugo/resources/resource_factories/create"
-	"github.com/gohugoio/hugo/tpl"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 	"github.com/mitchellh/mapstructure"
 	"github.com/spf13/cast"
 )
@@ -192,7 +192,7 @@ type BatcherClient struct {
 	d *deps.Deps
 
 	once           sync.Once
-	runnerTemplate tpl.Template
+	runnerTemplate *tplimpl.TemplInfo
 
 	createClient *create.Client
 	buildClient  *BuildClient
@@ -208,7 +208,7 @@ func (c *BatcherClient) New(id string) (js.Batcher, error) {
 	c.once.Do(func() {
 		// We should fix the initialization order here (or use the Go template package directly), but we need to wait
 		// for the Hugo templates to be ready.
-		tmpl, err := c.d.TextTmpl().Parse("batch-esm-runner", runnerTemplateStr)
+		tmpl, err := c.d.TemplateStore.TextParse("batch-esm-runner", runnerTemplateStr)
 		if err != nil {
 			initErr = err
 			return
@@ -287,7 +287,7 @@ func (c *BatcherClient) Store() *maps.Cache[string, js.Batcher] {
 func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
 	var buf bytes.Buffer
 
-	if err := c.d.Tmpl().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
+	if err := c.d.GetTemplateStore().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
 		return nil, "", err
 	}
 
diff --git a/media/builtin.go b/media/builtin.go
index ee35b7d087d..41d1ed655a8 100644
--- a/media/builtin.go
+++ b/media/builtin.go
@@ -5,6 +5,7 @@ type BuiltinTypes struct {
 	CSSType        Type
 	SCSSType       Type
 	SASSType       Type
+	GotmplType     Type
 	CSVType        Type
 	HTMLType       Type
 	JavascriptType Type
@@ -60,6 +61,7 @@ var Builtin = BuiltinTypes{
 	CSSType:        Type{Type: "text/css"},
 	SCSSType:       Type{Type: "text/x-scss"},
 	SASSType:       Type{Type: "text/x-sass"},
+	GotmplType:     Type{Type: "text/x-gotmpl"},
 	CSVType:        Type{Type: "text/csv"},
 	HTMLType:       Type{Type: "text/html"},
 	JavascriptType: Type{Type: "text/javascript"},
@@ -121,6 +123,7 @@ var defaultMediaTypesConfig = map[string]any{
 	"text/typescript": map[string]any{"suffixes": []string{"ts"}},
 	"text/tsx":        map[string]any{"suffixes": []string{"tsx"}},
 	"text/jsx":        map[string]any{"suffixes": []string{"jsx"}},
+	"text/x-gotmpl":   map[string]any{"suffixes": []string{"gotmpl"}},
 
 	"application/json":          map[string]any{"suffixes": []string{"json"}},
 	"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},
diff --git a/media/config.go b/media/config.go
index 394159d0400..6d3687a4fa4 100644
--- a/media/config.go
+++ b/media/config.go
@@ -17,6 +17,7 @@ import (
 	"fmt"
 	"path/filepath"
 	"reflect"
+	"slices"
 	"sort"
 	"strings"
 
@@ -26,7 +27,6 @@ import (
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/spf13/cast"
-	"slices"
 )
 
 // DefaultTypes is the default media types supported by Hugo.
@@ -271,4 +271,7 @@ var DefaultPathParser = &paths.PathParser{
 	IsContentExt: func(ext string) bool {
 		panic("not supported")
 	},
+	IsOutputFormat: func(name, ext string) bool {
+		panic("DefaultPathParser: not supported")
+	},
 }
diff --git a/media/mediaType.go b/media/mediaType.go
index 97b10879c07..b3b61544466 100644
--- a/media/mediaType.go
+++ b/media/mediaType.go
@@ -282,7 +282,7 @@ func (t Types) BySuffix(suffix string) []Type {
 	suffix = t.normalizeSuffix(suffix)
 	var types []Type
 	for _, tt := range t {
-		if tt.hasSuffix(suffix) {
+		if tt.HasSuffix(suffix) {
 			types = append(types, tt)
 		}
 	}
@@ -293,7 +293,7 @@ func (t Types) BySuffix(suffix string) []Type {
 func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
 	suffix = t.normalizeSuffix(suffix)
 	for _, tt := range t {
-		if tt.hasSuffix(suffix) {
+		if tt.HasSuffix(suffix) {
 			return tt, SuffixInfo{
 				FullSuffix: tt.Delimiter + suffix,
 				Suffix:     suffix,
@@ -310,7 +310,7 @@ func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
 func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
 	suffix = t.normalizeSuffix(suffix)
 	for _, tt := range t {
-		if tt.hasSuffix(suffix) {
+		if tt.HasSuffix(suffix) {
 			if found {
 				// ambiguous
 				found = false
@@ -330,14 +330,14 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
 func (t Types) IsTextSuffix(suffix string) bool {
 	suffix = t.normalizeSuffix(suffix)
 	for _, tt := range t {
-		if tt.hasSuffix(suffix) {
+		if tt.HasSuffix(suffix) {
 			return tt.IsText()
 		}
 	}
 	return false
 }
 
-func (m Type) hasSuffix(suffix string) bool {
+func (m Type) HasSuffix(suffix string) bool {
 	return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
 }
 
diff --git a/output/docshelper.go b/output/docshelper.go
index 0f2a0eff6dc..387c011ad7b 100644
--- a/output/docshelper.go
+++ b/output/docshelper.go
@@ -1,12 +1,10 @@
 package output
 
 import (
-	"strings"
 
 	//	"fmt"
 
 	"github.com/gohugoio/hugo/docshelper"
-	"github.com/gohugoio/hugo/output/layouts"
 )
 
 // This is is just some helpers used to create some JSON used in the Hugo docs.
@@ -14,90 +12,12 @@ func init() {
 	docsProvider := func() docshelper.DocProvider {
 		return docshelper.DocProvider{
 			"output": map[string]any{
-				"layouts": createLayoutExamples(),
+				// TODO(bep), maybe revisit this later, but I hope this isn't needed.
+				// "layouts": createLayoutExamples(),
+				"layouts": map[string]any{},
 			},
 		}
 	}
 
 	docshelper.AddDocProviderFunc(docsProvider)
 }
-
-func createLayoutExamples() any {
-	type Example struct {
-		Example      string
-		Kind         string
-		OutputFormat string
-		Suffix       string
-		Layouts      []string `json:"Template Lookup Order"`
-	}
-
-	var (
-		basicExamples []Example
-		demoLayout    = "demolayout"
-		demoType      = "demotype"
-	)
-
-	for _, example := range []struct {
-		name string
-		d    layouts.LayoutDescriptor
-	}{
-		// Taxonomy layouts.LayoutDescriptor={categories category taxonomy en  false Type Section
-		{"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
-		{"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
-		{"Single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
-		{"Base template for single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
-		{"AMP single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
-		{"AMP single page in \"posts\" section, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
-		// Typeless pages get "page" as type
-		{"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
-		{"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
-		{"Home page with type set to \"demotype\"", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
-		{"Base template for home page with type set to \"demotype\"", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
-		{"Home page with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
-		{"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
-		{"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}},
-		{"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "xml"}},
-
-		{"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
-		{"Section list for \"posts\" with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
-		{"Section list for \"posts\" with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}},
-		{"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "xml"}},
-
-		{"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
-		{"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
-
-		{"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
-		{"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
-	} {
-
-		l := layouts.NewLayoutHandler()
-		layouts, _ := l.For(example.d)
-
-		basicExamples = append(basicExamples, Example{
-			Example:      example.name,
-			Kind:         example.d.Kind,
-			OutputFormat: example.d.OutputFormatName,
-			Suffix:       example.d.Suffix,
-			Layouts:      makeLayoutsPresentable(layouts),
-		})
-	}
-
-	return basicExamples
-}
-
-func makeLayoutsPresentable(l []string) []string {
-	var filtered []string
-	for _, ll := range l {
-		if strings.Contains(ll, "page/") {
-			// This is a valid lookup, but it's more confusing than useful.
-			continue
-		}
-		ll = "layouts/" + strings.TrimPrefix(ll, "_text/")
-
-		if !strings.Contains(ll, "indexes") {
-			filtered = append(filtered, ll)
-		}
-	}
-
-	return filtered
-}
diff --git a/output/layouts/layout.go b/output/layouts/layout.go
deleted file mode 100644
index 79f718dda13..00000000000
--- a/output/layouts/layout.go
+++ /dev/null
@@ -1,336 +0,0 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// 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 layouts
-
-import (
-	"strings"
-	"sync"
-)
-
-// These may be used as content sections with potential conflicts. Avoid that.
-var reservedSections = map[string]bool{
-	"shortcodes": true,
-	"partials":   true,
-}
-
-// LayoutDescriptor describes how a layout should be chosen. This is
-// typically built from a Page.
-type LayoutDescriptor struct {
-	Type    string
-	Section string
-
-	// E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
-	Kind string
-
-	// Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
-	KindVariants string
-
-	Lang   string
-	Layout string
-	// LayoutOverride indicates what we should only look for the above layout.
-	LayoutOverride bool
-
-	// From OutputFormat and MediaType.
-	OutputFormatName string
-	Suffix           string
-
-	RenderingHook bool
-	Baseof        bool
-}
-
-func (d LayoutDescriptor) isList() bool {
-	return !d.RenderingHook && d.Kind != "page" && d.Kind != "404" && d.Kind != "sitemap" && d.Kind != "sitemapindex"
-}
-
-// LayoutHandler calculates the layout template to use to render a given output type.
-type LayoutHandler struct {
-	mu    sync.RWMutex
-	cache map[LayoutDescriptor][]string
-}
-
-// NewLayoutHandler creates a new LayoutHandler.
-func NewLayoutHandler() *LayoutHandler {
-	return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)}
-}
-
-// For returns a layout for the given LayoutDescriptor and options.
-// Layouts are rendered and cached internally.
-func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) {
-	// We will get lots of requests for the same layouts, so avoid recalculations.
-	l.mu.RLock()
-	if cacheVal, found := l.cache[d]; found {
-		l.mu.RUnlock()
-		return cacheVal, nil
-	}
-	l.mu.RUnlock()
-
-	layouts := resolvePageTemplate(d)
-
-	layouts = uniqueStringsReuse(layouts)
-
-	l.mu.Lock()
-	l.cache[d] = layouts
-	l.mu.Unlock()
-
-	return layouts, nil
-}
-
-type layoutBuilder struct {
-	layoutVariations []string
-	typeVariations   []string
-	d                LayoutDescriptor
-	// f                Format
-}
-
-func (l *layoutBuilder) addLayoutVariations(vars ...string) {
-	for _, layoutVar := range vars {
-		if l.d.Baseof && layoutVar != "baseof" {
-			l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
-			continue
-		}
-		if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
-			continue
-		}
-		l.layoutVariations = append(l.layoutVariations, layoutVar)
-	}
-}
-
-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)
-		}
-	}
-}
-
-func (l *layoutBuilder) addSectionType() {
-	if l.d.Section != "" {
-		l.addTypeVariations(l.d.Section)
-	}
-}
-
-func (l *layoutBuilder) addKind() {
-	l.addLayoutVariations(l.d.Kind)
-	l.addTypeVariations(l.d.Kind)
-}
-
-const renderingHookRoot = "/_markup"
-
-func resolvePageTemplate(d LayoutDescriptor) []string {
-	b := &layoutBuilder{d: d}
-
-	if !d.RenderingHook && d.Layout != "" {
-		b.addLayoutVariations(d.Layout)
-	}
-	if d.Type != "" {
-		b.addTypeVariations(d.Type)
-	}
-
-	if d.RenderingHook {
-		if d.KindVariants != "" {
-			// Add the more specific variants first.
-			for _, variant := range strings.Split(d.KindVariants, ",") {
-				b.addLayoutVariations(d.Kind + "-" + variant)
-			}
-		}
-		b.addLayoutVariations(d.Kind)
-		b.addSectionType()
-	}
-
-	switch d.Kind {
-	case "page":
-		b.addLayoutVariations("single")
-		b.addSectionType()
-	case "home":
-		b.addLayoutVariations("index", "home")
-		// Also look in the root
-		b.addTypeVariations("")
-	case "section":
-		if d.Section != "" {
-			b.addLayoutVariations(d.Section)
-		}
-		b.addSectionType()
-		b.addKind()
-	case "term":
-		b.addKind()
-		if d.Section != "" {
-			b.addLayoutVariations(d.Section)
-		}
-		b.addLayoutVariations("taxonomy")
-		b.addTypeVariations("taxonomy")
-		b.addSectionType()
-	case "taxonomy":
-		if d.Section != "" {
-			b.addLayoutVariations(d.Section + ".terms")
-		}
-		b.addSectionType()
-		b.addLayoutVariations("terms")
-		// For legacy reasons this is deliberately put last.
-		b.addKind()
-	case "404":
-		b.addLayoutVariations("404")
-		b.addTypeVariations("")
-	case "robotstxt":
-		b.addLayoutVariations("robots")
-		b.addTypeVariations("")
-	case "sitemap":
-		b.addLayoutVariations("sitemap")
-		b.addTypeVariations("")
-	case "sitemapindex":
-		b.addLayoutVariations("sitemapindex")
-		b.addTypeVariations("")
-	}
-
-	isRSS := d.OutputFormatName == "rss"
-	if !d.RenderingHook && !d.Baseof && isRSS {
-		// The historic and common rss.xml case
-		b.addLayoutVariations("")
-	}
-
-	if d.Baseof || d.Kind != "404" {
-		// Most have _default in their lookup path
-		b.addTypeVariations("_default")
-	}
-
-	if d.isList() {
-		// Add the common list type
-		b.addLayoutVariations("list")
-	}
-
-	if d.Baseof {
-		b.addLayoutVariations("baseof")
-	}
-
-	layouts := b.resolveVariations()
-
-	if !d.RenderingHook && !d.Baseof && isRSS {
-		layouts = append(layouts, "_internal/_default/rss.xml")
-	}
-
-	switch d.Kind {
-	case "robotstxt":
-		layouts = append(layouts, "_internal/_default/robots.txt")
-	case "sitemap":
-		layouts = append(layouts, "_internal/_default/sitemap.xml")
-	case "sitemapindex":
-		layouts = append(layouts, "_internal/_default/sitemapindex.xml")
-	}
-
-	return layouts
-}
-
-func (l *layoutBuilder) resolveVariations() []string {
-	var layouts []string
-
-	var variations []string
-	name := strings.ToLower(l.d.OutputFormatName)
-
-	if l.d.Lang != "" {
-		// We prefer the most specific type before language.
-		variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...)
-	} else {
-		variations = append(variations, name)
-	}
-
-	variations = append(variations, "")
-
-	for _, typeVar := range l.typeVariations {
-		for _, variation := range variations {
-			for _, layoutVar := range l.layoutVariations {
-				if variation == "" && layoutVar == "" {
-					continue
-				}
-
-				s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix)
-				if s != "" {
-					layouts = append(layouts, s)
-				}
-			}
-		}
-	}
-
-	return layouts
-}
-
-// constructLayoutPath constructs a layout path given a type, layout,
-// variations, and extension.  The path constructed follows the pattern of
-// type/layout.variations.extension.  If any value is empty, it will be left out
-// of the path construction.
-//
-// Path construction requires at least 2 of 3 out of layout, variations, and extension.
-// If more than one of those is empty, an empty string is returned.
-func constructLayoutPath(typ, layout, variations, extension string) string {
-	// we already know that layout and variations are not both empty because of
-	// checks in resolveVariants().
-	if extension == "" && (layout == "" || variations == "") {
-		return ""
-	}
-
-	// Commence valid path construction...
-
-	var (
-		p       strings.Builder
-		needDot bool
-	)
-
-	if typ != "" {
-		p.WriteString(typ)
-		p.WriteString("/")
-	}
-
-	if layout != "" {
-		p.WriteString(layout)
-		needDot = true
-	}
-
-	if variations != "" {
-		if needDot {
-			p.WriteString(".")
-		}
-		p.WriteString(variations)
-		needDot = true
-	}
-
-	if extension != "" {
-		if needDot {
-			p.WriteString(".")
-		}
-		p.WriteString(extension)
-	}
-
-	return p.String()
-}
-
-// Inline this here so we can use tinygo to compile a wasm binary of this package.
-func uniqueStringsReuse(s []string) []string {
-	result := s[:0]
-	for i, val := range s {
-		var seen bool
-
-		for j := range i {
-			if s[j] == val {
-				seen = true
-				break
-			}
-		}
-
-		if !seen {
-			result = append(result, val)
-		}
-	}
-	return result
-}
diff --git a/output/outputFormat.go b/output/outputFormat.go
index d249c72b933..8f3716e3ee9 100644
--- a/output/outputFormat.go
+++ b/output/outputFormat.go
@@ -133,6 +133,15 @@ var (
 		Weight: 10,
 	}
 
+	// Alias is the output format used for alias redirects.
+	AliasHTMLFormat = Format{
+		Name:          "alias",
+		MediaType:     media.Builtin.HTMLType,
+		IsHTML:        true,
+		Ugly:          true,
+		Permalinkable: false,
+	}
+
 	MarkdownFormat = Format{
 		Name:        "markdown",
 		MediaType:   media.Builtin.MarkdownType,
@@ -192,8 +201,17 @@ var (
 		Rel:       "sitemap",
 	}
 
-	HTTPStatusHTMLFormat = Format{
-		Name:           "httpstatus",
+	GotmplFormat = Format{
+		Name:           "gotmpl",
+		MediaType:      media.Builtin.GotmplType,
+		IsPlainText:    true,
+		NotAlternative: true,
+	}
+
+	// I'm not sure having a 404 format is a good idea,
+	// for one, we would want to have multiple formats for this.
+	HTTPStatus404HTMLFormat = Format{
+		Name:           "404",
 		MediaType:      media.Builtin.HTMLType,
 		NotAlternative: true,
 		Ugly:           true,
@@ -209,12 +227,16 @@ var DefaultFormats = Formats{
 	CSSFormat,
 	CSVFormat,
 	HTMLFormat,
+	GotmplFormat,
+	HTTPStatus404HTMLFormat,
+	AliasHTMLFormat,
 	JSONFormat,
 	MarkdownFormat,
 	WebAppManifestFormat,
 	RobotsTxtFormat,
 	RSSFormat,
 	SitemapFormat,
+	SitemapIndexFormat,
 }
 
 func init() {
diff --git a/resources/kinds/kinds.go b/resources/kinds/kinds.go
index 2660ec7194f..30bc35e4373 100644
--- a/resources/kinds/kinds.go
+++ b/resources/kinds/kinds.go
@@ -34,6 +34,7 @@ const (
 
 	// The following are (currently) temporary nodes,
 	// i.e. nodes we create just to render in isolation.
+	KindTemporary    = "temporary"
 	KindRSS          = "rss"
 	KindSitemap      = "sitemap"
 	KindSitemapIndex = "sitemapindex"
diff --git a/resources/page/page.go b/resources/page/page.go
index e9ecf26d791..da7b6beece6 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -150,8 +150,8 @@ type InSectionPositioner interface {
 
 // InternalDependencies is considered an internal interface.
 type InternalDependencies interface {
-	// GetRelatedDocsHandler is for internal use only.
-	GetRelatedDocsHandler() *RelatedDocsHandler
+	// GetInternalRelatedDocsHandler is for internal use only.
+	GetInternalRelatedDocsHandler() *RelatedDocsHandler
 }
 
 // OutputFormatsProvider provides the OutputFormats of a Page.
diff --git a/resources/page/page_paths.go b/resources/page/page_paths.go
index ea22eab8198..6b2c8e8b1d9 100644
--- a/resources/page/page_paths.go
+++ b/resources/page/page_paths.go
@@ -145,7 +145,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
 		pb.isUgly = true
 	}
 
-	if d.Type == output.HTTPStatusHTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
+	if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
 		pb.noSubResources = true
 	} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
 		if d.ExpandedPermalink != "" {
diff --git a/resources/page/pages_related.go b/resources/page/pages_related.go
index 3322a4fbf5d..9debda32456 100644
--- a/resources/page/pages_related.go
+++ b/resources/page/pages_related.go
@@ -129,7 +129,7 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I
 		return nil, fmt.Errorf("invalid type %T in related search", p[0])
 	}
 
-	cache := d.GetRelatedDocsHandler()
+	cache := d.GetInternalRelatedDocsHandler()
 
 	searchIndex, err := cache.getOrCreateIndex(ctx, p)
 	if err != nil {
diff --git a/resources/resource_spec.go b/resources/resource_spec.go
index f1c30e0a209..56ad9a27d6f 100644
--- a/resources/resource_spec.go
+++ b/resources/resource_spec.go
@@ -42,7 +42,6 @@ import (
 	"github.com/gohugoio/hugo/resources/images"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/resources/resource"
-	"github.com/gohugoio/hugo/tpl"
 )
 
 func NewSpec(
@@ -123,8 +122,6 @@ type Spec struct {
 	BuildClosers types.CloseAdder
 	Rebuilder    identity.SignalRebuilder
 
-	TextTemplates tpl.TemplateParseFinder
-
 	Permalinks page.PermalinkExpander
 
 	ImageCache *ImageCache
diff --git a/resources/resource_transformers/templates/execute_as_template.go b/resources/resource_transformers/templates/execute_as_template.go
index 79d249bd68b..cd421e08faf 100644
--- a/resources/resource_transformers/templates/execute_as_template.go
+++ b/resources/resource_transformers/templates/execute_as_template.go
@@ -23,17 +23,17 @@ import (
 	"github.com/gohugoio/hugo/resources"
 	"github.com/gohugoio/hugo/resources/internal"
 	"github.com/gohugoio/hugo/resources/resource"
-	"github.com/gohugoio/hugo/tpl"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 )
 
 // Client contains methods to perform template processing of Resource objects.
 type Client struct {
 	rs *resources.Spec
-	t  tpl.TemplatesProvider
+	t  tplimpl.TemplateStoreProvider
 }
 
 // New creates a new Client with the given specification.
-func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
+func New(rs *resources.Spec, t tplimpl.TemplateStoreProvider) *Client {
 	if rs == nil {
 		panic("must provide a resource Spec")
 	}
@@ -45,7 +45,7 @@ func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
 
 type executeAsTemplateTransform struct {
 	rs         *resources.Spec
-	t          tpl.TemplatesProvider
+	t          tplimpl.TemplateStoreProvider
 	targetPath string
 	data       any
 }
@@ -56,14 +56,13 @@ func (t *executeAsTemplateTransform) Key() internal.ResourceTransformationKey {
 
 func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
 	tplStr := helpers.ReaderToString(ctx.From)
-	templ, err := t.t.TextTmpl().Parse(ctx.InPath, tplStr)
+	th := t.t.GetTemplateStore()
+	ti, err := th.TextParse(ctx.InPath, tplStr)
 	if err != nil {
 		return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err)
 	}
-
 	ctx.OutPath = t.targetPath
-
-	return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
+	return th.ExecuteWithContext(ctx.Ctx, ti, ctx.To, t.data)
 }
 
 func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {
diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go
index 39b66a27f35..caa51f14912 100644
--- a/tpl/collections/apply.go
+++ b/tpl/collections/apply.go
@@ -21,7 +21,6 @@ import (
 	"strings"
 
 	"github.com/gohugoio/hugo/common/hreflect"
-	"github.com/gohugoio/hugo/tpl"
 )
 
 // Apply takes an array or slice c and returns a new slice with the function fname applied over it.
@@ -109,8 +108,7 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
 func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
 	namespace, methodName, ok := strings.Cut(fname, ".")
 	if !ok {
-		templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter)
-		return templ.GetFunc(fname)
+		return ns.deps.GetTemplateStore().GetFunc(fname)
 	}
 
 	// Namespace
diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go
index aae41e2ce57..cb7d0dc2506 100644
--- a/tpl/internal/go_templates/htmltemplate/hugo_template.go
+++ b/tpl/internal/go_templates/htmltemplate/hugo_template.go
@@ -14,6 +14,8 @@
 package template
 
 import (
+	"fmt"
+
 	"github.com/gohugoio/hugo/common/types"
 	template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 )
@@ -51,3 +53,28 @@ func indirect(a any) any {
 
 	return in
 }
+
+// CloneShallow creates a shallow copy of the template. It does not clone  or copy the nested templates.
+func (t *Template) CloneShallow() (*Template, error) {
+	t.nameSpace.mu.Lock()
+	defer t.nameSpace.mu.Unlock()
+	if t.escapeErr != nil {
+		return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+	}
+	textClone, err := t.text.Clone()
+	if err != nil {
+		return nil, err
+	}
+	ns := &nameSpace{set: make(map[string]*Template)}
+	ns.esc = makeEscaper(ns)
+	ret := &Template{
+		nil,
+		textClone,
+		textClone.Tree,
+		ns,
+	}
+	ret.set[ret.Name()] = ret
+
+	// Return the template associated with the name of this template.
+	return ret.set[ret.Name()], nil
+}
diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go
index 4582ddd5fd1..cd80e0183dd 100644
--- a/tpl/internal/go_templates/htmltemplate/template.go
+++ b/tpl/internal/go_templates/htmltemplate/template.go
@@ -267,7 +267,7 @@ func (t *Template) Clone() (*Template, error) {
 		name := x.Name()
 		src := t.set[name]
 		if src == nil || src.escapeErr != nil {
-			return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+			return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed, %q not found", t.Name(), name)
 		}
 		x.Tree = x.Tree.Copy()
 		ret.set[name] = &Template{
diff --git a/tpl/math/init.go b/tpl/math/init.go
index 0ef02550ce5..bfaf4526aea 100644
--- a/tpl/math/init.go
+++ b/tpl/math/init.go
@@ -24,7 +24,7 @@ const name = "math"
 
 func init() {
 	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
-		ctx := New()
+		ctx := New(d)
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,
diff --git a/tpl/math/math.go b/tpl/math/math.go
index 5a5e42f4eae..a0512c045ac 100644
--- a/tpl/math/math.go
+++ b/tpl/math/math.go
@@ -20,9 +20,9 @@ import (
 	"math"
 	"math/rand"
 	"reflect"
-	"sync/atomic"
 
 	_math "github.com/gohugoio/hugo/common/math"
+	"github.com/gohugoio/hugo/deps"
 	"github.com/spf13/cast"
 )
 
@@ -32,12 +32,16 @@ var (
 )
 
 // New returns a new instance of the math-namespaced template functions.
-func New() *Namespace {
-	return &Namespace{}
+func New(d *deps.Deps) *Namespace {
+	return &Namespace{
+		d: d,
+	}
 }
 
 // Namespace provides template functions for the "math" namespace.
-type Namespace struct{}
+type Namespace struct {
+	d *deps.Deps
+}
 
 // Abs returns the absolute value of n.
 func (ns *Namespace) Abs(n any) (float64, error) {
@@ -345,8 +349,6 @@ func (ns *Namespace) doArithmetic(inputs []any, operation rune) (value any, err
 	return
 }
 
-var counter uint64
-
 // Counter increments and returns a global counter.
 // This was originally added to be used in tests where now.UnixNano did not
 // have the needed precision (especially on Windows).
@@ -354,5 +356,5 @@ var counter uint64
 // and the counter will reset on new builds.
 // <docsmeta>{"identifiers": ["now.UnixNano"] }</docsmeta>
 func (ns *Namespace) Counter() uint64 {
-	return atomic.AddUint64(&counter, uint64(1))
+	return ns.d.Counters.MathCounter.Add(1)
 }
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index 4441b5fa509..27f13253a26 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -25,12 +25,12 @@ import (
 
 	"github.com/bep/lazycache"
 
+	"github.com/gohugoio/hugo/common/constants"
 	"github.com/gohugoio/hugo/common/hashing"
 	"github.com/gohugoio/hugo/identity"
 
-	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
 	"github.com/gohugoio/hugo/tpl"
+	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
 	"github.com/gohugoio/hugo/deps"
@@ -54,13 +54,6 @@ func (k partialCacheKey) Key() string {
 	return hashing.HashString(append([]any{k.Name}, k.Variants...)...)
 }
 
-func (k partialCacheKey) templateName() string {
-	if !strings.HasPrefix(k.Name, "partials/") {
-		return "partials/" + k.Name
-	}
-	return k.Name
-}
-
 // partialCache represents a LRU cache of partials.
 type partialCache struct {
 	cache *lazycache.Cache[string, includeResult]
@@ -129,6 +122,11 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
 }
 
 func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
+	if strings.HasPrefix(name, "partials/") {
+		// This is most likely not what the user intended.
+		// This worked before Hugo 0.146.0.
+		ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name)
+	}
 	// Create a new context with a timeout not connected to the incoming context.
 	timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
 	defer cancel()
@@ -159,28 +157,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
 	if len(dataList) > 0 {
 		data = dataList[0]
 	}
-
-	var n string
-	if strings.HasPrefix(name, "partials/") {
-		n = name
-	} else {
-		n = "partials/" + name
-	}
-
-	templ, found := ns.deps.Tmpl().Lookup(n)
-	if !found {
-		// For legacy reasons.
-		templ, found = ns.deps.Tmpl().Lookup(n + ".html")
-	}
-
-	if !found {
+	name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name)
+	v := ns.deps.TemplateStore.LookupPartial(name, desc)
+	if v == nil {
 		return includeResult{err: fmt.Errorf("partial %q not found", name)}
 	}
 
-	var info tpl.ParseInfo
-	if ip, ok := templ.(tpl.Info); ok {
-		info = ip.ParseInfo()
-	}
+	templ := v
+	info := v.ParseInfo
 
 	var w io.Writer
 
@@ -200,7 +184,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
 		w = b
 	}
 
-	if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil {
+	if err := ns.deps.GetTemplateStore().ExecuteWithContext(ctx, templ, w, data); err != nil {
 		return includeResult{err: err}
 	}
 
@@ -208,14 +192,14 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
 
 	if ctx, ok := data.(*contextWrapper); ok {
 		result = ctx.Result
-	} else if _, ok := templ.(*texttemplate.Template); ok {
+	} else if _, ok := templ.Template.(*texttemplate.Template); ok {
 		result = w.(fmt.Stringer).String()
 	} else {
 		result = template.HTML(w.(fmt.Stringer).String())
 	}
 
 	return includeResult{
-		name:   templ.Name(),
+		name:   templ.Template.Name(),
 		result: result,
 	}
 }
@@ -253,9 +237,9 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
 			// The templates that gets executed is measured in Execute.
 			// We need to track the time spent in the cache to
 			// get the totals correct.
-			ns.deps.Metrics.MeasureSince(key.templateName(), start)
+			ns.deps.Metrics.MeasureSince(r.name, start)
 		}
-		ns.deps.Metrics.TrackValue(key.templateName(), r.result, found)
+		ns.deps.Metrics.TrackValue(r.name, r.result, found)
 	}
 
 	if r.mangager != nil && depsManagerIn != nil {
diff --git a/tpl/template.go b/tpl/template.go
index 39c6b4f1cc5..865fea52e51 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 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.
@@ -16,9 +16,6 @@ package tpl
 
 import (
 	"context"
-	"io"
-	"reflect"
-	"regexp"
 	"strings"
 	"sync"
 	"unicode"
@@ -27,140 +24,18 @@ import (
 	"github.com/gohugoio/hugo/common/hcontext"
 	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/langs"
-	"github.com/gohugoio/hugo/output/layouts"
-
-	"github.com/gohugoio/hugo/output"
 
 	htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
 	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 )
 
-// TemplateManager manages the collection of templates.
-type TemplateManager interface {
-	TemplateHandler
-	TemplateFuncGetter
-	AddTemplate(name, tpl string) error
-}
-
-// TemplateVariants describes the possible variants of a template.
-// All of these may be empty.
-type TemplateVariants struct {
-	Language     string
-	OutputFormat output.Format
-}
-
-// TemplateFinder finds templates.
-type TemplateFinder interface {
-	TemplateLookup
-	TemplateLookupVariant
-}
-
-// UnusedTemplatesProvider lists unused templates if the build is configured to track those.
-type UnusedTemplatesProvider interface {
-	UnusedTemplates() []FileInfo
-}
-
-// TemplateHandlers holds the templates needed by Hugo.
-type TemplateHandlers struct {
-	Tmpl    TemplateHandler
-	TxtTmpl TemplateParseFinder
-}
-
-type TemplateExecutor interface {
-	ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
-}
-
-// TemplateHandler finds and executes templates.
-type TemplateHandler interface {
-	TemplateFinder
-	TemplateExecutor
-	LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error)
-	HasTemplate(name string) bool
-	GetIdentity(name string) (identity.Identity, bool)
-}
-
-type TemplateLookup interface {
-	Lookup(name string) (Template, bool)
-}
-
-type TemplateLookupVariant interface {
-	// TODO(bep) this currently only works for shortcodes.
-	// We may unify and expand this variant pattern to the
-	// other templates, but we need this now for the shortcodes to
-	// quickly determine if a shortcode has a template for a given
-	// output format.
-	// It returns the template, if it was found or not and if there are
-	// alternative representations (output format, language).
-	// We are currently only interested in output formats, so we should improve
-	// this for speed.
-	LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
-	LookupVariants(name string) []Template
-}
-
 // Template is the common interface between text/template and html/template.
 type Template interface {
 	Name() string
 	Prepare() (*texttemplate.Template, error)
 }
 
-// AddIdentity checks if t is an identity.Identity and returns it if so.
-// Else it wraps it in a templateIdentity using its name as the base.
-func AddIdentity(t Template) Template {
-	if _, ok := t.(identity.IdentityProvider); ok {
-		return t
-	}
-	return templateIdentityProvider{
-		Template: t,
-		id:       identity.StringIdentity(t.Name()),
-	}
-}
-
-type templateIdentityProvider struct {
-	Template
-	id identity.Identity
-}
-
-func (t templateIdentityProvider) GetIdentity() identity.Identity {
-	return t.id
-}
-
-// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
-type TemplateParser interface {
-	Parse(name, tpl string) (Template, error)
-}
-
-// TemplateParseFinder provides both parsing and finding.
-type TemplateParseFinder interface {
-	TemplateParser
-	TemplateFinder
-}
-
-// TemplateDebugger prints some debug info to stdout.
-type TemplateDebugger interface {
-	Debug()
-}
-
-// TemplatesProvider as implemented by deps.Deps.
-type TemplatesProvider interface {
-	Tmpl() TemplateHandler
-	TextTmpl() TemplateParseFinder
-}
-
-var baseOfRe = regexp.MustCompile("template: (.*?):")
-
-func extractBaseOf(err string) string {
-	m := baseOfRe.FindStringSubmatch(err)
-	if len(m) == 2 {
-		return m[1]
-	}
-	return ""
-}
-
-// TemplateFuncGetter allows to find a template func by name.
-type TemplateFuncGetter interface {
-	GetFunc(name string) (reflect.Value, bool)
-}
-
+// RenderingContext represents the currently rendered site/language.
 type RenderingContext struct {
 	Site       site
 	SiteOutIdx int
@@ -201,7 +76,9 @@ type site interface {
 }
 
 const (
+	// HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
 	HugoDeferredTemplatePrefix = "__hdeferred/"
+	// HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
 	HugoDeferredTemplateSuffix = "__d="
 )
 
@@ -243,10 +120,11 @@ func StripHTML(s string) string {
 	return s
 }
 
+// DeferredExecution holds the template and data for a deferred execution.
 type DeferredExecution struct {
 	Mu           sync.Mutex
 	Ctx          context.Context
-	TemplateName string
+	TemplatePath string
 	Data         any
 
 	Executed bool
diff --git a/tpl/templates/templates.go b/tpl/templates/templates.go
index 0be44a013ae..98ae0a63989 100644
--- a/tpl/templates/templates.go
+++ b/tpl/templates/templates.go
@@ -44,7 +44,7 @@ type Namespace struct {
 // Note that this is the Unix-styled relative path including filename suffix,
 // e.g. partials/header.html
 func (ns *Namespace) Exists(name string) bool {
-	return ns.deps.Tmpl().HasTemplate(name)
+	return ns.deps.GetTemplateStore().HasTemplate(name)
 }
 
 // Defer defers the execution of a template block.
@@ -93,7 +93,7 @@ func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
 	_, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
 		func() (*tpl.DeferredExecution, error) {
 			return &tpl.DeferredExecution{
-				TemplateName: templateName,
+				TemplatePath: templateName,
 				Ctx:          ctx,
 				Data:         opts.Data,
 				Executed:     false,
diff --git a/tpl/tplimpl/category_string.go b/tpl/tplimpl/category_string.go
new file mode 100644
index 00000000000..2f7730113fc
--- /dev/null
+++ b/tpl/tplimpl/category_string.go
@@ -0,0 +1,30 @@
+// Code generated by "stringer -type Category"; DO NOT EDIT.
+
+package tplimpl
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[CategoryLayout-1]
+	_ = x[CategoryBaseof-2]
+	_ = x[CategoryMarkup-3]
+	_ = x[CategoryShortcode-4]
+	_ = x[CategoryPartial-5]
+	_ = x[CategoryServer-6]
+	_ = x[CategoryHugo-7]
+}
+
+const _Category_name = "CategoryLayoutCategoryBaseofCategoryMarkupCategoryShortcodeCategoryPartialCategoryServerCategoryHugo"
+
+var _Category_index = [...]uint8{0, 14, 28, 42, 59, 74, 88, 100}
+
+func (i Category) String() string {
+	i -= 1
+	if i < 0 || i >= Category(len(_Category_index)-1) {
+		return "Category(" + strconv.FormatInt(int64(i+1), 10) + ")"
+	}
+	return _Category_name[_Category_index[i]:_Category_index[i+1]]
+}
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html b/tpl/tplimpl/embedded/templates/_markup/render-codeblock-goat.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html
rename to tpl/tplimpl/embedded/templates/_markup/render-codeblock-goat.html
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-image.html b/tpl/tplimpl/embedded/templates/_markup/render-image.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/_markup/render-image.html
rename to tpl/tplimpl/embedded/templates/_markup/render-image.html
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-link.html b/tpl/tplimpl/embedded/templates/_markup/render-link.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/_markup/render-link.html
rename to tpl/tplimpl/embedded/templates/_markup/render-link.html
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-table.html b/tpl/tplimpl/embedded/templates/_markup/render-table.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/_markup/render-table.html
rename to tpl/tplimpl/embedded/templates/_markup/render-table.html
diff --git a/tpl/tplimpl/embedded/templates/partials/_funcs/get-page-images.html b/tpl/tplimpl/embedded/templates/_partials/_funcs/get-page-images.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/partials/_funcs/get-page-images.html
rename to tpl/tplimpl/embedded/templates/_partials/_funcs/get-page-images.html
diff --git a/tpl/tplimpl/embedded/templates/disqus.html b/tpl/tplimpl/embedded/templates/_partials/disqus.html
similarity index 92%
rename from tpl/tplimpl/embedded/templates/disqus.html
rename to tpl/tplimpl/embedded/templates/_partials/disqus.html
index 19fcb7fda68..fb7cbd0fe16 100644
--- a/tpl/tplimpl/embedded/templates/disqus.html
+++ b/tpl/tplimpl/embedded/templates/_partials/disqus.html
@@ -5,7 +5,7 @@
     window.disqus_config = function () {
     {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
     {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
-    {{with .Params.disqus_url }}this.page.url = '{{ . | html  }}';{{end}}
+    {{with .Params.disqus_url }}this.page.url = '{{ . |  transform.HTMLEscape | safeURL }}';{{end}}
     };
     (function() {
         if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
diff --git a/tpl/tplimpl/embedded/templates/google_analytics.html b/tpl/tplimpl/embedded/templates/_partials/google_analytics.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/google_analytics.html
rename to tpl/tplimpl/embedded/templates/_partials/google_analytics.html
diff --git a/tpl/tplimpl/embedded/templates/opengraph.html b/tpl/tplimpl/embedded/templates/_partials/opengraph.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/opengraph.html
rename to tpl/tplimpl/embedded/templates/_partials/opengraph.html
diff --git a/tpl/tplimpl/embedded/templates/pagination.html b/tpl/tplimpl/embedded/templates/_partials/pagination.html
similarity index 98%
rename from tpl/tplimpl/embedded/templates/pagination.html
rename to tpl/tplimpl/embedded/templates/_partials/pagination.html
index 717797ab279..9f82447023c 100644
--- a/tpl/tplimpl/embedded/templates/pagination.html
+++ b/tpl/tplimpl/embedded/templates/_partials/pagination.html
@@ -20,7 +20,7 @@
 {{- if in $validFormats $format }}
   {{- if gt $page.Paginator.TotalPages 1 }}
     <ul class="pagination pagination-{{ $format }}">
-      {{- partial (printf "partials/inline/pagination/%s" $format) $page }}
+      {{- partial (printf "inline/pagination/%s" $format) $page }}
     </ul>
   {{- end }}
 {{- else }}
diff --git a/tpl/tplimpl/embedded/templates/schema.html b/tpl/tplimpl/embedded/templates/_partials/schema.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/schema.html
rename to tpl/tplimpl/embedded/templates/_partials/schema.html
diff --git a/tpl/tplimpl/embedded/templates/twitter_cards.html b/tpl/tplimpl/embedded/templates/_partials/twitter_cards.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/twitter_cards.html
rename to tpl/tplimpl/embedded/templates/_partials/twitter_cards.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/1__h_simple_assets.html b/tpl/tplimpl/embedded/templates/_shortcodes/1__h_simple_assets.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/1__h_simple_assets.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/1__h_simple_assets.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/comment.html b/tpl/tplimpl/embedded/templates/_shortcodes/comment.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/comment.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/comment.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/details.html b/tpl/tplimpl/embedded/templates/_shortcodes/details.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/details.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/details.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/figure.html b/tpl/tplimpl/embedded/templates/_shortcodes/figure.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/figure.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/figure.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/gist.html b/tpl/tplimpl/embedded/templates/_shortcodes/gist.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/gist.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/gist.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/highlight.html b/tpl/tplimpl/embedded/templates/_shortcodes/highlight.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/highlight.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/highlight.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram.html b/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/instagram.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/instagram.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/instagram_simple.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/instagram_simple.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/param.html b/tpl/tplimpl/embedded/templates/_shortcodes/param.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/param.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/param.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/qr.html b/tpl/tplimpl/embedded/templates/_shortcodes/qr.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/qr.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/qr.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/ref.html b/tpl/tplimpl/embedded/templates/_shortcodes/ref.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/ref.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/ref.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/relref.html b/tpl/tplimpl/embedded/templates/_shortcodes/relref.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/relref.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/relref.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/twitter.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/twitter_simple.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/twitter_simple.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/vimeo.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/x.html b/tpl/tplimpl/embedded/templates/_shortcodes/x.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/x.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/x.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/x_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/x_simple.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/x_simple.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/x_simple.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/youtube.html b/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/youtube.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/youtube.html
diff --git a/tpl/tplimpl/embedded/templates/_default/robots.txt b/tpl/tplimpl/embedded/templates/robots.txt
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/robots.txt
rename to tpl/tplimpl/embedded/templates/robots.txt
diff --git a/tpl/tplimpl/embedded/templates/_default/rss.xml b/tpl/tplimpl/embedded/templates/rss.xml
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/rss.xml
rename to tpl/tplimpl/embedded/templates/rss.xml
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemap.xml b/tpl/tplimpl/embedded/templates/sitemap.xml
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/sitemap.xml
rename to tpl/tplimpl/embedded/templates/sitemap.xml
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml b/tpl/tplimpl/embedded/templates/sitemapindex.xml
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/sitemapindex.xml
rename to tpl/tplimpl/embedded/templates/sitemapindex.xml
diff --git a/tpl/tplimpl/legacy.go b/tpl/tplimpl/legacy.go
new file mode 100644
index 00000000000..ee9a6e5d18c
--- /dev/null
+++ b/tpl/tplimpl/legacy.go
@@ -0,0 +1,130 @@
+// Copyright 2025 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 tplimpl
+
+import (
+	"github.com/gohugoio/hugo/hugofs"
+	"github.com/gohugoio/hugo/resources/kinds"
+)
+
+type layoutLegacyMapping struct {
+	sourcePath string
+	target     layoutLegacyMappingTarget
+}
+
+type layoutLegacyMappingTarget struct {
+	targetPath     string
+	targetDesc     TemplateDescriptor
+	targetCategory Category
+}
+
+var (
+	ltermPlural = layoutLegacyMappingTarget{
+		targetPath:     "/PLURAL",
+		targetDesc:     TemplateDescriptor{Kind: kinds.KindTerm},
+		targetCategory: CategoryLayout,
+	}
+	ltermBase = layoutLegacyMappingTarget{
+		targetPath:     "",
+		targetDesc:     TemplateDescriptor{Kind: kinds.KindTerm},
+		targetCategory: CategoryLayout,
+	}
+
+	ltaxPlural = layoutLegacyMappingTarget{
+		targetPath:     "/PLURAL",
+		targetDesc:     TemplateDescriptor{Kind: kinds.KindTaxonomy},
+		targetCategory: CategoryLayout,
+	}
+	ltaxBase = layoutLegacyMappingTarget{
+		targetPath:     "",
+		targetDesc:     TemplateDescriptor{Kind: kinds.KindTaxonomy},
+		targetCategory: CategoryLayout,
+	}
+
+	lsectBase = layoutLegacyMappingTarget{
+		targetPath:     "",
+		targetDesc:     TemplateDescriptor{Kind: kinds.KindSection},
+		targetCategory: CategoryLayout,
+	}
+	lsectTheSection = layoutLegacyMappingTarget{
+		targetPath:     "/THESECTION",
+		targetDesc:     TemplateDescriptor{Kind: kinds.KindSection},
+		targetCategory: CategoryLayout,
+	}
+)
+
+type legacyTargetPathIdentifiers struct {
+	targetPath     string
+	targetCategory Category
+	kind           string
+	lang           string
+	outputFormat   string
+	ext            string
+}
+
+type legacyOrdinalMapping struct {
+	ordinal int
+	mapping layoutLegacyMappingTarget
+}
+
+type legacyOrdinalMappingFi struct {
+	m  legacyOrdinalMapping
+	fi hugofs.FileMetaInfo
+}
+
+var legacyTermMappings = []layoutLegacyMapping{
+	{sourcePath: "/PLURAL/term", target: ltermPlural},
+	{sourcePath: "/PLURAL/SINGULAR", target: ltermPlural},
+	{sourcePath: "/term/term", target: ltermBase},
+	{sourcePath: "/term/SINGULAR", target: ltermPlural},
+	{sourcePath: "/term/taxonomy", target: ltermPlural},
+	{sourcePath: "/term/list", target: ltermBase},
+	{sourcePath: "/taxonomy/term", target: ltermBase},
+	{sourcePath: "/taxonomy/SINGULAR", target: ltermPlural},
+	{sourcePath: "/SINGULAR/term", target: ltermPlural},
+	{sourcePath: "/SINGULAR/SINGULAR", target: ltermPlural},
+	{sourcePath: "/_default/SINGULAR", target: ltermPlural},
+	{sourcePath: "/_default/taxonomy", target: ltermBase},
+}
+
+var legacyTaxonomyMappings = []layoutLegacyMapping{
+	{sourcePath: "/PLURAL/SINGULAR.terms", target: ltaxPlural},
+	{sourcePath: "/PLURAL/terms", target: ltaxPlural},
+	{sourcePath: "/PLURAL/taxonomy", target: ltaxPlural},
+	{sourcePath: "/PLURAL/list", target: ltaxPlural},
+	{sourcePath: "/SINGULAR/SINGULAR.terms", target: ltaxPlural},
+	{sourcePath: "/SINGULAR/terms", target: ltaxPlural},
+	{sourcePath: "/SINGULAR/taxonomy", target: ltaxPlural},
+	{sourcePath: "/SINGULAR/list", target: ltaxPlural},
+	{sourcePath: "/taxonomy/SINGULAR.terms", target: ltaxPlural},
+	{sourcePath: "/taxonomy/terms", target: ltaxBase},
+	{sourcePath: "/taxonomy/taxonomy", target: ltaxBase},
+	{sourcePath: "/taxonomy/list", target: ltaxBase},
+	{sourcePath: "/_default/SINGULAR.terms", target: ltaxBase},
+	{sourcePath: "/_default/terms", target: ltaxBase},
+	{sourcePath: "/_default/taxonomy", target: ltaxBase},
+}
+
+var legacySectionMappings = []layoutLegacyMapping{
+	// E.g. /mysection/mysection.html
+	{sourcePath: "/THESECTION/THESECTION", target: lsectTheSection},
+	// E.g. /section/mysection.html
+	{sourcePath: "/SECTIONKIND/THESECTION", target: lsectTheSection},
+	// E.g. /section/section.html
+	{sourcePath: "/SECTIONKIND/SECTIONKIND", target: lsectBase},
+	// E.g. /section/list.html
+	{sourcePath: "/SECTIONKIND/list", target: lsectBase},
+	// E.g. /_default/mysection.html
+	{sourcePath: "/_default/THESECTION", target: lsectTheSection},
+}
diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go
deleted file mode 100644
index a1e7b463ecc..00000000000
--- a/tpl/tplimpl/shortcodes.go
+++ /dev/null
@@ -1,153 +0,0 @@
-// 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 tplimpl
-
-import (
-	"strings"
-
-	"github.com/gohugoio/hugo/tpl"
-)
-
-// Currently lang, outFormat, suffix
-const numTemplateVariants = 3
-
-type shortcodeVariant struct {
-	// The possible variants: lang, outFormat, suffix
-	// gtag
-	// gtag.html
-	// gtag.no.html
-	// gtag.no.amp.html
-	// A slice of length numTemplateVariants.
-	variants []string
-
-	ts *templateState
-}
-
-type shortcodeTemplates struct {
-	variants []shortcodeVariant
-}
-
-func (s *shortcodeTemplates) indexOf(variants []string) int {
-L:
-	for i, v1 := range s.variants {
-		for i, v2 := range v1.variants {
-			if v2 != variants[i] {
-				continue L
-			}
-		}
-		return i
-	}
-	return -1
-}
-
-func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) {
-	return s.fromVariantsSlice([]string{
-		variants.Language,
-		strings.ToLower(variants.OutputFormat.Name),
-		variants.OutputFormat.MediaType.FirstSuffix.Suffix,
-	})
-}
-
-func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) {
-	var (
-		bestMatch       shortcodeVariant
-		bestMatchWeight int
-	)
-
-	for _, variant := range s.variants {
-		w := s.compareVariants(variants, variant.variants)
-		if bestMatchWeight == 0 || w > bestMatchWeight {
-			bestMatch = variant
-			bestMatchWeight = w
-		}
-	}
-
-	return bestMatch, true
-}
-
-// calculate a weight for two string slices of same length.
-// higher value means "better match".
-func (s *shortcodeTemplates) compareVariants(a, b []string) int {
-	weight := 0
-	k := len(a)
-	for i, av := range a {
-		bv := b[i]
-		if av == bv {
-			// Add more weight to the left side (language...).
-			weight = weight + k - i
-		} else {
-			weight--
-		}
-	}
-	return weight
-}
-
-func templateVariants(name string) []string {
-	_, variants := templateNameAndVariants(name)
-	return variants
-}
-
-func templateNameAndVariants(name string) (string, []string) {
-	variants := make([]string, numTemplateVariants)
-
-	parts := strings.Split(name, ".")
-
-	if len(parts) <= 1 {
-		// No variants.
-		return name, variants
-	}
-
-	name = parts[0]
-	parts = parts[1:]
-	lp := len(parts)
-	start := len(variants) - lp
-
-	for i, j := start, 0; i < len(variants); i, j = i+1, j+1 {
-		variants[i] = parts[j]
-	}
-
-	if lp > 1 && lp < len(variants) {
-		for i := lp - 1; i > 0; i-- {
-			variants[i-1] = variants[i]
-		}
-	}
-
-	if lp == 1 {
-		// Suffix only. Duplicate it into the output format field to
-		// make HTML win over AMP.
-		variants[len(variants)-2] = variants[len(variants)-1]
-	}
-
-	return name, variants
-}
-
-func resolveTemplateType(name string) templateType {
-	if isShortcode(name) {
-		return templateShortcode
-	}
-
-	if strings.Contains(name, "partials/") {
-		return templatePartial
-	}
-
-	return templateUndefined
-}
-
-func isShortcode(name string) bool {
-	return strings.Contains(name, shortcodesPathPrefix)
-}
-
-func isInternal(name string) bool {
-	return strings.HasPrefix(name, internalPathPrefix)
-}
diff --git a/tpl/tplimpl/subcategory_string.go b/tpl/tplimpl/subcategory_string.go
new file mode 100644
index 00000000000..a36bdee7cfa
--- /dev/null
+++ b/tpl/tplimpl/subcategory_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -type SubCategory"; DO NOT EDIT.
+
+package tplimpl
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[SubCategoryMain-0]
+	_ = x[SubCategoryEmbedded-1]
+	_ = x[SubCategoryInline-2]
+}
+
+const _SubCategory_name = "SubCategoryMainSubCategoryEmbeddedSubCategoryInline"
+
+var _SubCategory_index = [...]uint8{0, 15, 34, 51}
+
+func (i SubCategory) String() string {
+	if i < 0 || i >= SubCategory(len(_SubCategory_index)-1) {
+		return "SubCategory(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _SubCategory_name[_SubCategory_index[i]:_SubCategory_index[i+1]]
+}
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
deleted file mode 100644
index 3b643162a93..00000000000
--- a/tpl/tplimpl/template.go
+++ /dev/null
@@ -1,1235 +0,0 @@
-// 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 tplimpl
-
-import (
-	"bytes"
-	"context"
-	"embed"
-	"fmt"
-	"io"
-	"io/fs"
-	"path/filepath"
-	"reflect"
-	"regexp"
-	"sort"
-	"strings"
-	"sync"
-	"time"
-	"unicode"
-	"unicode/utf8"
-
-	"github.com/gohugoio/hugo/common/maps"
-	"github.com/gohugoio/hugo/common/types"
-	"github.com/gohugoio/hugo/output/layouts"
-
-	"github.com/gohugoio/hugo/helpers"
-
-	"github.com/gohugoio/hugo/output"
-
-	"github.com/gohugoio/hugo/deps"
-	"github.com/spf13/afero"
-
-	"github.com/gohugoio/hugo/common/herrors"
-	"github.com/gohugoio/hugo/hugofs"
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
-
-	htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
-	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
-	"github.com/gohugoio/hugo/identity"
-	"github.com/gohugoio/hugo/tpl"
-)
-
-const (
-	textTmplNamePrefix = "_text/"
-
-	shortcodesPathPrefix = "shortcodes/"
-	internalPathPrefix   = "_internal/"
-	embeddedPathPrefix   = "_embedded/"
-	baseFileBase         = "baseof"
-)
-
-// The identifiers may be truncated in the log, e.g.
-// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
-// We need this to identify position in templates with base templates applied.
-var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
-
-// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
-// in v0.141.0. We can remove these aliases in v0.155.0 or later.
-var embeddedTemplatesAliases = map[string][]string{
-	"shortcodes/twitter.html": {"shortcodes/tweet.html"},
-}
-
-var (
-	_ tpl.TemplateManager         = (*templateExec)(nil)
-	_ tpl.TemplateHandler         = (*templateExec)(nil)
-	_ tpl.TemplateFuncGetter      = (*templateExec)(nil)
-	_ tpl.TemplateFinder          = (*templateExec)(nil)
-	_ tpl.UnusedTemplatesProvider = (*templateExec)(nil)
-
-	_ tpl.Template = (*templateState)(nil)
-	_ tpl.Info     = (*templateState)(nil)
-)
-
-var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
-
-// needsBaseTemplate returns true if the first non-comment template block is a
-// define block.
-// If a base template does not exist, we will handle that when it's used.
-func needsBaseTemplate(templ string) bool {
-	idx := -1
-	inComment := false
-	for i := 0; i < len(templ); {
-		if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
-			inComment = true
-			i += 4
-		} else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
-			inComment = true
-			i += 6
-		} else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
-			inComment = false
-			i += 4
-		} else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
-			inComment = false
-			i += 6
-		} else {
-			r, size := utf8.DecodeRuneInString(templ[i:])
-			if !inComment {
-				if strings.HasPrefix(templ[i:], "{{") {
-					idx = i
-					break
-				} else if !unicode.IsSpace(r) {
-					break
-				}
-			}
-			i += size
-		}
-	}
-
-	if idx == -1 {
-		return false
-	}
-
-	return baseTemplateDefineRe.MatchString(templ[idx:])
-}
-
-func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder {
-	return &textTemplateWrapperWithLock{
-		RWMutex:  &sync.RWMutex{},
-		Template: texttemplate.New("").Funcs(funcs),
-	}
-}
-
-func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
-	exec, funcs := newTemplateExecuter(d)
-	funcMap := make(map[string]any)
-	for k, v := range funcs {
-		funcMap[k] = v.Interface()
-	}
-
-	var templateUsageTracker map[string]templateInfo
-	if d.Conf.PrintUnusedTemplates() {
-		templateUsageTracker = make(map[string]templateInfo)
-	}
-
-	h := &templateHandler{
-		nameBaseTemplateName: make(map[string]string),
-		transformNotFound:    make(map[string]*templateState),
-
-		shortcodes:   make(map[string]*shortcodeTemplates),
-		templateInfo: make(map[string]tpl.Info),
-		baseof:       make(map[string]templateInfo),
-		needsBaseof:  make(map[string]templateInfo),
-
-		main: newTemplateNamespace(funcMap),
-
-		Deps:                d,
-		layoutHandler:       layouts.NewLayoutHandler(),
-		layoutsFs:           d.BaseFs.Layouts.Fs,
-		layoutTemplateCache: make(map[layoutCacheKey]layoutCacheEntry),
-
-		templateUsageTracker: templateUsageTracker,
-	}
-
-	if err := h.loadEmbedded(); err != nil {
-		return nil, err
-	}
-
-	if err := h.loadTemplates(); err != nil {
-		return nil, err
-	}
-
-	if err := h.main.createPrototypes(); err != nil {
-		return nil, err
-	}
-
-	e := &templateExec{
-		d:               d,
-		executor:        exec,
-		funcs:           funcs,
-		templateHandler: h,
-	}
-
-	if err := e.postTransform(); err != nil {
-		return nil, err
-	}
-
-	return &tpl.TemplateHandlers{
-		Tmpl:    e,
-		TxtTmpl: newStandaloneTextTemplate(funcMap),
-	}, nil
-}
-
-func newTemplateNamespace(funcs map[string]any) *templateNamespace {
-	return &templateNamespace{
-		prototypeHTML:           htmltemplate.New("").Funcs(funcs),
-		prototypeText:           texttemplate.New("").Funcs(funcs),
-		prototypeHTMLCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](),
-		prototypeTextCloneCache: maps.NewCache[prototypeCloneID, *texttemplate.Template](),
-		templateStateMap: &templateStateMap{
-			templates: make(map[string]*templateState),
-		},
-	}
-}
-
-func newTemplateState(owner *templateState, templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
-	if id == nil {
-		id = info
-	}
-	return &templateState{
-		owner:     owner,
-		info:      info,
-		typ:       info.resolveType(),
-		Template:  templ,
-		parseInfo: tpl.DefaultParseInfo,
-		id:        id,
-	}
-}
-
-type layoutCacheKey struct {
-	d layouts.LayoutDescriptor
-	f string
-}
-
-type templateExec struct {
-	d        *deps.Deps
-	executor texttemplate.Executer
-	funcs    map[string]reflect.Value
-
-	*templateHandler
-}
-
-func (t templateExec) Clone(d *deps.Deps) *templateExec {
-	exec, funcs := newTemplateExecuter(d)
-	t.executor = exec
-	t.funcs = funcs
-	t.d = d
-	return &t
-}
-
-func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error {
-	return t.ExecuteWithContext(context.Background(), templ, wr, data)
-}
-
-func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Template, wr io.Writer, data any) error {
-	if rlocker, ok := templ.(types.RLocker); ok {
-		rlocker.RLock()
-		defer rlocker.RUnlock()
-	}
-	if t.Metrics != nil {
-		defer t.Metrics.MeasureSince(templ.Name(), time.Now())
-	}
-
-	if t.templateUsageTracker != nil {
-		if ts, ok := templ.(*templateState); ok {
-
-			t.templateUsageTrackerMu.Lock()
-			if _, found := t.templateUsageTracker[ts.Name()]; !found {
-				t.templateUsageTracker[ts.Name()] = ts.info
-			}
-
-			if !ts.baseInfo.IsZero() {
-				if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found {
-					t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo
-				}
-			}
-			t.templateUsageTrackerMu.Unlock()
-		}
-	}
-
-	execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data)
-	if execErr != nil {
-		owner := templ
-		if ts, ok := templ.(*templateState); ok && ts.owner != nil {
-			owner = ts.owner
-		}
-		execErr = t.addFileContext(owner, execErr)
-	}
-	return execErr
-}
-
-func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
-	if t.templateUsageTracker == nil {
-		return nil
-	}
-	var unused []tpl.FileInfo
-
-	for _, ti := range t.needsBaseof {
-		if _, found := t.templateUsageTracker[ti.name]; !found {
-			unused = append(unused, ti)
-		}
-	}
-
-	for _, ti := range t.baseof {
-		if _, found := t.templateUsageTracker[ti.name]; !found {
-			unused = append(unused, ti)
-		}
-	}
-
-	for _, ts := range t.main.templates {
-		ti := ts.info
-		if strings.HasPrefix(ti.name, "_internal/") || ti.meta == nil {
-			continue
-		}
-
-		if _, found := t.templateUsageTracker[ti.name]; !found {
-			unused = append(unused, ti)
-		}
-	}
-
-	sort.Slice(unused, func(i, j int) bool {
-		return unused[i].Name() < unused[j].Name()
-	})
-
-	return unused
-}
-
-func (t *templateExec) GetFunc(name string) (reflect.Value, bool) {
-	v, found := t.funcs[name]
-	return v, found
-}
-
-type templateHandler struct {
-	main        *templateNamespace
-	needsBaseof map[string]templateInfo
-	baseof      map[string]templateInfo
-
-	// This is the filesystem to load the templates from. All the templates are
-	// stored in the root of this filesystem.
-	layoutsFs afero.Fs
-
-	layoutHandler *layouts.LayoutHandler
-
-	layoutTemplateCache   map[layoutCacheKey]layoutCacheEntry
-	layoutTemplateCacheMu sync.RWMutex
-
-	*deps.Deps
-
-	// Used to get proper filenames in errors
-	nameBaseTemplateName map[string]string
-
-	// Holds name and source of template definitions not found during the first
-	// AST transformation pass.
-	transformNotFound map[string]*templateState
-
-	// shortcodes maps shortcode name to template variants
-	// (language, output format etc.) of that shortcode.
-	shortcodes map[string]*shortcodeTemplates
-
-	// templateInfo maps template name to some additional information about that template.
-	// Note that for shortcodes that same information is embedded in the
-	// shortcodeTemplates type.
-	templateInfo map[string]tpl.Info
-
-	// May be nil.
-	templateUsageTracker   map[string]templateInfo
-	templateUsageTrackerMu sync.Mutex
-}
-
-type layoutCacheEntry struct {
-	found bool
-	templ tpl.Template
-	err   error
-}
-
-// AddTemplate parses and adds a template to the collection.
-// Templates with name prefixed with "_text" will be handled as plain
-// text templates.
-func (t *templateHandler) AddTemplate(name, tpl string) error {
-	templ, err := t.addTemplateTo(t.newTemplateInfo(name, tpl), t.main)
-	if err == nil {
-		_, err = t.applyTemplateTransformers(t.main, templ)
-	}
-	return err
-}
-
-func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
-	templ, found := t.main.Lookup(name)
-	if found {
-		return templ, true
-	}
-
-	return nil, false
-}
-
-func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
-	key := layoutCacheKey{d, f.Name}
-	t.layoutTemplateCacheMu.RLock()
-	if cacheVal, found := t.layoutTemplateCache[key]; found {
-		t.layoutTemplateCacheMu.RUnlock()
-		return cacheVal.templ, cacheVal.found, cacheVal.err
-	}
-
-	t.layoutTemplateCacheMu.RUnlock()
-
-	t.layoutTemplateCacheMu.Lock()
-	defer t.layoutTemplateCacheMu.Unlock()
-
-	templ, found, err := t.findLayout(d, f)
-	cacheVal := layoutCacheEntry{found: found, templ: templ, err: err}
-	t.layoutTemplateCache[key] = cacheVal
-	return cacheVal.templ, cacheVal.found, cacheVal.err
-}
-
-// 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) {
-	name = templateBaseName(templateShortcode, name)
-	s, found := t.shortcodes[name]
-	if !found {
-		return nil, false, false
-	}
-
-	sv, found := s.fromVariants(variants)
-	if !found {
-		return nil, false, false
-	}
-
-	more := len(s.variants) > 1
-
-	return sv.ts, true, more
-}
-
-// LookupVariants returns all variants of name, nil if none found.
-func (t *templateHandler) LookupVariants(name string) []tpl.Template {
-	name = templateBaseName(templateShortcode, name)
-	s, found := t.shortcodes[name]
-	if !found {
-		return nil
-	}
-
-	variants := make([]tpl.Template, len(s.variants))
-	for i := range variants {
-		variants[i] = s.variants[i].ts
-	}
-
-	return variants
-}
-
-func (t *templateHandler) HasTemplate(name string) bool {
-	if _, found := t.baseof[name]; found {
-		return true
-	}
-
-	if _, found := t.needsBaseof[name]; found {
-		return true
-	}
-
-	_, found := t.Lookup(name)
-	return found
-}
-
-func (t *templateHandler) GetIdentity(name string) (identity.Identity, bool) {
-	if _, found := t.needsBaseof[name]; found {
-		return identity.StringIdentity(name), true
-	}
-
-	if _, found := t.baseof[name]; found {
-		return identity.StringIdentity(name), true
-	}
-
-	tt, found := t.Lookup(name)
-	if !found {
-		return nil, false
-	}
-	return tt.(identity.IdentityProvider).GetIdentity(), found
-}
-
-func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
-	d.OutputFormatName = f.Name
-	d.Suffix = f.MediaType.FirstSuffix.Suffix
-	layouts, _ := t.layoutHandler.For(d)
-	for _, name := range layouts {
-		templ, found := t.main.Lookup(name)
-		if found {
-			return templ, true, nil
-		}
-
-		overlay, found := t.needsBaseof[name]
-
-		if !found {
-			continue
-		}
-
-		d.Baseof = true
-		baseLayouts, _ := t.layoutHandler.For(d)
-		var base templateInfo
-		found = false
-		for _, l := range baseLayouts {
-			base, found = t.baseof[l]
-			if found {
-				break
-			}
-		}
-
-		templ, err := t.applyBaseTemplate(overlay, base)
-		if err != nil {
-			return nil, false, err
-		}
-
-		ts := newTemplateState(nil, templ, overlay, identity.Or(base, overlay))
-
-		if found {
-			ts.baseInfo = base
-		}
-
-		if _, err := t.applyTemplateTransformers(t.main, ts); err != nil {
-			return nil, false, err
-		}
-
-		if err := t.extractPartials(ts.Template); err != nil {
-			return nil, false, err
-		}
-
-		return ts, true, nil
-
-	}
-
-	return nil, false, nil
-}
-
-func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
-	var isText bool
-	var isEmbedded bool
-
-	if strings.HasPrefix(name, embeddedPathPrefix) {
-		isEmbedded = true
-		name = strings.TrimPrefix(name, embeddedPathPrefix)
-	}
-
-	name, isText = t.nameIsText(name)
-	return templateInfo{
-		name:       name,
-		isText:     isText,
-		isEmbedded: isEmbedded,
-		template:   tpl,
-	}
-}
-
-func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error {
-	if strings.HasPrefix(templ.Name(), "_internal") {
-		return inerr
-	}
-
-	ts, ok := templ.(*templateState)
-	if !ok {
-		return inerr
-	}
-
-	identifiers := t.extractIdentifiers(inerr.Error())
-
-	checkFilename := func(info templateInfo, inErr error) (error, bool) {
-		if info.meta == nil {
-			return inErr, false
-		}
-
-		lineMatcher := func(m herrors.LineMatcher) int {
-			if m.Position.LineNumber != m.LineNumber {
-				return -1
-			}
-
-			for _, id := range identifiers {
-				if strings.Contains(m.Line, id) {
-					// We found the line, but return a 0 to signal to
-					// use the column from the error message.
-					return 0
-				}
-			}
-			return -1
-		}
-
-		f, err := info.meta.Open()
-		if err != nil {
-			return inErr, false
-		}
-		defer f.Close()
-
-		fe := herrors.NewFileErrorFromName(inErr, info.meta.Filename)
-		fe.UpdateContent(f, lineMatcher)
-
-		if !fe.ErrorContext().Position.IsValid() {
-			return inErr, false
-		}
-		return fe, true
-	}
-
-	inerr = fmt.Errorf("execute of template failed: %w", inerr)
-
-	if err, ok := checkFilename(ts.info, inerr); ok {
-		return err
-	}
-
-	err, _ := checkFilename(ts.baseInfo, inerr)
-
-	return err
-}
-
-func (t *templateHandler) extractIdentifiers(line string) []string {
-	m := identifiersRe.FindAllStringSubmatch(line, -1)
-	identifiers := make([]string, len(m))
-	for i := range m {
-		identifiers[i] = m[i][1]
-	}
-	return identifiers
-}
-
-func (t *templateHandler) addShortcodeVariant(ts *templateState) {
-	name := ts.Name()
-	base := templateBaseName(templateShortcode, name)
-
-	shortcodename, variants := templateNameAndVariants(base)
-
-	templs, found := t.shortcodes[shortcodename]
-	if !found {
-		templs = &shortcodeTemplates{}
-		t.shortcodes[shortcodename] = templs
-	}
-
-	sv := shortcodeVariant{variants: variants, ts: ts}
-
-	i := templs.indexOf(variants)
-
-	if i != -1 {
-		// Only replace if it's an override of an internal template.
-		if !isInternal(name) {
-			templs.variants[i] = sv
-		}
-	} else {
-		templs.variants = append(templs.variants, sv)
-	}
-}
-
-func (t *templateHandler) addTemplateFile(name string, fim hugofs.FileMetaInfo) error {
-	getTemplate := func(fim hugofs.FileMetaInfo) (templateInfo, error) {
-		meta := fim.Meta()
-		f, err := meta.Open()
-		if err != nil {
-			return templateInfo{meta: meta}, err
-		}
-		defer f.Close()
-		b, err := io.ReadAll(f)
-		if err != nil {
-			return templateInfo{meta: meta}, err
-		}
-
-		s := removeLeadingBOM(string(b))
-
-		var isText bool
-		name, isText = t.nameIsText(name)
-
-		return templateInfo{
-			name:     name,
-			isText:   isText,
-			template: s,
-			meta:     meta,
-		}, nil
-	}
-
-	tinfo, err := getTemplate(fim)
-	if err != nil {
-		return err
-	}
-
-	if isBaseTemplatePath(name) {
-		// Store it for later.
-		t.baseof[name] = tinfo
-		return nil
-	}
-
-	needsBaseof := !t.noBaseNeeded(name) && needsBaseTemplate(tinfo.template)
-	if needsBaseof {
-		t.needsBaseof[name] = tinfo
-		return nil
-	}
-
-	templ, err := t.addTemplateTo(tinfo, t.main)
-	if err != nil {
-		return tinfo.errWithFileContext("parse failed", err)
-	}
-
-	if _, err = t.applyTemplateTransformers(t.main, templ); err != nil {
-		return tinfo.errWithFileContext("transform failed", err)
-	}
-
-	return nil
-}
-
-func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace) (*templateState, error) {
-	return to.parse(info)
-}
-
-func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
-	if overlay.isText {
-		var (
-			templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
-			err   error
-		)
-
-		if !base.IsZero() {
-			templ, err = templ.Parse(base.template)
-			if err != nil {
-				return nil, base.errWithFileContext("text: base: parse failed", err)
-			}
-		}
-
-		templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
-		if err != nil {
-			return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
-		}
-
-		// The extra lookup is a workaround, see
-		// * https://github.com/golang/go/issues/16101
-		// * https://github.com/gohugoio/hugo/issues/2549
-		// templ = templ.Lookup(templ.Name())
-
-		return templ, nil
-	}
-
-	var (
-		templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
-		err   error
-	)
-
-	if !base.IsZero() {
-		templ, err = templ.Parse(base.template)
-		if err != nil {
-			return nil, base.errWithFileContext("html: base: parse failed", err)
-		}
-	}
-
-	templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
-	if err != nil {
-		return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
-	}
-
-	// The extra lookup is a workaround, see
-	// * https://github.com/golang/go/issues/16101
-	// * https://github.com/gohugoio/hugo/issues/2549
-	templ = templ.Lookup(templ.Name())
-
-	return templ, err
-}
-
-func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *templateState) (*templateContext, error) {
-	c, err := applyTemplateTransformers(ts, ns.newTemplateLookup(ts))
-	if err != nil {
-		return nil, err
-	}
-
-	for k := range c.templateNotFound {
-		t.transformNotFound[k] = ts
-	}
-
-	for k, v := range c.deferNodes {
-		if err = t.main.addDeferredTemplate(ts, k, v); err != nil {
-			return nil, err
-		}
-	}
-
-	return c, err
-}
-
-//go:embed all:embedded/templates/*
-//go:embed embedded/templates/_default/*
-//go:embed embedded/templates/_server/*
-var embeddedTemplatesFs embed.FS
-
-func (t *templateHandler) loadEmbedded() error {
-	return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-		if d == nil || d.IsDir() {
-			return nil
-		}
-
-		templb, err := embeddedTemplatesFs.ReadFile(path)
-		if err != nil {
-			return err
-		}
-
-		// Get the newlines on Windows in line with how we had it back when we used Go Generate
-		// to write the templates to Go files.
-		templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
-		name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
-		templateName := name
-
-		// For the render hooks and the server templates it does not make sense to preserve the
-		// double _internal double book-keeping,
-		// just add it if its now provided by the user.
-		if !strings.Contains(path, "_default/_markup") && !strings.HasPrefix(name, "_server/") && !strings.HasPrefix(name, "partials/_funcs/") {
-			templateName = internalPathPrefix + name
-		}
-
-		if _, found := t.Lookup(templateName); !found {
-			if err := t.AddTemplate(embeddedPathPrefix+templateName, templ); err != nil {
-				return err
-			}
-		}
-
-		if aliases, found := embeddedTemplatesAliases[name]; found {
-			// TODO(bep) avoid reparsing these aliases
-			for _, alias := range aliases {
-				alias = internalPathPrefix + alias
-				if err := t.AddTemplate(embeddedPathPrefix+alias, templ); err != nil {
-					return err
-				}
-			}
-		}
-
-		return nil
-	})
-}
-
-func (t *templateHandler) loadTemplates() error {
-	walker := func(path string, fi hugofs.FileMetaInfo) error {
-		if fi.IsDir() {
-			return nil
-		}
-
-		if isDotFile(path) || isBackupFile(path) {
-			return nil
-		}
-
-		name := strings.TrimPrefix(filepath.ToSlash(path), "/")
-		filename := filepath.Base(path)
-		outputFormats := t.Conf.GetConfigSection("outputFormats").(output.Formats)
-		outputFormat, found := outputFormats.FromFilename(filename)
-
-		if found && outputFormat.IsPlainText {
-			name = textTmplNamePrefix + name
-		}
-
-		if err := t.addTemplateFile(name, fi); err != nil {
-			return err
-		}
-
-		return nil
-	}
-
-	if err := helpers.Walk(t.Layouts.Fs, "", walker); err != nil {
-		if !herrors.IsNotExist(err) {
-			return err
-		}
-		return nil
-	}
-
-	return nil
-}
-
-func (t *templateHandler) nameIsText(name string) (string, bool) {
-	isText := strings.HasPrefix(name, textTmplNamePrefix)
-	if isText {
-		name = strings.TrimPrefix(name, textTmplNamePrefix)
-	}
-	return name, isText
-}
-
-func (t *templateHandler) noBaseNeeded(name string) bool {
-	if strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/") {
-		return true
-	}
-	return strings.Contains(name, "_markup/")
-}
-
-func (t *templateHandler) extractPartials(templ tpl.Template) error {
-	templs := templates(templ)
-	for _, templ := range templs {
-		if templ.Name() == "" || !strings.HasPrefix(templ.Name(), "partials/") {
-			continue
-		}
-
-		ts := newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
-		ts.typ = templatePartial
-
-		t.main.mu.RLock()
-		_, found := t.main.templates[templ.Name()]
-		t.main.mu.RUnlock()
-
-		if !found {
-			t.main.mu.Lock()
-			// This is a template defined inline.
-			_, err := applyTemplateTransformers(ts, t.main.newTemplateLookup(ts))
-			if err != nil {
-				t.main.mu.Unlock()
-				return err
-			}
-			t.main.templates[templ.Name()] = ts
-			t.main.mu.Unlock()
-
-		}
-	}
-
-	return nil
-}
-
-func (t *templateHandler) postTransform() error {
-	defineCheckedHTML := false
-	defineCheckedText := false
-
-	for _, v := range t.main.templates {
-		if v.typ == templateShortcode {
-			t.addShortcodeVariant(v)
-		}
-
-		if defineCheckedHTML && defineCheckedText {
-			continue
-		}
-
-		isText := isText(v.Template)
-		if isText {
-			if defineCheckedText {
-				continue
-			}
-			defineCheckedText = true
-		} else {
-			if defineCheckedHTML {
-				continue
-			}
-			defineCheckedHTML = true
-		}
-
-		if err := t.extractPartials(v.Template); err != nil {
-			return err
-		}
-	}
-
-	for name, source := range t.transformNotFound {
-		lookup := t.main.newTemplateLookup(source)
-		templ := lookup(name)
-		if templ != nil {
-			_, err := applyTemplateTransformers(templ, lookup)
-			if err != nil {
-				return err
-			}
-		}
-	}
-
-	for _, v := range t.shortcodes {
-		sort.Slice(v.variants, func(i, j int) bool {
-			v1, v2 := v.variants[i], v.variants[j]
-			name1, name2 := v1.ts.Name(), v2.ts.Name()
-			isHTMl1, isHTML2 := strings.HasSuffix(name1, "html"), strings.HasSuffix(name2, "html")
-
-			// There will be a weighted selection later, but make
-			// sure these are sorted to get a stable selection for
-			// output formats missing specific templates.
-			// Prefer HTML.
-			if isHTMl1 || isHTML2 && !(isHTMl1 && isHTML2) {
-				return isHTMl1
-			}
-
-			return name1 < name2
-		})
-	}
-
-	return nil
-}
-
-type prototypeCloneID uint16
-
-const (
-	prototypeCloneIDBaseof prototypeCloneID = iota + 1
-	prototypeCloneIDDefer
-)
-
-type templateNamespace struct {
-	prototypeText *texttemplate.Template
-	prototypeHTML *htmltemplate.Template
-
-	prototypeHTMLCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template]
-	prototypeTextCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template]
-
-	*templateStateMap
-}
-
-func (t *templateNamespace) getPrototypeText(id prototypeCloneID) *texttemplate.Template {
-	v, ok := t.prototypeTextCloneCache.Get(id)
-	if !ok {
-		return t.prototypeText
-	}
-	return v
-}
-
-func (t *templateNamespace) getPrototypeHTML(id prototypeCloneID) *htmltemplate.Template {
-	v, ok := t.prototypeHTMLCloneCache.Get(id)
-	if !ok {
-		return t.prototypeHTML
-	}
-	return v
-}
-
-func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
-	t.mu.RLock()
-	defer t.mu.RUnlock()
-
-	templ, found := t.templates[name]
-	if !found {
-		return nil, false
-	}
-
-	return templ, found
-}
-
-func (t *templateNamespace) createPrototypes() error {
-	for _, id := range []prototypeCloneID{prototypeCloneIDBaseof, prototypeCloneIDDefer} {
-		t.prototypeHTMLCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone()))
-		t.prototypeTextCloneCache.Set(id, texttemplate.Must(t.prototypeText.Clone()))
-	}
-	return nil
-}
-
-func (t *templateNamespace) newTemplateLookup(in *templateState) func(name string) *templateState {
-	return func(name string) *templateState {
-		if templ, found := t.templates[name]; found {
-			if templ.isText() != in.isText() {
-				return nil
-			}
-			return templ
-		}
-		if templ, found := findTemplateIn(name, in); found {
-			return newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
-		}
-		return nil
-	}
-}
-
-func (t *templateNamespace) addDeferredTemplate(owner *templateState, name string, n *parse.ListNode) error {
-	t.mu.Lock()
-	defer t.mu.Unlock()
-
-	if _, found := t.templates[name]; found {
-		return nil
-	}
-
-	var templ tpl.Template
-
-	if owner.isText() {
-		prototype := t.getPrototypeText(prototypeCloneIDDefer)
-		tt, err := prototype.New(name).Parse("")
-		if err != nil {
-			return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
-		}
-		tt.Tree.Root = n
-		templ = tt
-	} else {
-		prototype := t.getPrototypeHTML(prototypeCloneIDDefer)
-		tt, err := prototype.New(name).Parse("")
-		if err != nil {
-			return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
-		}
-		tt.Tree.Root = n
-		templ = tt
-	}
-
-	dts := newTemplateState(owner, templ, templateInfo{name: name}, nil)
-	t.templates[name] = dts
-
-	return nil
-}
-
-func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
-	t.mu.Lock()
-	defer t.mu.Unlock()
-
-	if info.isText {
-		prototype := t.prototypeText
-
-		templ, err := prototype.New(info.name).Parse(info.template)
-		if err != nil {
-			return nil, err
-		}
-
-		ts := newTemplateState(nil, templ, info, nil)
-
-		t.templates[info.name] = ts
-
-		return ts, nil
-	}
-
-	prototype := t.prototypeHTML
-
-	templ, err := prototype.New(info.name).Parse(info.template)
-	if err != nil {
-		return nil, err
-	}
-
-	ts := newTemplateState(nil, templ, info, nil)
-
-	t.templates[info.name] = ts
-
-	return ts, nil
-}
-
-var _ tpl.IsInternalTemplateProvider = (*templateState)(nil)
-
-type templateState struct {
-	tpl.Template
-
-	// Set for deferred templates.
-	owner *templateState
-
-	typ       templateType
-	parseInfo tpl.ParseInfo
-	id        identity.Identity
-
-	info     templateInfo
-	baseInfo templateInfo // Set when a base template is used.
-}
-
-func (t *templateState) IsInternalTemplate() bool {
-	return t.info.isEmbedded
-}
-
-func (t *templateState) GetIdentity() identity.Identity {
-	return t.id
-}
-
-func (t *templateState) ParseInfo() tpl.ParseInfo {
-	return t.parseInfo
-}
-
-func (t *templateState) isText() bool {
-	return isText(t.Template)
-}
-
-func (t *templateState) String() string {
-	return t.Name()
-}
-
-func isText(templ tpl.Template) bool {
-	_, isText := templ.(*texttemplate.Template)
-	return isText
-}
-
-type templateStateMap struct {
-	mu        sync.RWMutex
-	templates map[string]*templateState
-}
-
-type textTemplateWrapperWithLock struct {
-	*sync.RWMutex
-	*texttemplate.Template
-}
-
-func (t *textTemplateWrapperWithLock) Lookup(name string) (tpl.Template, bool) {
-	t.RLock()
-	templ := t.Template.Lookup(name)
-	t.RUnlock()
-	if templ == nil {
-		return nil, false
-	}
-	return &textTemplateWrapperWithLock{
-		RWMutex:  t.RWMutex,
-		Template: templ,
-	}, true
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
-	panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariants(name string) []tpl.Template {
-	panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) Parse(name, tpl string) (tpl.Template, error) {
-	t.Lock()
-	defer t.Unlock()
-	return t.Template.New(name).Parse(tpl)
-}
-
-func isBackupFile(path string) bool {
-	return path[len(path)-1] == '~'
-}
-
-func isBaseTemplatePath(path string) bool {
-	return strings.Contains(filepath.Base(path), baseFileBase)
-}
-
-func isDotFile(path string) bool {
-	return filepath.Base(path)[0] == '.'
-}
-
-func removeLeadingBOM(s string) string {
-	const bom = '\ufeff'
-
-	for i, r := range s {
-		if i == 0 && r != bom {
-			return s
-		}
-		if i > 0 {
-			return s[i:]
-		}
-	}
-
-	return s
-}
-
-// resolves _internal/shortcodes/param.html => param.html etc.
-func templateBaseName(typ templateType, name string) string {
-	name = strings.TrimPrefix(name, internalPathPrefix)
-	switch typ {
-	case templateShortcode:
-		return strings.TrimPrefix(name, shortcodesPathPrefix)
-	default:
-		panic("not implemented")
-	}
-}
-
-func unwrap(templ tpl.Template) tpl.Template {
-	if ts, ok := templ.(*templateState); ok {
-		return ts.Template
-	}
-	return templ
-}
-
-func templates(in tpl.Template) []tpl.Template {
-	var templs []tpl.Template
-	in = unwrap(in)
-	if textt, ok := in.(*texttemplate.Template); ok {
-		for _, t := range textt.Templates() {
-			templs = append(templs, t)
-		}
-	}
-
-	if htmlt, ok := in.(*htmltemplate.Template); ok {
-		for _, t := range htmlt.Templates() {
-			templs = append(templs, t)
-		}
-	}
-
-	return templs
-}
diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go
deleted file mode 100644
index 96404f51b29..00000000000
--- a/tpl/tplimpl/templateFuncster.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// 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 tplimpl
diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go
deleted file mode 100644
index 435868964a4..00000000000
--- a/tpl/tplimpl/templateProvider.go
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2017-present 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 tplimpl
-
-import (
-	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/tpl"
-)
-
-// TemplateProvider manages templates.
-type TemplateProvider struct{}
-
-// DefaultTemplateProvider is a globally available TemplateProvider.
-var DefaultTemplateProvider *TemplateProvider
-
-// Update updates the Hugo Template System in the provided Deps
-// with all the additional features, templates & functions.
-func (*TemplateProvider) NewResource(dst *deps.Deps) error {
-	handlers, err := newTemplateHandlers(dst)
-	if err != nil {
-		return err
-	}
-	dst.SetTempl(handlers)
-	return nil
-}
-
-// Clone clones.
-func (*TemplateProvider) CloneResource(dst, src *deps.Deps) error {
-	t := src.Tmpl().(*templateExec)
-	c := t.Clone(dst)
-	funcMap := make(map[string]any)
-	for k, v := range c.funcs {
-		funcMap[k] = v.Interface()
-	}
-	dst.SetTempl(&tpl.TemplateHandlers{
-		Tmpl:    c,
-		TxtTmpl: newStandaloneTextTemplate(funcMap),
-	})
-	return nil
-}
diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go
deleted file mode 100644
index a9d25922005..00000000000
--- a/tpl/tplimpl/template_errors.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2018 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 tplimpl
-
-import (
-	"fmt"
-
-	"github.com/gohugoio/hugo/common/herrors"
-	"github.com/gohugoio/hugo/hugofs"
-	"github.com/gohugoio/hugo/identity"
-)
-
-var _ identity.Identity = (*templateInfo)(nil)
-
-type templateInfo struct {
-	name       string
-	template   string
-	isText     bool // HTML or plain text template.
-	isEmbedded bool
-
-	meta *hugofs.FileMeta
-}
-
-func (t templateInfo) IdentifierBase() string {
-	return t.name
-}
-
-func (t templateInfo) Name() string {
-	return t.name
-}
-
-func (t templateInfo) Filename() string {
-	return t.meta.Filename
-}
-
-func (t templateInfo) IsZero() bool {
-	return t.name == ""
-}
-
-func (t templateInfo) resolveType() templateType {
-	return resolveTemplateType(t.name)
-}
-
-func (info templateInfo) errWithFileContext(what string, err error) error {
-	err = fmt.Errorf(what+": %w", err)
-	fe := herrors.NewFileErrorFromName(err, info.meta.Filename)
-	f, err := info.meta.Open()
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-	return fe.UpdateContent(f, nil)
-}
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
index b181db061d7..18d4d4a2796 100644
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -1,4 +1,4 @@
-// Copyright 2017-present The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
 //
 // Portions Copyright The Go Authors.
 
@@ -25,46 +25,7 @@ import (
 	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/tpl"
 
-	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
 	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
-	"github.com/gohugoio/hugo/deps"
-
-	"github.com/gohugoio/hugo/tpl/internal"
-
-	// Init the namespaces
-	_ "github.com/gohugoio/hugo/tpl/cast"
-	_ "github.com/gohugoio/hugo/tpl/collections"
-	_ "github.com/gohugoio/hugo/tpl/compare"
-	_ "github.com/gohugoio/hugo/tpl/crypto"
-	_ "github.com/gohugoio/hugo/tpl/css"
-	_ "github.com/gohugoio/hugo/tpl/data"
-	_ "github.com/gohugoio/hugo/tpl/debug"
-	_ "github.com/gohugoio/hugo/tpl/diagrams"
-	_ "github.com/gohugoio/hugo/tpl/encoding"
-	_ "github.com/gohugoio/hugo/tpl/fmt"
-	_ "github.com/gohugoio/hugo/tpl/hash"
-	_ "github.com/gohugoio/hugo/tpl/hugo"
-	_ "github.com/gohugoio/hugo/tpl/images"
-	_ "github.com/gohugoio/hugo/tpl/inflect"
-	_ "github.com/gohugoio/hugo/tpl/js"
-	_ "github.com/gohugoio/hugo/tpl/lang"
-	_ "github.com/gohugoio/hugo/tpl/math"
-	_ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
-	_ "github.com/gohugoio/hugo/tpl/os"
-	_ "github.com/gohugoio/hugo/tpl/page"
-	_ "github.com/gohugoio/hugo/tpl/partials"
-	_ "github.com/gohugoio/hugo/tpl/path"
-	_ "github.com/gohugoio/hugo/tpl/reflect"
-	_ "github.com/gohugoio/hugo/tpl/resources"
-	_ "github.com/gohugoio/hugo/tpl/safe"
-	_ "github.com/gohugoio/hugo/tpl/site"
-	_ "github.com/gohugoio/hugo/tpl/strings"
-	_ "github.com/gohugoio/hugo/tpl/templates"
-	_ "github.com/gohugoio/hugo/tpl/time"
-	_ "github.com/gohugoio/hugo/tpl/transform"
-	_ "github.com/gohugoio/hugo/tpl/urls"
-	maps0 "maps"
 )
 
 var (
@@ -212,89 +173,3 @@ func (t *templateExecHelper) trackDependencies(ctx context.Context, tmpl texttem
 
 	return ctx
 }
-
-func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
-	funcs := createFuncMap(d)
-	funcsv := make(map[string]reflect.Value)
-
-	for k, v := range funcs {
-		vv := reflect.ValueOf(v)
-		funcsv[k] = vv
-	}
-
-	// Duplicate Go's internal funcs here for faster lookups.
-	for k, v := range template.GoFuncs {
-		if _, exists := funcsv[k]; !exists {
-			vv, ok := v.(reflect.Value)
-			if !ok {
-				vv = reflect.ValueOf(v)
-			}
-			funcsv[k] = vv
-		}
-	}
-
-	for k, v := range texttemplate.GoFuncs {
-		if _, exists := funcsv[k]; !exists {
-			funcsv[k] = v
-		}
-	}
-
-	exeHelper := &templateExecHelper{
-		watching:   d.Conf.Watching(),
-		funcs:      funcsv,
-		site:       reflect.ValueOf(d.Site),
-		siteParams: reflect.ValueOf(d.Site.Params()),
-	}
-
-	return texttemplate.NewExecuter(
-		exeHelper,
-	), funcsv
-}
-
-func createFuncMap(d *deps.Deps) map[string]any {
-	if d.TmplFuncMap != nil {
-		return d.TmplFuncMap
-	}
-	funcMap := template.FuncMap{}
-
-	nsMap := make(map[string]any)
-	var onCreated []func(namespaces map[string]any)
-
-	// Merge the namespace funcs
-	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-		ns := nsf(d)
-		if _, exists := funcMap[ns.Name]; exists {
-			panic(ns.Name + " is a duplicate template func")
-		}
-		funcMap[ns.Name] = ns.Context
-		contextV, err := ns.Context(context.Background())
-		if err != nil {
-			panic(err)
-		}
-		nsMap[ns.Name] = contextV
-		for _, mm := range ns.MethodMappings {
-			for _, alias := range mm.Aliases {
-				if _, exists := funcMap[alias]; exists {
-					panic(alias + " is a duplicate template func")
-				}
-				funcMap[alias] = mm.Method
-			}
-		}
-
-		if ns.OnCreated != nil {
-			onCreated = append(onCreated, ns.OnCreated)
-		}
-	}
-
-	for _, f := range onCreated {
-		f(nsMap)
-	}
-
-	if d.OverloadedTemplateFuncs != nil {
-		maps0.Copy(funcMap, d.OverloadedTemplateFuncs)
-	}
-
-	d.TmplFuncMap = funcMap
-
-	return d.TmplFuncMap
-}
diff --git a/tpl/template_info.go b/tpl/tplimpl/template_info.go
similarity index 73%
rename from tpl/template_info.go
rename to tpl/tplimpl/template_info.go
index fd126d80f64..0bc807d910a 100644
--- a/tpl/template_info.go
+++ b/tpl/tplimpl/template_info.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 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.
@@ -11,24 +11,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package tpl
+package tplimpl
 
 // Increments on breaking changes.
 const TemplateVersion = 2
 
-type Info interface {
-	ParseInfo() ParseInfo
-}
-
-type FileInfo interface {
-	Name() string
-	Filename() string
-}
-
-type IsInternalTemplateProvider interface {
-	IsInternalTemplate() bool
-}
-
+// ParseInfo holds information about a parsed ntemplate.
 type ParseInfo struct {
 	// Set for shortcode templates with any {{ .Inner }}
 	IsInner bool
@@ -44,14 +32,15 @@ func (info ParseInfo) IsZero() bool {
 	return info.Config.Version == 0
 }
 
+// ParseConfig holds configuration extracted from the template.
 type ParseConfig struct {
 	Version int
 }
 
-var DefaultParseConfig = ParseConfig{
+var defaultParseConfig = ParseConfig{
 	Version: TemplateVersion,
 }
 
-var DefaultParseInfo = ParseInfo{
-	Config: DefaultParseConfig,
+var defaultParseInfo = ParseInfo{
+	Config: defaultParseConfig,
 }
diff --git a/tpl/tplimpl/templatedescriptor.go b/tpl/tplimpl/templatedescriptor.go
new file mode 100644
index 00000000000..21649b0327b
--- /dev/null
+++ b/tpl/tplimpl/templatedescriptor.go
@@ -0,0 +1,225 @@
+// Copyright 2025 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 tplimpl
+
+import (
+	"github.com/gohugoio/hugo/resources/kinds"
+)
+
+const baseNameBaseof = "baseof"
+
+// This is used both as a key and in lookups.
+type TemplateDescriptor struct {
+	// Group 1.
+	Kind   string // page, home, section, taxonomy, term (and only those)
+	Layout string // list, single, baseof, mycustomlayout.
+
+	// Group 2.
+	OutputFormat string // rss, csv ...
+	MediaType    string // text/html, text/plain, ...
+	Lang         string // en, nn, fr, ...
+
+	Variant1 string // contextual variant, e.g. "link" in render hooks."
+	Variant2 string // contextual variant, e.g. "id" in render.
+
+	// Misc.
+	LayoutMustMatch bool // If set, we only look for the exact layout.
+	IsPlainText     bool // Whether this is a plain text template.
+}
+
+func (d *TemplateDescriptor) normalizeFromFile() {
+	// fmt.Println("normalizeFromFile", "kind:", d.Kind, "layout:", d.Layout, "of:", d.OutputFormat)
+
+	if d.Layout == d.OutputFormat {
+		d.Layout = ""
+	}
+
+	if d.Kind == kinds.KindTemporary {
+		d.Kind = ""
+	}
+
+	if d.Layout == d.Kind {
+		d.Layout = ""
+	}
+}
+
+type descriptorHandler struct {
+	opts StoreOptions
+}
+
+// Note that this in this setup is usually a descriptor constructed from a page,
+// so we want to find the best match for that page.
+func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor) weight {
+	if this.LayoutMustMatch && this.Layout != other.Layout {
+		return weightNoMatch
+	}
+
+	w := this.doCompare(category, other)
+	if w.w1 <= 0 {
+		if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
+			// See issue 13242.
+			if this.OutputFormat != other.OutputFormat && this.OutputFormat == s.opts.DefaultOutputFormat {
+				return w
+			}
+
+			w.w1 = 1
+			return w
+		}
+	}
+
+	return w
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) doCompare(category Category, other TemplateDescriptor) weight {
+	w := weightNoMatch
+
+	// HTML in plain text is OK, but not the other way around.
+	if other.IsPlainText && !this.IsPlainText {
+		return w
+	}
+	if other.Kind != "" && other.Kind != this.Kind {
+		return w
+	}
+	if other.Layout != "" && other.Layout != layoutAll && other.Layout != this.Layout {
+		if isLayoutCustom(this.Layout) {
+			if this.Kind == "" {
+				this.Layout = ""
+			} else if this.Kind == kinds.KindPage {
+				this.Layout = layoutSingle
+			} else {
+				this.Layout = layoutList
+			}
+		}
+
+		// Test again.
+		if other.Layout != this.Layout {
+			return w
+		}
+	}
+	if other.Lang != "" && other.Lang != this.Lang {
+		return w
+	}
+
+	if other.OutputFormat != "" && other.OutputFormat != this.OutputFormat {
+		if this.MediaType != other.MediaType {
+			return w
+		}
+
+		// We want e.g. home page in amp output format (media type text/html) to
+		// find a template even if one isn't specified for that output format,
+		// when one exist for the html output format (same media type).
+		if category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && this.Layout != other.Layout)) {
+			return w
+		}
+
+		// Continue.
+	}
+
+	// One example of variant1 and 2 is for render codeblocks:
+	// variant1=codeblock, variant2=go (language).
+	if other.Variant1 != "" && other.Variant1 != this.Variant1 {
+		return w
+	}
+
+	// If both are set and different, no match.
+	if other.Variant2 != "" && this.Variant2 != "" && other.Variant2 != this.Variant2 {
+		return w
+	}
+
+	const (
+		weightKind         = 3 // page, home, section, taxonomy, term (and only those)
+		weightcustomLayout = 4 // custom layout (mylayout, set in e.g. front matter)
+		weightLayout       = 2 // standard layouts (single,list,all)
+		weightOutputFormat = 2 // a configured output format (e.g. rss, html, json)
+		weightMediaType    = 1 // a configured media type (e.g. text/html, text/plain)
+		weightLang         = 1 // a configured language (e.g. en, nn, fr, ...)
+		weightVariant1     = 4 // currently used for render hooks, e.g. "link", "image"
+		weightVariant2     = 2 // currently used for render hooks, e.g. the language "go" in code blocks.
+
+		// We will use the values for group 2 and 3
+		// if the distance up to the template is shorter than
+		// the one we're comparing with.
+		// E.g for a page in /posts/mypage.md with the
+		// two templates /layouts/posts/single.html and /layouts/page.html,
+		// the first one is the best match even if the second one
+		// has a higher w1 value.
+		weight2Group1 = 1 // kind, standardl layout (single,list,all)
+		weight2Group2 = 2 // custom layout (mylayout)
+
+		weight3 = 1 // for media type, lang, output format.
+	)
+
+	// Now we now know that the other descriptor is a subset of this.
+	// Now calculate the weights.
+	w.w1++
+
+	if other.Kind != "" && other.Kind == this.Kind {
+		w.w1 += weightKind
+		w.w2 = weight2Group1
+	}
+
+	if other.Layout != "" && other.Layout == this.Layout || other.Layout == layoutAll {
+		if isLayoutCustom(this.Layout) {
+			w.w1 += weightcustomLayout
+			w.w2 = weight2Group2
+		} else {
+			w.w1 += weightLayout
+			w.w2 = weight2Group1
+		}
+	}
+
+	if other.Lang != "" && other.Lang == this.Lang {
+		w.w1 += weightLang
+		w.w3 += weight3
+	}
+
+	if other.OutputFormat != "" && other.OutputFormat == this.OutputFormat {
+		w.w1 += weightOutputFormat
+		w.w3 += weight3
+	}
+
+	if other.MediaType != "" && other.MediaType == this.MediaType {
+		w.w1 += weightMediaType
+		w.w3 += weight3
+	}
+
+	if other.Variant1 != "" && other.Variant1 == this.Variant1 {
+		w.w1 += weightVariant1
+	}
+
+	if other.Variant2 != "" && other.Variant2 == this.Variant2 {
+		w.w1 += weightVariant2
+	}
+	if other.Variant2 != "" && this.Variant2 == "" {
+		w.w1--
+	}
+
+	return w
+}
+
+func (d TemplateDescriptor) IsZero() bool {
+	return d == TemplateDescriptor{}
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) isKindInLayout(layout string) bool {
+	if this.Kind == "" {
+		return true
+	}
+	if this.Kind != kinds.KindPage {
+		return layout != layoutSingle
+	}
+	return layout != layoutList
+}
diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go
new file mode 100644
index 00000000000..57012f6e3d8
--- /dev/null
+++ b/tpl/tplimpl/templates.go
@@ -0,0 +1,331 @@
+package tplimpl
+
+import (
+	"io"
+	"regexp"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+
+	"github.com/gohugoio/hugo/common/paths"
+	"github.com/gohugoio/hugo/tpl"
+	htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+)
+
+func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
+	if err := func() error {
+		meta := templ.Fi.Meta()
+		f, err := meta.Open()
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+		b, err := io.ReadAll(f)
+		if err != nil {
+			return err
+		}
+		templ.Content = removeLeadingBOM(string(b))
+		if !templ.NoBaseOf {
+			templ.NoBaseOf = !needsBaseTemplate(templ.Content)
+		}
+		return nil
+	}(); err != nil {
+		return err
+	}
+	return nil
+}
+
+// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
+// in v0.141.0. We can remove these aliases in v0.155.0 or later.
+var embeddedTemplatesAliases = map[string][]string{
+	"_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
+}
+
+func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
+	if !ti.NoBaseOf || ti.Category == CategoryBaseof {
+		// Delay parsing until we have the base template.
+		return nil
+	}
+	pi := ti.PathInfo
+	name := pi.PathNoLeadingSlash()
+	if ti.isLegacyMapped {
+		// When mapping the old taxonomy structure to the new one, we may map the same path to multiple templates per kind.
+		// Append the kind here to make the name unique.
+		name += ("-" + ti.D.Kind)
+	}
+
+	var (
+		templ tpl.Template
+		err   error
+	)
+
+	if ti.D.IsPlainText {
+		prototype := t.parseText
+		templ, err = prototype.New(name).Parse(ti.Content)
+		if err != nil {
+			return err
+		}
+	} else {
+		prototype := t.parseHTML
+		templ, err = prototype.New(name).Parse(ti.Content)
+		if err != nil {
+			return err
+		}
+
+		if ti.SubCategory == SubCategoryEmbedded {
+			// In Hugo 0.146.0 we moved the internal templates around.
+			// For the "_internal/twitter_cards.html" style templates, they
+			// were moved to the _partials directory.
+			// But we need to make them accessible from the old path for a while.
+			if pi.Type() == paths.TypePartial {
+				aliasName := strings.TrimPrefix(name, "_partials/")
+				aliasName = "_internal/" + aliasName
+				_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+				if err != nil {
+					return err
+				}
+			}
+
+			// This was also possible before Hugo 0.146.0, but this should be deprecated.
+			if pi.Type() == paths.TypeShortcode {
+				aliasName := strings.TrimPrefix(name, "_shortcodes/")
+				aliasName = "_internal/shortcodes/" + aliasName
+				_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+				if err != nil {
+					return err
+				}
+			}
+
+		}
+	}
+
+	ti.Template = templ
+
+	return nil
+}
+
+func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTemplateInfo) error {
+	tb := &TemplWithBaseApplied{
+		Overlay: overlay,
+		Base:    base.Info,
+	}
+
+	base.Info.Overlays = append(base.Info.Overlays, overlay)
+
+	var templ tpl.Template
+	if overlay.D.IsPlainText {
+		tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
+		var err error
+		tt, err = tt.Parse(base.Info.Content)
+		if err != nil {
+			return err
+		}
+		tt, err = tt.Parse(overlay.Content)
+		if err != nil {
+			return err
+		}
+		templ = tt
+		t.baseofTextClones = append(t.baseofTextClones, tt)
+	} else {
+		tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
+		var err error
+		tt, err = tt.Parse(base.Info.Content)
+		if err != nil {
+			return err
+		}
+		tt, err = tt.Parse(overlay.Content)
+		if err != nil {
+			return err
+		}
+		templ = tt
+
+		t.baseofHtmlClones = append(t.baseofHtmlClones, tt)
+
+	}
+
+	tb.Template = &TemplInfo{
+		Template: templ,
+		Base:     base.Info,
+		PathInfo: overlay.PathInfo,
+		Fi:       overlay.Fi,
+		D:        overlay.D,
+		NoBaseOf: true,
+	}
+
+	variants := overlay.BaseVariants.Get(base.Key)
+	if variants == nil {
+		variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
+		overlay.BaseVariants.Insert(base.Key, variants)
+	}
+	variants[base.Info.D] = tb
+	return nil
+}
+
+func (t *templateNamespace) templatesIn(in tpl.Template) []tpl.Template {
+	var templs []tpl.Template
+	if textt, ok := in.(*texttemplate.Template); ok {
+		for _, t := range textt.Templates() {
+			templs = append(templs, t)
+		}
+	}
+	if htmlt, ok := in.(*htmltemplate.Template); ok {
+		for _, t := range htmlt.Templates() {
+			templs = append(templs, t)
+		}
+	}
+	return templs
+}
+
+/*
+
+
+func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
+	if overlay.isText {
+		var (
+			templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
+			err   error
+		)
+
+		if !base.IsZero() {
+			templ, err = templ.Parse(base.template)
+			if err != nil {
+				return nil, base.errWithFileContext("text: base: parse failed", err)
+			}
+		}
+
+		templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
+		if err != nil {
+			return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
+		}
+
+		// The extra lookup is a workaround, see
+		// * https://github.com/golang/go/issues/16101
+		// * https://github.com/gohugoio/hugo/issues/2549
+		// templ = templ.Lookup(templ.Name())
+
+		return templ, nil
+	}
+
+	var (
+		templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
+		err   error
+	)
+
+	if !base.IsZero() {
+		templ, err = templ.Parse(base.template)
+		if err != nil {
+			return nil, base.errWithFileContext("html: base: parse failed", err)
+		}
+	}
+
+	templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
+	if err != nil {
+		return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
+	}
+
+	// The extra lookup is a workaround, see
+	// * https://github.com/golang/go/issues/16101
+	// * https://github.com/gohugoio/hugo/issues/2549
+	templ = templ.Lookup(templ.Name())
+
+	return templ, err
+}
+
+*/
+
+var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
+
+// needsBaseTemplate returns true if the first non-comment template block is a
+// define block.
+func needsBaseTemplate(templ string) bool {
+	idx := -1
+	inComment := false
+	for i := 0; i < len(templ); {
+		if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
+			inComment = true
+			i += 4
+		} else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
+			inComment = true
+			i += 6
+		} else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
+			inComment = false
+			i += 4
+		} else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
+			inComment = false
+			i += 6
+		} else {
+			r, size := utf8.DecodeRuneInString(templ[i:])
+			if !inComment {
+				if strings.HasPrefix(templ[i:], "{{") {
+					idx = i
+					break
+				} else if !unicode.IsSpace(r) {
+					break
+				}
+			}
+			i += size
+		}
+	}
+
+	if idx == -1 {
+		return false
+	}
+
+	return baseTemplateDefineRe.MatchString(templ[idx:])
+}
+
+func removeLeadingBOM(s string) string {
+	const bom = '\ufeff'
+
+	for i, r := range s {
+		if i == 0 && r != bom {
+			return s
+		}
+		if i > 0 {
+			return s[i:]
+		}
+	}
+
+	return s
+}
+
+type templateNamespace struct {
+	parseText     *texttemplate.Template
+	parseHTML     *htmltemplate.Template
+	prototypeText *texttemplate.Template
+	prototypeHTML *htmltemplate.Template
+
+	standaloneText *texttemplate.Template
+
+	baseofTextClones []*texttemplate.Template
+	baseofHtmlClones []*htmltemplate.Template
+}
+
+func (t *templateNamespace) createPrototypesParse() error {
+	if t.prototypeHTML == nil {
+		panic("prototypeHTML not set")
+	}
+	t.parseHTML = htmltemplate.Must(t.prototypeHTML.Clone())
+	t.parseText = texttemplate.Must(t.prototypeText.Clone())
+	return nil
+}
+
+func (t *templateNamespace) createPrototypes(init bool) error {
+	if init {
+		t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
+		t.prototypeText = texttemplate.Must(t.parseText.Clone())
+	}
+	// t.execHTML = htmltemplate.Must(t.parseHTML.Clone())
+	// t.execText = texttemplate.Must(t.parseText.Clone())
+
+	return nil
+}
+
+func newTemplateNamespace(funcs map[string]any) *templateNamespace {
+	return &templateNamespace{
+		parseHTML:      htmltemplate.New("").Funcs(funcs),
+		parseText:      texttemplate.New("").Funcs(funcs),
+		standaloneText: texttemplate.New("").Funcs(funcs),
+	}
+}
diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/templatetransform.go
similarity index 74%
rename from tpl/tplimpl/template_ast_transformers.go
rename to tpl/tplimpl/templatetransform.go
index 4deadd05279..f9ce298de42 100644
--- a/tpl/tplimpl/template_ast_transformers.go
+++ b/tpl/tplimpl/templatetransform.go
@@ -1,48 +1,27 @@
-// Copyright 2016 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 tplimpl
 
 import (
 	"errors"
 	"fmt"
+	"slices"
 	"strings"
 
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+
 	htmltemplate "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/common/hashing"
 	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/tpl"
 	"github.com/mitchellh/mapstructure"
-	"slices"
 )
 
-type templateType int
-
-const (
-	templateUndefined templateType = iota
-	templateShortcode
-	templatePartial
-)
-
-type templateContext struct {
+type templateTransformContext struct {
 	visited          map[string]bool
 	templateNotFound map[string]bool
 	deferNodes       map[string]*parse.ListNode
-	lookupFn         func(name string) *templateState
+	lookupFn         func(name string, in *TemplInfo) *TemplInfo
 
 	// The last error encountered.
 	err error
@@ -50,18 +29,18 @@ type templateContext struct {
 	// Set when we're done checking for config header.
 	configChecked bool
 
-	t *templateState
+	t *TemplInfo
 
 	// Store away the return node in partials.
 	returnNode *parse.CommandNode
 }
 
-func (c templateContext) getIfNotVisited(name string) *templateState {
+func (c templateTransformContext) getIfNotVisited(name string) *TemplInfo {
 	if c.visited[name] {
 		return nil
 	}
 	c.visited[name] = true
-	templ := c.lookupFn(name)
+	templ := c.lookupFn(name, c.t)
 	if templ == nil {
 		// This may be a inline template defined outside of this file
 		// and not yet parsed. Unusual, but it happens.
@@ -72,11 +51,11 @@ func (c templateContext) getIfNotVisited(name string) *templateState {
 	return templ
 }
 
-func newTemplateContext(
-	t *templateState,
-	lookupFn func(name string) *templateState,
-) *templateContext {
-	return &templateContext{
+func newTemplateTransformContext(
+	t *TemplInfo,
+	lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) *templateTransformContext {
+	return &templateTransformContext{
 		t:                t,
 		lookupFn:         lookupFn,
 		visited:          make(map[string]bool),
@@ -86,21 +65,25 @@ func newTemplateContext(
 }
 
 func applyTemplateTransformers(
-	t *templateState,
-	lookupFn func(name string) *templateState,
-) (*templateContext, error) {
+	t *TemplInfo,
+	lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) (*templateTransformContext, error) {
 	if t == nil {
 		return nil, errors.New("expected template, but none provided")
 	}
 
-	c := newTemplateContext(t, lookupFn)
+	c := newTemplateTransformContext(t, lookupFn)
+	c.t.ParseInfo = defaultParseInfo
 	tree := getParseTree(t.Template)
+	if tree == nil {
+		panic(fmt.Errorf("template %s not parsed", t))
+	}
 
 	_, err := c.applyTransformations(tree.Root)
 
 	if err == nil && c.returnNode != nil {
 		// This is a partial with a return statement.
-		c.t.parseInfo.HasReturn = true
+		c.t.ParseInfo.HasReturn = true
 		tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
 	}
 
@@ -108,7 +91,6 @@ func applyTemplateTransformers(
 }
 
 func getParseTree(templ tpl.Template) *parse.Tree {
-	templ = unwrap(templ)
 	if text, ok := templ.(*texttemplate.Template); ok {
 		return text.Tree
 	}
@@ -146,7 +128,7 @@ func init() {
 
 // wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
 // predefined partial return wrapper to insert those of a user-defined partial.
-func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
+func (c *templateTransformContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
 	wrapper := partialReturnWrapper.CopyList()
 	rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
 	retn := rangeNode.List.Nodes[0]
@@ -163,7 +145,7 @@ func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.L
 // applyTransformations do 2 things:
 // 1) Parses partial return statement.
 // 2) Tracks template (partial) dependencies and some other info.
-func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
+func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, error) {
 	switch x := n.(type) {
 	case *parse.ListNode:
 		if x != nil {
@@ -208,7 +190,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
 	return true, c.err
 }
 
-func (c *templateContext) handleDefer(withNode *parse.WithNode) {
+func (c *templateTransformContext) handleDefer(withNode *parse.WithNode) {
 	if len(withNode.Pipe.Cmds) != 1 {
 		return
 	}
@@ -265,13 +247,13 @@ func (c *templateContext) handleDefer(withNode *parse.WithNode) {
 	n.Pipe.Cmds[0].Args[2] = deferArg
 }
 
-func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
+func (c *templateTransformContext) applyTransformationsToNodes(nodes ...parse.Node) {
 	for _, node := range nodes {
 		c.applyTransformations(node)
 	}
 }
 
-func (c *templateContext) hasIdent(idents []string, ident string) bool {
+func (c *templateTransformContext) hasIdent(idents []string, ident string) bool {
 	return slices.Contains(idents, ident)
 }
 
@@ -280,8 +262,8 @@ func (c *templateContext) hasIdent(idents []string, ident string) bool {
 // on the form:
 //
 //	{{ $_hugo_config:= `{ "version": 1 }` }}
-func (c *templateContext) collectConfig(n *parse.PipeNode) {
-	if c.t.typ != templateShortcode {
+func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
+	if c.t.Category != CategoryShortcode {
 		return
 	}
 	if c.configChecked {
@@ -313,7 +295,7 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
 			c.err = fmt.Errorf(errMsg, err)
 			return
 		}
-		if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil {
+		if err := mapstructure.WeakDecode(m, &c.t.ParseInfo.Config); err != nil {
 			c.err = fmt.Errorf(errMsg, err)
 		}
 	}
@@ -321,11 +303,11 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
 
 // collectInner determines if the given CommandNode represents a
 // shortcode call to its .Inner.
-func (c *templateContext) collectInner(n *parse.CommandNode) {
-	if c.t.typ != templateShortcode {
+func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
+	if c.t.Category != CategoryShortcode {
 		return
 	}
-	if c.t.parseInfo.IsInner || len(n.Args) == 0 {
+	if c.t.ParseInfo.IsInner || len(n.Args) == 0 {
 		return
 	}
 
@@ -339,14 +321,14 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
 		}
 
 		if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
-			c.t.parseInfo.IsInner = true
+			c.t.ParseInfo.IsInner = true
 			break
 		}
 	}
 }
 
-func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
-	if c.t.typ != templatePartial || c.returnNode != nil {
+func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
+	if c.t.Category != CategoryPartial || c.returnNode != nil {
 		return true
 	}
 
@@ -365,17 +347,3 @@ func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
 
 	return false
 }
-
-func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) {
-	in = unwrap(in)
-	if text, ok := in.(*texttemplate.Template); ok {
-		if templ := text.Lookup(name); templ != nil {
-			return templ, true
-		}
-		return nil, false
-	}
-	if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil {
-		return templ, true
-	}
-	return nil, false
-}
diff --git a/tpl/tplimplinit/tplimplinit.go b/tpl/tplimplinit/tplimplinit.go
new file mode 100644
index 00000000000..6316e8897c9
--- /dev/null
+++ b/tpl/tplimplinit/tplimplinit.go
@@ -0,0 +1,96 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// 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 tplimplinit
+
+import (
+	// Init the template funcs namespaces
+	"context"
+	"html/template"
+
+	"github.com/gohugoio/hugo/deps"
+	_ "github.com/gohugoio/hugo/tpl/cast"
+	_ "github.com/gohugoio/hugo/tpl/collections"
+	_ "github.com/gohugoio/hugo/tpl/compare"
+	_ "github.com/gohugoio/hugo/tpl/crypto"
+	_ "github.com/gohugoio/hugo/tpl/css"
+	_ "github.com/gohugoio/hugo/tpl/data"
+	_ "github.com/gohugoio/hugo/tpl/debug"
+	_ "github.com/gohugoio/hugo/tpl/diagrams"
+	_ "github.com/gohugoio/hugo/tpl/encoding"
+	_ "github.com/gohugoio/hugo/tpl/fmt"
+	_ "github.com/gohugoio/hugo/tpl/hash"
+	_ "github.com/gohugoio/hugo/tpl/hugo"
+	_ "github.com/gohugoio/hugo/tpl/images"
+	_ "github.com/gohugoio/hugo/tpl/inflect"
+	"github.com/gohugoio/hugo/tpl/internal"
+	_ "github.com/gohugoio/hugo/tpl/js"
+	_ "github.com/gohugoio/hugo/tpl/lang"
+	_ "github.com/gohugoio/hugo/tpl/math"
+	_ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
+	_ "github.com/gohugoio/hugo/tpl/os"
+	_ "github.com/gohugoio/hugo/tpl/page"
+	_ "github.com/gohugoio/hugo/tpl/partials"
+	_ "github.com/gohugoio/hugo/tpl/path"
+	_ "github.com/gohugoio/hugo/tpl/reflect"
+	_ "github.com/gohugoio/hugo/tpl/resources"
+	_ "github.com/gohugoio/hugo/tpl/safe"
+	_ "github.com/gohugoio/hugo/tpl/site"
+	_ "github.com/gohugoio/hugo/tpl/strings"
+	_ "github.com/gohugoio/hugo/tpl/templates"
+	_ "github.com/gohugoio/hugo/tpl/time"
+	_ "github.com/gohugoio/hugo/tpl/transform"
+	_ "github.com/gohugoio/hugo/tpl/urls"
+)
+
+// CreateFuncMap creates a template.FuncMap with all of Hugo's template funcs,
+// excluding the Go built-ins.
+func CreateFuncMap(d *deps.Deps) map[string]any {
+	funcMap := template.FuncMap{}
+	nsMap := make(map[string]any)
+	var onCreated []func(namespaces map[string]any)
+
+	// Merge the namespace funcs
+	for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+		ns := nsf(d)
+		if _, exists := funcMap[ns.Name]; exists {
+			panic(ns.Name + " is a duplicate template func")
+		}
+		funcMap[ns.Name] = ns.Context
+		contextV, err := ns.Context(context.Background())
+		if err != nil {
+			panic(err)
+		}
+		nsMap[ns.Name] = contextV
+		for _, mm := range ns.MethodMappings {
+			for _, alias := range mm.Aliases {
+				if _, exists := funcMap[alias]; exists {
+					panic(alias + " is a duplicate template func")
+				}
+				funcMap[alias] = mm.Method
+			}
+		}
+
+		if ns.OnCreated != nil {
+			onCreated = append(onCreated, ns.OnCreated)
+		}
+	}
+
+	for _, f := range onCreated {
+		f(nsMap)
+	}
+
+	return funcMap
+}


Test patch:
diff --git a/common/paths/pathparser_test.go b/common/paths/pathparser_test.go
index 584b1a78aac..fd1590c73e0 100644
--- a/common/paths/pathparser_test.go
+++ b/common/paths/pathparser_test.go
@@ -18,6 +18,7 @@ import (
 	"testing"
 
 	"github.com/gohugoio/hugo/hugofs/files"
+	"github.com/gohugoio/hugo/resources/kinds"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -26,10 +27,18 @@ var testParser = &PathParser{
 	LanguageIndex: map[string]int{
 		"no": 0,
 		"en": 1,
+		"fr": 2,
 	},
 	IsContentExt: func(ext string) bool {
 		return ext == "md"
 	},
+	IsOutputFormat: func(name, ext string) bool {
+		switch name {
+		case "html", "amp", "csv", "rss":
+			return true
+		}
+		return false
+	},
 }
 
 func TestParse(t *testing.T) {
@@ -105,17 +114,19 @@ func TestParse(t *testing.T) {
 			"Basic Markdown file",
 			"/a/b/c.md",
 			func(c *qt.C, p *Path) {
+				c.Assert(p.Ext(), qt.Equals, "md")
+				c.Assert(p.Type(), qt.Equals, TypeContentSingle)
 				c.Assert(p.IsContent(), qt.IsTrue)
 				c.Assert(p.IsLeafBundle(), qt.IsFalse)
 				c.Assert(p.Name(), qt.Equals, "c.md")
 				c.Assert(p.Base(), qt.Equals, "/a/b/c")
+				c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
 				c.Assert(p.Section(), qt.Equals, "a")
 				c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
 				c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
 				c.Assert(p.Dir(), qt.Equals, "/a/b")
 				c.Assert(p.Container(), qt.Equals, "b")
 				c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
-				c.Assert(p.Ext(), qt.Equals, "md")
 			},
 		},
 		{
@@ -130,7 +141,7 @@ func TestParse(t *testing.T) {
 
 				// Reclassify it as a content resource.
 				ModifyPathBundleTypeResource(p)
-				c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
+				c.Assert(p.Type(), qt.Equals, TypeContentResource)
 				c.Assert(p.IsContent(), qt.IsTrue)
 				c.Assert(p.Name(), qt.Equals, "b.md")
 				c.Assert(p.Base(), qt.Equals, "/a/b.md")
@@ -160,15 +171,16 @@ func TestParse(t *testing.T) {
 			"/a/b.a.b.no.txt",
 			func(c *qt.C, p *Path) {
 				c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
-				c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
+				c.Assert(p.NameNoIdentifier(), qt.Equals, "b")
 				c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
-				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
-				c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
-				c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "b", "a", "b"})
+				c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
+				c.Assert(p.Base(), qt.Equals, "/a/b.txt")
+				c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.txt")
 				c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
-				c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
+				c.Assert(p.PathNoLang(), qt.Equals, "/a/b.txt")
 				c.Assert(p.Ext(), qt.Equals, "txt")
-				c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
+				c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b")
 			},
 		},
 		{
@@ -176,6 +188,7 @@ func TestParse(t *testing.T) {
 			"/_index.md",
 			func(c *qt.C, p *Path) {
 				c.Assert(p.Base(), qt.Equals, "/")
+				c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
 				c.Assert(p.Path(), qt.Equals, "/_index.md")
 				c.Assert(p.Container(), qt.Equals, "")
 				c.Assert(p.ContainerDir(), qt.Equals, "/")
@@ -186,13 +199,14 @@ func TestParse(t *testing.T) {
 			"/a/index.md",
 			func(c *qt.C, p *Path) {
 				c.Assert(p.Base(), qt.Equals, "/a")
+				c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
 				c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
 				c.Assert(p.Container(), qt.Equals, "a")
 				c.Assert(p.Container(), qt.Equals, "a")
 				c.Assert(p.ContainerDir(), qt.Equals, "")
 				c.Assert(p.Dir(), qt.Equals, "/a")
 				c.Assert(p.Ext(), qt.Equals, "md")
-				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "index"})
 				c.Assert(p.IsBranchBundle(), qt.IsFalse)
 				c.Assert(p.IsBundle(), qt.IsTrue)
 				c.Assert(p.IsLeafBundle(), qt.IsTrue)
@@ -209,11 +223,12 @@ func TestParse(t *testing.T) {
 			func(c *qt.C, p *Path) {
 				c.Assert(p.Base(), qt.Equals, "/a/b")
 				c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
+				c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
 				c.Assert(p.Container(), qt.Equals, "b")
 				c.Assert(p.ContainerDir(), qt.Equals, "/a")
 				c.Assert(p.Dir(), qt.Equals, "/a/b")
 				c.Assert(p.Ext(), qt.Equals, "md")
-				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "index"})
 				c.Assert(p.IsBranchBundle(), qt.IsFalse)
 				c.Assert(p.IsBundle(), qt.IsTrue)
 				c.Assert(p.IsLeafBundle(), qt.IsTrue)
@@ -235,7 +250,7 @@ func TestParse(t *testing.T) {
 				c.Assert(p.Container(), qt.Equals, "b")
 				c.Assert(p.ContainerDir(), qt.Equals, "/a")
 				c.Assert(p.Ext(), qt.Equals, "md")
-				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no", "_index"})
 				c.Assert(p.IsBranchBundle(), qt.IsTrue)
 				c.Assert(p.IsBundle(), qt.IsTrue)
 				c.Assert(p.IsLeafBundle(), qt.IsFalse)
@@ -274,7 +289,7 @@ func TestParse(t *testing.T) {
 			func(c *qt.C, p *Path) {
 				c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
 				c.Assert(p.Ext(), qt.Equals, "txt")
-				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no", "index"})
 				c.Assert(p.IsLeafBundle(), qt.IsFalse)
 				c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
 			},
@@ -357,11 +372,140 @@ func TestParse(t *testing.T) {
 	}
 	for _, test := range tests {
 		c.Run(test.name, func(c *qt.C) {
+			if test.name != "Basic Markdown file" {
+				// return
+			}
 			test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
 		})
 	}
 }
 
+func TestParseLayouts(t *testing.T) {
+	c := qt.New(t)
+
+	tests := []struct {
+		name   string
+		path   string
+		assert func(c *qt.C, p *Path)
+	}{
+		{
+			"Basic",
+			"/list.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Base(), qt.Equals, "/list.html")
+				c.Assert(p.OutputFormat(), qt.Equals, "html")
+			},
+		},
+		{
+			"Lang",
+			"/list.no.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
+				c.Assert(p.Base(), qt.Equals, "/list.html")
+				c.Assert(p.Lang(), qt.Equals, "no")
+			},
+		},
+		{
+			"Lang and output format",
+			"/list.no.amp.not.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
+				c.Assert(p.OutputFormat(), qt.Equals, "amp")
+				c.Assert(p.Ext(), qt.Equals, "html")
+				c.Assert(p.Lang(), qt.Equals, "no")
+				c.Assert(p.Base(), qt.Equals, "/list.html")
+			},
+		},
+		{
+			"Term",
+			"/term.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Base(), qt.Equals, "/term.html")
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
+				c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
+				c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
+				c.Assert(p.Lang(), qt.Equals, "")
+				c.Assert(p.Kind(), qt.Equals, "term")
+				c.Assert(p.OutputFormat(), qt.Equals, "html")
+			},
+		},
+		{
+			"Sub dir",
+			"/pages/home.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
+				c.Assert(p.Lang(), qt.Equals, "")
+				c.Assert(p.Kind(), qt.Equals, "home")
+				c.Assert(p.OutputFormat(), qt.Equals, "html")
+				c.Assert(p.Dir(), qt.Equals, "/pages")
+			},
+		},
+		{
+			"Baseof",
+			"/pages/baseof.list.section.fr.amp.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
+				c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
+				c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
+				c.Assert(p.Lang(), qt.Equals, "fr")
+				c.Assert(p.OutputFormat(), qt.Equals, "amp")
+				c.Assert(p.Dir(), qt.Equals, "/pages")
+				c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
+				c.Assert(p.Type(), qt.Equals, TypeBaseof)
+				c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
+			},
+		},
+		{
+			"Markup",
+			"/_markup/render-link.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Type(), qt.Equals, TypeMarkup)
+			},
+		},
+		{
+			"Markup nested",
+			"/foo/_markup/render-link.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Type(), qt.Equals, TypeMarkup)
+			},
+		},
+		{
+			"Shortcode",
+			"/_shortcodes/myshortcode.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Type(), qt.Equals, TypeShortcode)
+			},
+		},
+		{
+			"Shortcode nested",
+			"/foo/_shortcodes/myshortcode.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Type(), qt.Equals, TypeShortcode)
+			},
+		},
+		{
+			"Shortcode nested sub",
+			"/foo/_shortcodes/foo/myshortcode.html",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Type(), qt.Equals, TypeShortcode)
+			},
+		},
+		{
+			"Partials",
+			"/_partials/foo.bar",
+			func(c *qt.C, p *Path) {
+				c.Assert(p.Type(), qt.Equals, TypePartial)
+			},
+		},
+	}
+
+	for _, test := range tests {
+		c.Run(test.name, func(c *qt.C) {
+			test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
+		})
+	}
+}
+
 func TestHasExt(t *testing.T) {
 	c := qt.New(t)
 
diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go
index c017050c6ac..1ba7bb16c7a 100644
--- a/hugolib/alias_test.go
+++ b/hugolib/alias_test.go
@@ -107,13 +107,26 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
 func TestAliasTemplate(t *testing.T) {
 	t.Parallel()
 
-	b := newTestSitesBuilder(t)
-	b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias).WithTemplatesAdded("alias.html", aliasTemplate)
+	files := `
+-- hugo.toml --
+baseURL = "http://example.com"
+-- layouts/single.html --
+Single.
+-- layouts/home.html --
+Home.
+-- layouts/alias.html --
+ALIASTEMPLATE
+-- content/page.md --
+---
+title: "Page"
+aliases: ["/foo/bar/"]
+---
+`
 
-	b.CreateSites().Build(BuildCfg{})
+	b := Test(t, files)
 
 	// the real page
-	b.AssertFileContent("public/page/index.html", "For some moments the old man")
+	b.AssertFileContent("public/page/index.html", "Single.")
 	// the alias redirector
 	b.AssertFileContent("public/foo/bar/index.html", "ALIASTEMPLATE")
 }
diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go
index 7202840f349..aed2a7f1398 100644
--- a/hugolib/content_map_test.go
+++ b/hugolib/content_map_test.go
@@ -242,8 +242,13 @@ Data en
 }
 
 func TestBundleMultipleContentPageWithSamePath(t *testing.T) {
+	t.Parallel()
+
 	files := `
 -- hugo.toml --
+printPathWarnings = true
+-- layouts/all.html --
+All.
 -- content/bundle/index.md --
 ---
 title: "Bundle md"
@@ -273,14 +278,18 @@ Bundle: {{ $bundle.Title }}|{{ $bundle.Params.foo }}|{{ $bundle.File.Filename }}
 P1: {{ $p1.Title }}|{{ $p1.Params.foo }}|{{ $p1.File.Filename }}|
 `
 
-	b := Test(t, files)
+	for range 3 {
+		b := Test(t, files, TestOptWarn())
 
-	// There's multiple content files sharing the same logical path and language.
-	// This is a little arbitrary, but we have to pick one and prefer the Markdown version.
-	b.AssertFileContent("public/index.html",
-		filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
-		filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
-	)
+		b.AssertLogContains("WARN  Duplicate content path: \"/p1\"")
+
+		// There's multiple content files sharing the same logical path and language.
+		// This is a little arbitrary, but we have to pick one and prefer the Markdown version.
+		b.AssertFileContent("public/index.html",
+			filepath.FromSlash("Bundle: Bundle md|md|/content/bundle/index.md|"),
+			filepath.FromSlash("P1: P1 md|md|/content/p1.md|"),
+		)
+	}
 }
 
 // Issue #11944
diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go
index 9df8d2e2e05..56ae0a052ab 100644
--- a/hugolib/content_render_hooks_test.go
+++ b/hugolib/content_render_hooks_test.go
@@ -17,6 +17,8 @@ import (
 	"fmt"
 	"strings"
 	"testing"
+
+	qt "github.com/frankban/quicktest"
 )
 
 func TestRenderHooksRSS(t *testing.T) {
@@ -129,6 +131,7 @@ P1: <p>P1. <a href="https://www.gohugo.io">I&rsquo;m an inline-style link</a></p
 <h1 id="heading-in-p1">Heading in p1</h1>
 <h1 id="heading-in-p2">Heading in p2</h1>
 `)
+
 	b.AssertFileContent("public/index.xml", `
 P2: <p>P2. xml-link: https://www.bep.is|</p>
 P3: <p>P3. xml-link: https://www.example.org|</p>
@@ -378,3 +381,93 @@ Content: {{ .Content}}|
 		"|Text: First line.\nSecond line.||\n",
 	)
 }
+
+func TestContentOutputReuseRenderHooksAndShortcodesHTMLOnly(t *testing.T) {
+	files := `
+-- hugo.toml --
+-- layouts/index.html --
+HTML: {{ .Title }}|{{ .Content }}|
+-- layouts/index.xml --
+XML: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-heading.html --
+Render heading.
+-- layouts/shortcodes/myshortcode.html --
+My shortcode.
+-- content/_index.md --
+---
+title: "Home"
+---
+## Heading
+
+{{< myshortcode >}}
+`
+	b := Test(t, files)
+
+	s := b.H.Sites[0]
+	b.Assert(s.home.pageOutputTemplateVariationsState.Load(), qt.Equals, uint32(1))
+	b.AssertFileContent("public/index.html", "HTML: Home|Render heading.\nMy shortcode.\n|")
+	b.AssertFileContent("public/index.xml", "XML: Home|Render heading.\nMy shortcode.\n|")
+}
+
+func TestContentOutputNoReuseRenderHooksInBothHTMLAnXML(t *testing.T) {
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/index.html --
+HTML: {{ .Title }}|{{ .Content }}|
+-- layouts/index.xml --
+XML: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-heading.html --
+Render heading.
+-- layouts/_markup/render-heading.xml --
+Render heading XML.
+-- content/_index.md --
+---
+title: "Home"
+---
+## Heading
+
+
+`
+	b := Test(t, files)
+
+	s := b.H.Sites[0]
+	b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
+	b.AssertFileContentExact("public/index.xml", "XML: Home|Render heading XML.|")
+	b.AssertFileContentExact("public/index.html", "HTML: Home|Render heading.|")
+}
+
+func TestContentOutputNoReuseShortcodesInBothHTMLAnXML(t *testing.T) {
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/index.html --
+HTML: {{ .Title }}|{{ .Content }}|
+-- layouts/index.xml --
+XML: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-heading.html --
+Render heading.
+
+-- layouts/shortcodes/myshortcode.html --
+My shortcode HTML.
+-- layouts/shortcodes/myshortcode.xml --
+My shortcode XML.
+-- content/_index.md --
+---
+title: "Home"
+---
+## Heading
+
+{{< myshortcode >}}
+
+
+`
+	b := Test(t, files)
+
+	// b.DebugPrint("", tplimpl.CategoryShortcode)
+
+	b.AssertFileContentExact("public/index.xml", "My shortcode XML.")
+	b.AssertFileContentExact("public/index.html", "My shortcode HTML.")
+	s := b.H.Sites[0]
+	b.Assert(s.home.pageOutputTemplateVariationsState.Load() > 1, qt.IsTrue)
+}
diff --git a/hugolib/hugo_smoke_test.go b/hugolib/hugo_smoke_test.go
index c43ba72937f..09d57bbffb5 100644
--- a/hugolib/hugo_smoke_test.go
+++ b/hugolib/hugo_smoke_test.go
@@ -76,12 +76,13 @@ Single: {{ .Title }}|{{ .RelPermalink}}|{{ range .OutputFormats }}{{ .Name }}: {
 
 `
 
-	b := Test(t, files)
-
-	b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
-	b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
-	b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
-	b.AssertFileExists("public/p1/index.xml", false)
+	for i := 0; i < 2; i++ {
+		b := Test(t, files)
+		b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`)
+		b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`)
+		b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`)
+		b.AssertFileExists("public/p1/index.xml", false)
+	}
 }
 
 func TestSmoke(t *testing.T) {
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index ff45ec2757f..4ea6f420de9 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -219,19 +219,31 @@ type IntegrationTestBuilder struct {
 
 type lockingBuffer struct {
 	sync.Mutex
-	bytes.Buffer
+	buf bytes.Buffer
+}
+
+func (b *lockingBuffer) String() string {
+	b.Lock()
+	defer b.Unlock()
+	return b.buf.String()
+}
+
+func (b *lockingBuffer) Reset() {
+	b.Lock()
+	defer b.Unlock()
+	b.buf.Reset()
 }
 
 func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
 	b.Lock()
-	n, err = b.Buffer.ReadFrom(r)
+	n, err = b.buf.ReadFrom(r)
 	b.Unlock()
 	return
 }
 
 func (b *lockingBuffer) Write(p []byte) (n int, err error) {
 	b.Lock()
-	n, err = b.Buffer.Write(p)
+	n, err = b.buf.Write(p)
 	b.Unlock()
 	return
 }
diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
index c00a95bc1fd..a31b9399926 100644
--- a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
+++ b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
@@ -98,7 +98,8 @@ ADD_MORE_PLACEHOLDER
 
 func TestPagesFromGoTmplMisc(t *testing.T) {
 	t.Parallel()
-	b := hugolib.Test(t, filesPagesFromDataTempleBasic)
+	b := hugolib.Test(t, filesPagesFromDataTempleBasic, hugolib.TestOptWarn())
+	b.AssertLogContains("! WARN")
 	b.AssertPublishDir(`
 docs/p1/mytext.txt
 docs/p1/sub/mytex2.tx
diff --git a/hugolib/paginator_test.go b/hugolib/paginator_test.go
index 2470a904651..2fb87956f28 100644
--- a/hugolib/paginator_test.go
+++ b/hugolib/paginator_test.go
@@ -15,7 +15,6 @@ package hugolib
 
 import (
 	"fmt"
-	"path/filepath"
 	"testing"
 
 	qt "github.com/frankban/quicktest"
@@ -102,10 +101,18 @@ URL: {{ $pag.URL }}
 
 // Issue 6023
 func TestPaginateWithSort(t *testing.T) {
-	b := newTestSitesBuilder(t).WithSimpleConfigFile()
-	b.WithTemplatesAdded("index.html", `{{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .File.Filename }}{{ end }}`)
-	b.Build(BuildCfg{}).AssertFileContent("public/index.html",
-		filepath.FromSlash("|content/sect/doc1.nn.md|content/sect/doc1.nb.md|content/sect/doc1.fr.md|content/sect/doc1.en.md"))
+	files := `
+-- hugo.toml --
+-- content/a/a.md --
+-- content/z/b.md --
+-- content/x/b.md --
+-- content/x/a.md --
+-- layouts/home.html --
+Paginate: {{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .Path }}{{ end }}
+`
+	b := Test(t, files)
+
+	b.AssertFileContent("public/index.html", "Paginate: |/z/b|/x/b|/x/a|/a/a")
 }
 
 // https://github.com/gohugoio/hugo/issues/6797
@@ -176,12 +183,12 @@ Paginator: {{ .Paginator }}
 
 func TestNilPointerErrorMessage(t *testing.T) {
 	files := `
--- hugo.toml --
+-- hugo.toml --	
 -- content/p1.md --
 -- layouts/_default/single.html --
 Home Filename: {{ site.Home.File.Filename }}
 `
 	b, err := TestE(t, files)
 	b.Assert(err, qt.IsNotNil)
-	b.Assert(err.Error(), qt.Contains, `_default/single.html:1:22: executing "_default/single.html" – File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
+	b.Assert(err.Error(), qt.Contains, `single.html:1:22: executing "single.html" – File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`)
 }
diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go
index fab47679f33..9d95a4b7554 100644
--- a/hugolib/rebuild_test.go
+++ b/hugolib/rebuild_test.go
@@ -51,6 +51,7 @@ My Section Bundle Content Content.
 title: "My Section"
 ---
 -- content/mysection/mysectiontext.txt --
+Content.
 -- content/_index.md --
 ---
 title: "Home"
@@ -99,15 +100,17 @@ My Other Text: {{ $r.Content }}|{{ $r.Permalink }}|
 `
 
 func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) {
-	b := TestRunning(t, rebuildFilesSimple)
-	b.AssertFileContent("public/mysection/mysectionbundle/index.html",
-		"My Section Bundle Content Content.")
-
-	b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
-	b.AssertFileContent("public/mysection/mysectionbundle/index.html",
-		"My Section Bundle Content Edited.")
-	b.AssertRenderCountPage(2) // home (rss) + bundle.
-	b.AssertRenderCountContent(1)
+	t.Parallel()
+	for i := 0; i < 3; i++ {
+		b := TestRunning(t, rebuildFilesSimple)
+		b.AssertFileContent("public/mysection/mysectionbundle/index.html",
+			"My Section Bundle Content Content.")
+		b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build()
+		b.AssertFileContent("public/mysection/mysectionbundle/index.html",
+			"My Section Bundle Content Edited.")
+		b.AssertRenderCountPage(2) // home (rss) + bundle.
+		b.AssertRenderCountContent(1)
+	}
 }
 
 func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
@@ -119,7 +122,7 @@ func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
 	b.AssertFileContent("public/mysection/mysectionbundle/index.html",
 		"Text 2 Content Edited")
 	b.AssertRenderCountPage(1)
-	b.AssertRenderCountContent(1)
+	b.AssertRenderCountContent(0)
 }
 
 func TestRebuildEditTextFileInShortcode(t *testing.T) {
@@ -180,17 +183,17 @@ func TestRebuildEditTextFileInHomeBundle(t *testing.T) {
 	b.AssertFileContent("public/index.html", "Home Content.")
 	b.AssertFileContent("public/index.html", "Home Text Content Edited.")
 	b.AssertRenderCountPage(1)
-	b.AssertRenderCountContent(1)
+	b.AssertRenderCountContent(0)
 }
 
 func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
 	b := TestRunning(t, rebuildFilesSimple)
-	b.AssertFileContent("public/mysection/index.html", "My Section")
+	b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content.|")
 
 	b.EditFileReplaceAll("content/mysection/mysectiontext.txt", "Content.", "Content Edited.").Build()
-	b.AssertFileContent("public/mysection/index.html", "My Section")
+	b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content Edited.|")
 	b.AssertRenderCountPage(1)
-	b.AssertRenderCountContent(1)
+	b.AssertRenderCountContent(0)
 }
 
 func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
@@ -484,7 +487,43 @@ Home: {{ .Title }}|{{ .Content }}|
 	})
 }
 
-func TestRebuildSingleWithBaseof(t *testing.T) {
+func TestRebuildSingle(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+-- layouts/index.html --
+Home.
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer.
+{{ end }}
+`
+	b := Test(t, files, TestOptRunning())
+	b.AssertFileContent("public/p1/index.html", "Single: P1|", "Defer.")
+	b.AssertRenderCountPage(3)
+	b.AssertRenderCountContent(1)
+	b.EditFileReplaceFunc("layouts/single.html", func(s string) string {
+		s = strings.Replace(s, "Single", "Single Edited", 1)
+		s = strings.Replace(s, "Defer.", "Defer Edited.", 1)
+		return s
+	}).Build()
+	b.AssertFileContent("public/p1/index.html", "Single Edited: P1|", "Defer Edited.")
+	b.AssertRenderCountPage(1)
+	b.AssertRenderCountContent(0)
+}
+
+func TestRebuildSingleWithBaseofEditSingle(t *testing.T) {
 	t.Parallel()
 
 	files := `
@@ -498,9 +537,13 @@ disableLiveReload = true
 title: "P1"
 ---
 P1 Content.
+[foo](/foo)
 -- layouts/_default/baseof.html --
 Baseof: {{ .Title }}|
 {{ block "main" . }}default{{ end }}
+{{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
 -- layouts/index.html --
 Home.
 -- layouts/_default/single.html --
@@ -509,11 +552,81 @@ Single: {{ .Title }}|{{ .Content }}|
 {{ end }}
 `
 	b := Test(t, files, TestOptRunning())
-	b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle: P1|<p>P1 Content.</p>\n|")
+	b.AssertFileContent("public/p1/index.html", "Single: P1|")
 	b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string {
 		return strings.Replace(s, "Single", "Single Edited", 1)
 	}).Build()
-	b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle Edited: P1|<p>P1 Content.</p>\n|")
+	b.AssertFileContent("public/p1/index.html", "Single Edited")
+}
+
+func TestRebuildSingleWithBaseofEditBaseof(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+[foo](/foo)
+-- layouts/_default/baseof.html --
+Baseof: {{ .Title }}|
+{{ block "main" . }}default{{ end }}
+{{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
+-- layouts/index.html --
+Home.
+-- layouts/_default/single.html --
+{{ define "main" }}
+Single: {{ .Title }}|{{ .Content }}|
+{{ end }}
+`
+	b := Test(t, files, TestOptRunning())
+	b.AssertFileContent("public/p1/index.html", "Single: P1|")
+	fmt.Println("===============")
+	b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof Edited").Build()
+	b.AssertFileContent("public/p1/index.html", "Baseof Edited")
+}
+
+func TestRebuildWithDeferEditRenderHook(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+title = "Hugo Site"
+baseURL = "https://example.com"
+disableKinds = ["term", "taxonomy"]
+disableLiveReload = true
+-- content/p1.md --
+---
+title: "P1"
+---
+P1 Content.
+[foo](/foo)
+-- layouts/_default/baseof.html --
+Baseof: {{ .Title }}|
+{{ block "main" . }}default{{ end }}
+ {{ with (templates.Defer (dict "foo" "bar")) }}
+Defer.
+{{ end }}
+-- layouts/single.html --
+{{ define "main" }}
+Single: {{ .Title }}|{{ .Content }}|
+{{ end }}
+-- layouts/_default/_markup/render-link.html --
+Render Link.
+`
+	b := Test(t, files, TestOptRunning())
+	// Edit render hook.
+	b.EditFileReplaceAll("layouts/_default/_markup/render-link.html", "Render Link", "Render Link Edited").Build()
+
+	b.AssertFileContent("public/p1/index.html", "Render Link Edited")
 }
 
 func TestRebuildFromString(t *testing.T) {
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
index 93edd934546..f1d90e22ed6 100644
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -33,14 +33,14 @@ func TestExtractShortcodes(t *testing.T) {
 	b := newTestSitesBuilder(t).WithSimpleConfigFile()
 
 	b.WithTemplates(
-		"default/single.html", `EMPTY`,
-		"_internal/shortcodes/tag.html", `tag`,
-		"_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
-		"_internal/shortcodes/sc1.html", `sc1`,
-		"_internal/shortcodes/sc2.html", `sc2`,
-		"_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
-		"_internal/shortcodes/inner2.html", `{{.Inner}}`,
-		"_internal/shortcodes/inner3.html", `{{.Inner}}`,
+		"pages/single.html", `EMPTY`,
+		"shortcodes/tag.html", `tag`,
+		"shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
+		"shortcodes/sc1.html", `sc1`,
+		"shortcodes/sc2.html", `sc2`,
+		"shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
+		"shortcodes/inner2.html", `{{.Inner}}`,
+		"shortcodes/inner3.html", `{{.Inner}}`,
 	).WithContent("page.md", `---
 title: "Shortcodes Galore!"
 ---
@@ -57,10 +57,9 @@ title: "Shortcodes Galore!"
 		if s == nil {
 			return "<nil>"
 		}
-
 		var version int
-		if s.info != nil {
-			version = s.info.ParseInfo().Config.Version
+		if s.templ != nil {
+			version = s.templ.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, version, s.pos))
@@ -69,7 +68,7 @@ title: "Shortcodes Galore!"
 	regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
 		return func(c *qt.C, shortcode *shortcode, err error) {
 			c.Assert(err, qt.IsNil)
-			c.Assert(str(shortcode), qt.Matches, ".*"+re+".*")
+			c.Assert(str(shortcode), qt.Matches, ".*"+re+".*", qt.Commentf("%s", shortcode.name))
 		}
 	}
 
@@ -888,6 +887,7 @@ outputs: ["html", "css", "csv", "json"]
 			"_default/single.json", "{{ .Content }}",
 			"shortcodes/myshort.html", `Short-HTML`,
 			"shortcodes/myshort.csv", `Short-CSV`,
+			"shortcodes/myshort.txt", `Short-TXT`,
 		)
 
 		b.Build(BuildCfg{})
@@ -897,12 +897,12 @@ outputs: ["html", "css", "csv", "json"]
 		for i := range numPages {
 			b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML")
 			b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV")
-			b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML")
+			b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-CSV")
 
 		}
 
 		for i := range numPages {
-			b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML")
+			b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-CSV")
 		}
 
 	}
diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go
index 37ebce73066..caec4c7007f 100644
--- a/hugolib/site_output_test.go
+++ b/hugolib/site_output_test.go
@@ -387,7 +387,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
 		c.Assert(outputs[kinds.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
 		c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
 		c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat})
-		c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
+		c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTTPStatus404HTMLFormat})
 	})
 
 	// Issue #4528
@@ -481,6 +481,7 @@ permalinkable = true
 [outputFormats.nobase]
 mediaType = "application/json"
 permalinkable = true
+isPlainText = true
 
 `
 
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
index e611897fe2f..a9fe977cf5e 100644
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -978,8 +978,7 @@ func TestRefLinking(t *testing.T) {
 		{".", "", true, "/level2/level3/"},
 		{"./", "", true, "/level2/level3/"},
 
-		// try to confuse parsing
-		{"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"},
+		{"embedded.dot.md", "", true, "/level2/level3/embedded/"},
 
 		// test empty link, as well as fragment only link
 		{"", "", true, ""},
diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go
index 6577a22c1e4..7aeaa780c6f 100644
--- a/hugolib/taxonomy_test.go
+++ b/hugolib/taxonomy_test.go
@@ -76,6 +76,8 @@ func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) {
 }
 
 func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) {
+	t.Helper()
+
 	siteConfig := `
 baseURL = "http://example.com/blog"
 titleCaseStyle = "firstupper"
diff --git a/hugolib/template_test.go b/hugolib/template_test.go
index 01dfc7eba1a..a08f83cb859 100644
--- a/hugolib/template_test.go
+++ b/hugolib/template_test.go
@@ -26,6 +26,8 @@ import (
 	"github.com/gohugoio/hugo/hugofs"
 )
 
+// TODO(bep) keep this until we release v0.146.0 as a security against breaking changes, but it's rather messy and mostly duplicate of
+// tests in the tplimpl package, so eventually just remove it.
 func TestTemplateLookupOrder(t *testing.T) {
 	var (
 		fs      *hugofs.Fs
@@ -185,6 +187,9 @@ func TestTemplateLookupOrder(t *testing.T) {
 	} {
 
 		this := this
+		if this.name != "Variant 1" {
+			continue
+		}
 		t.Run(this.name, func(t *testing.T) {
 			// TODO(bep) there are some function vars need to pull down here to enable => t.Parallel()
 			cfg, fs = newTestCfg()
@@ -200,7 +205,7 @@ Some content
 			}
 
 			buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{})
-			// helpers.PrintFs(s.BaseFs.Layouts.Fs, "", os.Stdout)
+			// s.TemplateStore.PrintDebug("", 0, os.Stdout)
 			this.assert(t)
 		})
 
@@ -270,11 +275,11 @@ func TestTemplateNoBasePlease(t *testing.T) {
 	b := newTestSitesBuilder(t).WithSimpleConfigFile()
 
 	b.WithTemplates("_default/list.html", `
-	{{ define "main" }}
-	  Bonjour
-	{{ end }}
+{{ define "main" }}
+  Bonjour
+{{ end }}
 
-	{{ printf "list" }}
+{{ printf "list" }}
 
 
 	`)
@@ -344,33 +349,36 @@ title: %s
 		b.AssertFileContent("public/p1/index.html", `Single: P1`)
 	})
 
-	t.Run("baseof", func(t *testing.T) {
-		t.Parallel()
-		b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+	{
+	}
+}
 
-		b.WithTemplatesAdded(
-			"index.html", `{{ define "main" }}Main Home En{{ end }}`,
-			"index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
-			"baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
-			"baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
-			"mysection/baseof.html", `Baseof mysection: {{ block "main" .  }}mysection block{{ end }}`,
-			"_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
-			"_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
-		)
+func TestTemplateLookupSitBaseOf(t *testing.T) {
+	t.Parallel()
+	b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+
+	b.WithTemplatesAdded(
+		"index.html", `{{ define "main" }}Main Home En{{ end }}`,
+		"index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
+		"baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
+		"baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
+		"mysection/baseof.html", `Baseof mysection: {{ block "main" .  }}mysection block{{ end }}`,
+		"_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
+		"_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
+	)
 
-		b.WithContent("mysection/p1.md", `---
+	b.WithContent("mysection/p1.md", `---
 title: My Page
 ---
 
 `)
 
-		b.CreateSites().Build(BuildCfg{})
+	b.CreateSites().Build(BuildCfg{})
 
-		b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
-		b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
-		b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
-		b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
-	})
+	b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
+	b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
+	b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
+	b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
 }
 
 func TestTemplateFuncs(t *testing.T) {
@@ -707,6 +715,7 @@ a: {{ $a }}
 	b.AssertFileContent("public/index.html", `a: [a b c]`)
 }
 
+// Legacy behavior for internal templates.
 func TestOverrideInternalTemplate(t *testing.T) {
 	files := `
 -- hugo.toml --
diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
index 8d34e069dd8..a23cee5393a 100644
--- a/langs/i18n/i18n_test.go
+++ b/langs/i18n/i18n_test.go
@@ -23,8 +23,6 @@ import (
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/config/testconfig"
 
-	"github.com/gohugoio/hugo/tpl/tplimpl"
-
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/spf13/afero"
 
@@ -472,7 +470,6 @@ func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider
 func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) {
 	d := testconfig.GetTestDeps(afs, cfg)
 	translationProvider := NewTranslationProvider()
-	d.TemplateProvider = tplimpl.DefaultTemplateProvider
 	d.TranslationProvider = translationProvider
 	d.Site = page.NewDummyHugoSite(d.Conf)
 	if err := d.Compile(nil); err != nil {
diff --git a/markup/goldmark/codeblocks/codeblocks_integration_test.go b/markup/goldmark/codeblocks/codeblocks_integration_test.go
index 8ed691302d6..97f4eb3846b 100644
--- a/markup/goldmark/codeblocks/codeblocks_integration_test.go
+++ b/markup/goldmark/codeblocks/codeblocks_integration_test.go
@@ -69,7 +69,7 @@ fmt.Println("Hello, World!");
 
 ## Golang Code
 
-§§§golang
+§§§go
 fmt.Println("Hello, Golang!");
 §§§
 
@@ -97,14 +97,14 @@ Go Language: go|
 Go Code: fmt.Println("Hello, World!");
 
 Go Code: fmt.Println("Hello, Golang!");
-Go Language: golang|
+Go Language: go|
 
 
 	`,
 		"Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
 		"Goat Attribute: 600|",
 		"<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|",
-		"<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|",
+		"<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: go|",
 		"<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">&#34;l1&#34;</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>",
 	)
 }
diff --git a/media/config_test.go b/media/config_test.go
index 6346860605d..5abbcac2f2e 100644
--- a/media/config_test.go
+++ b/media/config_test.go
@@ -151,5 +151,5 @@ func TestDefaultTypes(t *testing.T) {
 
 	}
 
-	c.Assert(len(DefaultTypes), qt.Equals, 40)
+	c.Assert(len(DefaultTypes), qt.Equals, 41)
 }
diff --git a/output/layouts/layout_test.go b/output/layouts/layout_test.go
deleted file mode 100644
index b6033247ce2..00000000000
--- a/output/layouts/layout_test.go
+++ /dev/null
@@ -1,982 +0,0 @@
-// Copyright 2017-present 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 layouts
-
-import (
-	"fmt"
-	"reflect"
-	"strings"
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/kylelemons/godebug/diff"
-)
-
-func TestLayout(t *testing.T) {
-	c := qt.New(t)
-
-	for _, this := range []struct {
-		name             string
-		layoutDescriptor LayoutDescriptor
-		layoutOverride   string
-		expect           []string
-	}{
-		{
-			"Home",
-			LayoutDescriptor{Kind: "home", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"index.amp.html",
-				"home.amp.html",
-				"list.amp.html",
-				"index.html",
-				"home.html",
-				"list.html",
-				"_default/index.amp.html",
-				"_default/home.amp.html",
-				"_default/list.amp.html",
-				"_default/index.html",
-				"_default/home.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Home baseof",
-			LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"index-baseof.amp.html",
-				"home-baseof.amp.html",
-				"list-baseof.amp.html",
-				"baseof.amp.html",
-				"index-baseof.html",
-				"home-baseof.html",
-				"list-baseof.html",
-				"baseof.html",
-				"_default/index-baseof.amp.html",
-				"_default/home-baseof.amp.html",
-				"_default/list-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/index-baseof.html",
-				"_default/home-baseof.html",
-				"_default/list-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Home, HTML",
-			LayoutDescriptor{Kind: "home", OutputFormatName: "html", Suffix: "html"},
-			"",
-			// We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand.
-			[]string{
-				"index.html.html",
-				"home.html.html",
-				"list.html.html",
-				"index.html",
-				"home.html",
-				"list.html",
-				"_default/index.html.html",
-				"_default/home.html.html",
-				"_default/list.html.html",
-				"_default/index.html",
-				"_default/home.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Home, HTML, baseof",
-			LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "html", Suffix: "html"},
-			"",
-			[]string{
-				"index-baseof.html.html",
-				"home-baseof.html.html",
-				"list-baseof.html.html",
-				"baseof.html.html",
-				"index-baseof.html",
-				"home-baseof.html",
-				"list-baseof.html",
-				"baseof.html",
-				"_default/index-baseof.html.html",
-				"_default/home-baseof.html.html",
-				"_default/list-baseof.html.html",
-				"_default/baseof.html.html",
-				"_default/index-baseof.html",
-				"_default/home-baseof.html",
-				"_default/list-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Home, french language",
-			LayoutDescriptor{Kind: "home", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"index.fr.amp.html",
-				"home.fr.amp.html",
-				"list.fr.amp.html",
-				"index.amp.html",
-				"home.amp.html",
-				"list.amp.html",
-				"index.fr.html",
-				"home.fr.html",
-				"list.fr.html",
-				"index.html",
-				"home.html",
-				"list.html",
-				"_default/index.fr.amp.html",
-				"_default/home.fr.amp.html",
-				"_default/list.fr.amp.html",
-				"_default/index.amp.html",
-				"_default/home.amp.html",
-				"_default/list.amp.html",
-				"_default/index.fr.html",
-				"_default/home.fr.html",
-				"_default/list.fr.html",
-				"_default/index.html",
-				"_default/home.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Home, no ext or delim",
-			LayoutDescriptor{Kind: "home", OutputFormatName: "nem", Suffix: ""},
-			"",
-			[]string{
-				"index.nem",
-				"home.nem",
-				"list.nem",
-				"_default/index.nem",
-				"_default/home.nem",
-				"_default/list.nem",
-			},
-		},
-		{
-			"Home, no ext",
-			LayoutDescriptor{Kind: "home", OutputFormatName: "nex", Suffix: ""},
-			"",
-			[]string{
-				"index.nex",
-				"home.nex",
-				"list.nex",
-				"_default/index.nex",
-				"_default/home.nex",
-				"_default/list.nex",
-			},
-		},
-		{
-			"Page, no ext or delim",
-			LayoutDescriptor{Kind: "page", OutputFormatName: "nem", Suffix: ""},
-			"",
-			[]string{"_default/single.nem"},
-		},
-		{
-			"Section",
-			LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"sect1/sect1.amp.html",
-				"sect1/section.amp.html",
-				"sect1/list.amp.html",
-				"sect1/sect1.html",
-				"sect1/section.html",
-				"sect1/list.html",
-				"section/sect1.amp.html",
-				"section/section.amp.html",
-				"section/list.amp.html",
-				"section/sect1.html",
-				"section/section.html",
-				"section/list.html",
-				"_default/sect1.amp.html",
-				"_default/section.amp.html",
-				"_default/list.amp.html",
-				"_default/sect1.html",
-				"_default/section.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Section, baseof",
-			LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"sect1/sect1-baseof.amp.html",
-				"sect1/section-baseof.amp.html",
-				"sect1/list-baseof.amp.html",
-				"sect1/baseof.amp.html",
-				"sect1/sect1-baseof.html",
-				"sect1/section-baseof.html",
-				"sect1/list-baseof.html",
-				"sect1/baseof.html",
-				"section/sect1-baseof.amp.html",
-				"section/section-baseof.amp.html",
-				"section/list-baseof.amp.html",
-				"section/baseof.amp.html",
-				"section/sect1-baseof.html",
-				"section/section-baseof.html",
-				"section/list-baseof.html",
-				"section/baseof.html",
-				"_default/sect1-baseof.amp.html",
-				"_default/section-baseof.amp.html",
-				"_default/list-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/sect1-baseof.html",
-				"_default/section-baseof.html",
-				"_default/list-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Section, baseof, French, AMP",
-			LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"sect1/sect1-baseof.fr.amp.html",
-				"sect1/section-baseof.fr.amp.html",
-				"sect1/list-baseof.fr.amp.html",
-				"sect1/baseof.fr.amp.html",
-				"sect1/sect1-baseof.amp.html",
-				"sect1/section-baseof.amp.html",
-				"sect1/list-baseof.amp.html",
-				"sect1/baseof.amp.html",
-				"sect1/sect1-baseof.fr.html",
-				"sect1/section-baseof.fr.html",
-				"sect1/list-baseof.fr.html",
-				"sect1/baseof.fr.html",
-				"sect1/sect1-baseof.html",
-				"sect1/section-baseof.html",
-				"sect1/list-baseof.html",
-				"sect1/baseof.html",
-				"section/sect1-baseof.fr.amp.html",
-				"section/section-baseof.fr.amp.html",
-				"section/list-baseof.fr.amp.html",
-				"section/baseof.fr.amp.html",
-				"section/sect1-baseof.amp.html",
-				"section/section-baseof.amp.html",
-				"section/list-baseof.amp.html",
-				"section/baseof.amp.html",
-				"section/sect1-baseof.fr.html",
-				"section/section-baseof.fr.html",
-				"section/list-baseof.fr.html",
-				"section/baseof.fr.html",
-				"section/sect1-baseof.html",
-				"section/section-baseof.html",
-				"section/list-baseof.html",
-				"section/baseof.html",
-				"_default/sect1-baseof.fr.amp.html",
-				"_default/section-baseof.fr.amp.html",
-				"_default/list-baseof.fr.amp.html",
-				"_default/baseof.fr.amp.html",
-				"_default/sect1-baseof.amp.html",
-				"_default/section-baseof.amp.html",
-				"_default/list-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/sect1-baseof.fr.html",
-				"_default/section-baseof.fr.html",
-				"_default/list-baseof.fr.html",
-				"_default/baseof.fr.html",
-				"_default/sect1-baseof.html",
-				"_default/section-baseof.html",
-				"_default/list-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Section with layout",
-			LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"sect1/mylayout.amp.html",
-				"sect1/sect1.amp.html",
-				"sect1/section.amp.html",
-				"sect1/list.amp.html",
-				"sect1/mylayout.html",
-				"sect1/sect1.html",
-				"sect1/section.html",
-				"sect1/list.html",
-				"section/mylayout.amp.html",
-				"section/sect1.amp.html",
-				"section/section.amp.html",
-				"section/list.amp.html",
-				"section/mylayout.html",
-				"section/sect1.html",
-				"section/section.html",
-				"section/list.html",
-				"_default/mylayout.amp.html",
-				"_default/sect1.amp.html",
-				"_default/section.amp.html",
-				"_default/list.amp.html",
-				"_default/mylayout.html",
-				"_default/sect1.html",
-				"_default/section.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Term, French, AMP",
-			LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"term/term.fr.amp.html",
-				"term/tags.fr.amp.html",
-				"term/taxonomy.fr.amp.html",
-				"term/list.fr.amp.html",
-				"term/term.amp.html",
-				"term/tags.amp.html",
-				"term/taxonomy.amp.html",
-				"term/list.amp.html",
-				"term/term.fr.html",
-				"term/tags.fr.html",
-				"term/taxonomy.fr.html",
-				"term/list.fr.html",
-				"term/term.html",
-				"term/tags.html",
-				"term/taxonomy.html",
-				"term/list.html",
-				"taxonomy/term.fr.amp.html",
-				"taxonomy/tags.fr.amp.html",
-				"taxonomy/taxonomy.fr.amp.html",
-				"taxonomy/list.fr.amp.html",
-				"taxonomy/term.amp.html",
-				"taxonomy/tags.amp.html",
-				"taxonomy/taxonomy.amp.html",
-				"taxonomy/list.amp.html",
-				"taxonomy/term.fr.html",
-				"taxonomy/tags.fr.html",
-				"taxonomy/taxonomy.fr.html",
-				"taxonomy/list.fr.html",
-				"taxonomy/term.html",
-				"taxonomy/tags.html",
-				"taxonomy/taxonomy.html",
-				"taxonomy/list.html",
-				"tags/term.fr.amp.html",
-				"tags/tags.fr.amp.html",
-				"tags/taxonomy.fr.amp.html",
-				"tags/list.fr.amp.html",
-				"tags/term.amp.html",
-				"tags/tags.amp.html",
-				"tags/taxonomy.amp.html",
-				"tags/list.amp.html",
-				"tags/term.fr.html",
-				"tags/tags.fr.html",
-				"tags/taxonomy.fr.html",
-				"tags/list.fr.html",
-				"tags/term.html",
-				"tags/tags.html",
-				"tags/taxonomy.html",
-				"tags/list.html",
-				"_default/term.fr.amp.html",
-				"_default/tags.fr.amp.html",
-				"_default/taxonomy.fr.amp.html",
-				"_default/list.fr.amp.html",
-				"_default/term.amp.html",
-				"_default/tags.amp.html",
-				"_default/taxonomy.amp.html",
-				"_default/list.amp.html",
-				"_default/term.fr.html",
-				"_default/tags.fr.html",
-				"_default/taxonomy.fr.html",
-				"_default/list.fr.html",
-				"_default/term.html",
-				"_default/tags.html",
-				"_default/taxonomy.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Term, baseof, French, AMP",
-			LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"term/term-baseof.fr.amp.html",
-				"term/tags-baseof.fr.amp.html",
-				"term/taxonomy-baseof.fr.amp.html",
-				"term/list-baseof.fr.amp.html",
-				"term/baseof.fr.amp.html",
-				"term/term-baseof.amp.html",
-				"term/tags-baseof.amp.html",
-				"term/taxonomy-baseof.amp.html",
-				"term/list-baseof.amp.html",
-				"term/baseof.amp.html",
-				"term/term-baseof.fr.html",
-				"term/tags-baseof.fr.html",
-				"term/taxonomy-baseof.fr.html",
-				"term/list-baseof.fr.html",
-				"term/baseof.fr.html",
-				"term/term-baseof.html",
-				"term/tags-baseof.html",
-				"term/taxonomy-baseof.html",
-				"term/list-baseof.html",
-				"term/baseof.html",
-				"taxonomy/term-baseof.fr.amp.html",
-				"taxonomy/tags-baseof.fr.amp.html",
-				"taxonomy/taxonomy-baseof.fr.amp.html",
-				"taxonomy/list-baseof.fr.amp.html",
-				"taxonomy/baseof.fr.amp.html",
-				"taxonomy/term-baseof.amp.html",
-				"taxonomy/tags-baseof.amp.html",
-				"taxonomy/taxonomy-baseof.amp.html",
-				"taxonomy/list-baseof.amp.html",
-				"taxonomy/baseof.amp.html",
-				"taxonomy/term-baseof.fr.html",
-				"taxonomy/tags-baseof.fr.html",
-				"taxonomy/taxonomy-baseof.fr.html",
-				"taxonomy/list-baseof.fr.html",
-				"taxonomy/baseof.fr.html",
-				"taxonomy/term-baseof.html",
-				"taxonomy/tags-baseof.html",
-				"taxonomy/taxonomy-baseof.html",
-				"taxonomy/list-baseof.html",
-				"taxonomy/baseof.html",
-				"tags/term-baseof.fr.amp.html",
-				"tags/tags-baseof.fr.amp.html",
-				"tags/taxonomy-baseof.fr.amp.html",
-				"tags/list-baseof.fr.amp.html",
-				"tags/baseof.fr.amp.html",
-				"tags/term-baseof.amp.html",
-				"tags/tags-baseof.amp.html",
-				"tags/taxonomy-baseof.amp.html",
-				"tags/list-baseof.amp.html",
-				"tags/baseof.amp.html",
-				"tags/term-baseof.fr.html",
-				"tags/tags-baseof.fr.html",
-				"tags/taxonomy-baseof.fr.html",
-				"tags/list-baseof.fr.html",
-				"tags/baseof.fr.html",
-				"tags/term-baseof.html",
-				"tags/tags-baseof.html",
-				"tags/taxonomy-baseof.html",
-				"tags/list-baseof.html",
-				"tags/baseof.html",
-				"_default/term-baseof.fr.amp.html",
-				"_default/tags-baseof.fr.amp.html",
-				"_default/taxonomy-baseof.fr.amp.html",
-				"_default/list-baseof.fr.amp.html",
-				"_default/baseof.fr.amp.html",
-				"_default/term-baseof.amp.html",
-				"_default/tags-baseof.amp.html",
-				"_default/taxonomy-baseof.amp.html",
-				"_default/list-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/term-baseof.fr.html",
-				"_default/tags-baseof.fr.html",
-				"_default/taxonomy-baseof.fr.html",
-				"_default/list-baseof.fr.html",
-				"_default/baseof.fr.html",
-				"_default/term-baseof.html",
-				"_default/tags-baseof.html",
-				"_default/taxonomy-baseof.html",
-				"_default/list-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Term",
-			LayoutDescriptor{Kind: "term", Section: "tags", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"term/term.amp.html",
-				"term/tags.amp.html",
-				"term/taxonomy.amp.html",
-				"term/list.amp.html",
-				"term/term.html",
-				"term/tags.html",
-				"term/taxonomy.html",
-				"term/list.html",
-				"taxonomy/term.amp.html",
-				"taxonomy/tags.amp.html",
-				"taxonomy/taxonomy.amp.html",
-				"taxonomy/list.amp.html",
-				"taxonomy/term.html",
-				"taxonomy/tags.html",
-				"taxonomy/taxonomy.html",
-				"taxonomy/list.html",
-				"tags/term.amp.html",
-				"tags/tags.amp.html",
-				"tags/taxonomy.amp.html",
-				"tags/list.amp.html",
-				"tags/term.html",
-				"tags/tags.html",
-				"tags/taxonomy.html",
-				"tags/list.html",
-				"_default/term.amp.html",
-				"_default/tags.amp.html",
-				"_default/taxonomy.amp.html",
-				"_default/list.amp.html",
-				"_default/term.html",
-				"_default/tags.html",
-				"_default/taxonomy.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Taxonomy",
-			LayoutDescriptor{Kind: "taxonomy", Section: "categories", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"categories/categories.terms.amp.html",
-				"categories/terms.amp.html",
-				"categories/taxonomy.amp.html",
-				"categories/list.amp.html",
-				"categories/categories.terms.html",
-				"categories/terms.html",
-				"categories/taxonomy.html",
-				"categories/list.html",
-				"taxonomy/categories.terms.amp.html",
-				"taxonomy/terms.amp.html",
-				"taxonomy/taxonomy.amp.html",
-				"taxonomy/list.amp.html",
-				"taxonomy/categories.terms.html",
-				"taxonomy/terms.html",
-				"taxonomy/taxonomy.html",
-				"taxonomy/list.html",
-				"_default/categories.terms.amp.html",
-				"_default/terms.amp.html",
-				"_default/taxonomy.amp.html",
-				"_default/list.amp.html",
-				"_default/categories.terms.html",
-				"_default/terms.html",
-				"_default/taxonomy.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Page",
-			LayoutDescriptor{Kind: "page", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"_default/single.amp.html",
-				"_default/single.html",
-			},
-		},
-		{
-			"Page, baseof",
-			LayoutDescriptor{Kind: "page", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"_default/single-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/single-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Page with layout",
-			LayoutDescriptor{Kind: "page", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"_default/mylayout.amp.html",
-				"_default/single.amp.html",
-				"_default/mylayout.html",
-				"_default/single.html",
-			},
-		},
-		{
-			"Page with layout, baseof",
-			LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"_default/mylayout-baseof.amp.html",
-				"_default/single-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/mylayout-baseof.html",
-				"_default/single-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Page with layout and type",
-			LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"myttype/mylayout.amp.html",
-				"myttype/single.amp.html",
-				"myttype/mylayout.html",
-				"myttype/single.html",
-				"_default/mylayout.amp.html",
-				"_default/single.amp.html",
-				"_default/mylayout.html",
-				"_default/single.html",
-			},
-		},
-		{
-			"Page baseof with layout and type",
-			LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"myttype/mylayout-baseof.amp.html",
-				"myttype/single-baseof.amp.html",
-				"myttype/baseof.amp.html",
-				"myttype/mylayout-baseof.html",
-				"myttype/single-baseof.html",
-				"myttype/baseof.html",
-				"_default/mylayout-baseof.amp.html",
-				"_default/single-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/mylayout-baseof.html",
-				"_default/single-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Page baseof with layout and type in French",
-			LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"myttype/mylayout-baseof.fr.amp.html",
-				"myttype/single-baseof.fr.amp.html",
-				"myttype/baseof.fr.amp.html",
-				"myttype/mylayout-baseof.amp.html",
-				"myttype/single-baseof.amp.html",
-				"myttype/baseof.amp.html",
-				"myttype/mylayout-baseof.fr.html",
-				"myttype/single-baseof.fr.html",
-				"myttype/baseof.fr.html",
-				"myttype/mylayout-baseof.html",
-				"myttype/single-baseof.html",
-				"myttype/baseof.html",
-				"_default/mylayout-baseof.fr.amp.html",
-				"_default/single-baseof.fr.amp.html",
-				"_default/baseof.fr.amp.html",
-				"_default/mylayout-baseof.amp.html",
-				"_default/single-baseof.amp.html",
-				"_default/baseof.amp.html",
-				"_default/mylayout-baseof.fr.html",
-				"_default/single-baseof.fr.html",
-				"_default/baseof.fr.html",
-				"_default/mylayout-baseof.html",
-				"_default/single-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Page with layout and type with subtype",
-			LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"myttype/mysubtype/mylayout.amp.html",
-				"myttype/mysubtype/single.amp.html",
-				"myttype/mysubtype/mylayout.html",
-				"myttype/mysubtype/single.html",
-				"_default/mylayout.amp.html",
-				"_default/single.amp.html",
-				"_default/mylayout.html",
-				"_default/single.html",
-			},
-		},
-		// RSS
-		{
-			"RSS Home",
-			LayoutDescriptor{Kind: "home", OutputFormatName: "rss", Suffix: "xml"},
-			"",
-			[]string{
-				"index.rss.xml",
-				"home.rss.xml",
-				"rss.xml",
-				"list.rss.xml",
-				"index.xml",
-				"home.xml",
-				"list.xml",
-				"_default/index.rss.xml",
-				"_default/home.rss.xml",
-				"_default/rss.xml",
-				"_default/list.rss.xml",
-				"_default/index.xml",
-				"_default/home.xml",
-				"_default/list.xml",
-				"_internal/_default/rss.xml",
-			},
-		},
-		{
-			"RSS Home, baseof",
-			LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "rss", Suffix: "xml"},
-			"",
-			[]string{
-				"index-baseof.rss.xml",
-				"home-baseof.rss.xml",
-				"list-baseof.rss.xml",
-				"baseof.rss.xml",
-				"index-baseof.xml",
-				"home-baseof.xml",
-				"list-baseof.xml",
-				"baseof.xml",
-				"_default/index-baseof.rss.xml",
-				"_default/home-baseof.rss.xml",
-				"_default/list-baseof.rss.xml",
-				"_default/baseof.rss.xml",
-				"_default/index-baseof.xml",
-				"_default/home-baseof.xml",
-				"_default/list-baseof.xml",
-				"_default/baseof.xml",
-			},
-		},
-		{
-			"RSS Section",
-			LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "rss", Suffix: "xml"},
-			"",
-			[]string{
-				"sect1/sect1.rss.xml",
-				"sect1/section.rss.xml",
-				"sect1/rss.xml",
-				"sect1/list.rss.xml",
-				"sect1/sect1.xml",
-				"sect1/section.xml",
-				"sect1/list.xml",
-				"section/sect1.rss.xml",
-				"section/section.rss.xml",
-				"section/rss.xml",
-				"section/list.rss.xml",
-				"section/sect1.xml",
-				"section/section.xml",
-				"section/list.xml",
-				"_default/sect1.rss.xml",
-				"_default/section.rss.xml",
-				"_default/rss.xml",
-				"_default/list.rss.xml",
-				"_default/sect1.xml",
-				"_default/section.xml",
-				"_default/list.xml",
-				"_internal/_default/rss.xml",
-			},
-		},
-		{
-			"RSS Term",
-			LayoutDescriptor{Kind: "term", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
-			"",
-			[]string{
-				"term/term.rss.xml",
-				"term/tag.rss.xml",
-				"term/taxonomy.rss.xml",
-				"term/rss.xml",
-				"term/list.rss.xml",
-				"term/term.xml",
-				"term/tag.xml",
-				"term/taxonomy.xml",
-				"term/list.xml",
-				"taxonomy/term.rss.xml",
-				"taxonomy/tag.rss.xml",
-				"taxonomy/taxonomy.rss.xml",
-				"taxonomy/rss.xml",
-				"taxonomy/list.rss.xml",
-				"taxonomy/term.xml",
-				"taxonomy/tag.xml",
-				"taxonomy/taxonomy.xml",
-				"taxonomy/list.xml",
-				"tag/term.rss.xml",
-				"tag/tag.rss.xml",
-				"tag/taxonomy.rss.xml",
-				"tag/rss.xml",
-				"tag/list.rss.xml",
-				"tag/term.xml",
-				"tag/tag.xml",
-				"tag/taxonomy.xml",
-				"tag/list.xml",
-				"_default/term.rss.xml",
-				"_default/tag.rss.xml",
-				"_default/taxonomy.rss.xml",
-				"_default/rss.xml",
-				"_default/list.rss.xml",
-				"_default/term.xml",
-				"_default/tag.xml",
-				"_default/taxonomy.xml",
-				"_default/list.xml",
-				"_internal/_default/rss.xml",
-			},
-		},
-		{
-			"RSS Taxonomy",
-			LayoutDescriptor{Kind: "taxonomy", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
-			"",
-			[]string{
-				"tag/tag.terms.rss.xml",
-				"tag/terms.rss.xml",
-				"tag/taxonomy.rss.xml",
-				"tag/rss.xml",
-				"tag/list.rss.xml",
-				"tag/tag.terms.xml",
-				"tag/terms.xml",
-				"tag/taxonomy.xml",
-				"tag/list.xml",
-				"taxonomy/tag.terms.rss.xml",
-				"taxonomy/terms.rss.xml",
-				"taxonomy/taxonomy.rss.xml",
-				"taxonomy/rss.xml",
-				"taxonomy/list.rss.xml",
-				"taxonomy/tag.terms.xml",
-				"taxonomy/terms.xml",
-				"taxonomy/taxonomy.xml",
-				"taxonomy/list.xml",
-				"_default/tag.terms.rss.xml",
-				"_default/terms.rss.xml",
-				"_default/taxonomy.rss.xml",
-				"_default/rss.xml",
-				"_default/list.rss.xml",
-				"_default/tag.terms.xml",
-				"_default/terms.xml",
-				"_default/taxonomy.xml",
-				"_default/list.xml",
-				"_internal/_default/rss.xml",
-			},
-		},
-		{
-			"Home plain text",
-			LayoutDescriptor{Kind: "home", OutputFormatName: "json", Suffix: "json"},
-			"",
-			[]string{
-				"index.json.json",
-				"home.json.json",
-				"list.json.json",
-				"index.json",
-				"home.json",
-				"list.json",
-				"_default/index.json.json",
-				"_default/home.json.json",
-				"_default/list.json.json",
-				"_default/index.json",
-				"_default/home.json",
-				"_default/list.json",
-			},
-		},
-		{
-			"Page plain text",
-			LayoutDescriptor{Kind: "page", OutputFormatName: "json", Suffix: "json"},
-			"",
-			[]string{
-				"_default/single.json.json",
-				"_default/single.json",
-			},
-		},
-		{
-			"Reserved section, shortcodes",
-			LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"section/shortcodes.amp.html",
-				"section/section.amp.html",
-				"section/list.amp.html",
-				"section/shortcodes.html",
-				"section/section.html",
-				"section/list.html",
-				"_default/shortcodes.amp.html",
-				"_default/section.amp.html",
-				"_default/list.amp.html",
-				"_default/shortcodes.html",
-				"_default/section.html",
-				"_default/list.html",
-			},
-		},
-		{
-			"Reserved section, partials",
-			LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"section/partials.amp.html",
-				"section/section.amp.html",
-				"section/list.amp.html",
-				"section/partials.html",
-				"section/section.html",
-				"section/list.html",
-				"_default/partials.amp.html",
-				"_default/section.amp.html",
-				"_default/list.amp.html",
-				"_default/partials.html",
-				"_default/section.html",
-				"_default/list.html",
-			},
-		},
-		// This is currently always HTML only
-		{
-			"404, HTML",
-			LayoutDescriptor{Kind: "404", OutputFormatName: "html", Suffix: "html"},
-			"",
-			[]string{
-				"404.html.html",
-				"404.html",
-			},
-		},
-		{
-			"404, HTML baseof",
-			LayoutDescriptor{Kind: "404", Baseof: true, OutputFormatName: "html", Suffix: "html"},
-			"",
-			[]string{
-				"404-baseof.html.html",
-				"baseof.html.html",
-				"404-baseof.html",
-				"baseof.html",
-				"_default/404-baseof.html.html",
-				"_default/baseof.html.html",
-				"_default/404-baseof.html",
-				"_default/baseof.html",
-			},
-		},
-		{
-			"Content hook",
-			LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog", OutputFormatName: "amp", Suffix: "html"},
-			"",
-			[]string{
-				"blog/_markup/render-link.amp.html",
-				"blog/_markup/render-link.html",
-				"_default/_markup/render-link.amp.html",
-				"_default/_markup/render-link.html",
-			},
-		},
-	} {
-		c.Run(this.name, func(c *qt.C) {
-			l := NewLayoutHandler()
-
-			layouts, err := l.For(this.layoutDescriptor)
-
-			c.Assert(err, qt.IsNil)
-			c.Assert(layouts, qt.Not(qt.IsNil), qt.Commentf(this.layoutDescriptor.Kind))
-
-			if !reflect.DeepEqual(layouts, this.expect) {
-				r := strings.NewReplacer(
-					"[", "\t\"",
-					"]", "\",",
-					" ", "\",\n\t\"",
-				)
-				fmtGot := r.Replace(fmt.Sprintf("%v", layouts))
-				fmtExp := r.Replace(fmt.Sprintf("%v", this.expect))
-
-				c.Fatalf("got %d items, expected %d:\nGot:\n\t%v\nExpected:\n\t%v\nDiff:\n%s", len(layouts), len(this.expect), layouts, this.expect, diff.Diff(fmtExp, fmtGot))
-
-			}
-		})
-	}
-}
-
-/*
-func BenchmarkLayout(b *testing.B) {
-	descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
-	l := NewLayoutHandler()
-
-	for i := 0; i < b.N; i++ {
-		_, err := l.For(descriptor, HTMLFormat)
-		if err != nil {
-			panic(err)
-		}
-	}
-}
-
-func BenchmarkLayoutUncached(b *testing.B) {
-	for i := 0; i < b.N; i++ {
-		descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
-		l := NewLayoutHandler()
-
-		_, err := l.For(descriptor, HTMLFormat)
-		if err != nil {
-			panic(err)
-		}
-	}
-}
-*/
diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go
index c86bcd63461..5950c590c70 100644
--- a/output/outputFormat_test.go
+++ b/output/outputFormat_test.go
@@ -68,7 +68,7 @@ func TestDefaultTypes(t *testing.T) {
 	c.Assert(RSSFormat.NoUgly, qt.Equals, true)
 	c.Assert(CalendarFormat.IsHTML, qt.Equals, false)
 
-	c.Assert(len(DefaultFormats), qt.Equals, 11)
+	c.Assert(len(DefaultFormats), qt.Equals, 15)
 }
 
 func TestGetFormatByName(t *testing.T) {
@@ -140,7 +140,7 @@ func TestGetFormatByFilename(t *testing.T) {
 func TestSort(t *testing.T) {
 	c := qt.New(t)
 	c.Assert(DefaultFormats[0].Name, qt.Equals, "html")
-	c.Assert(DefaultFormats[1].Name, qt.Equals, "amp")
+	c.Assert(DefaultFormats[1].Name, qt.Equals, "404")
 
 	json := JSONFormat
 	json.Weight = 1
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
index 19bdc0068a0..1d2ee6223a6 100644
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -221,7 +221,7 @@ func (p *testPage) GetTerms(taxonomy string) Pages {
 	panic("testpage: not implemented")
 }
 
-func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
+func (p *testPage) GetInternalRelatedDocsHandler() *RelatedDocsHandler {
 	return relatedDocsHandler
 }
 
diff --git a/testscripts/commands/hugo_printunusedtemplates.txt b/testscripts/commands/hugo_printunusedtemplates.txt
index eb9e068d72d..a7a5d87c3a9 100644
--- a/testscripts/commands/hugo_printunusedtemplates.txt
+++ b/testscripts/commands/hugo_printunusedtemplates.txt
@@ -1,6 +1,6 @@
 hugo  --printUnusedTemplates
 
-stderr 'Template _default/list.html is unused'
+stderr 'Template /list.html is unused'
 
 -- hugo.toml --
 disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]
diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go
deleted file mode 100644
index 0a5764264f4..00000000000
--- a/tpl/collections/apply_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// 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 collections
-
-import (
-	"context"
-	"fmt"
-	"io"
-	"reflect"
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/config/testconfig"
-	"github.com/gohugoio/hugo/identity"
-	"github.com/gohugoio/hugo/output"
-	"github.com/gohugoio/hugo/output/layouts"
-	"github.com/gohugoio/hugo/tpl"
-)
-
-type templateFinder int
-
-func (templateFinder) GetIdentity(string) (identity.Identity, bool) {
-	return identity.StringIdentity("test"), true
-}
-
-func (templateFinder) Lookup(name string) (tpl.Template, bool) {
-	return nil, false
-}
-
-func (templateFinder) HasTemplate(name string) bool {
-	return false
-}
-
-func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
-	return nil, false, false
-}
-
-func (templateFinder) LookupVariants(name string) []tpl.Template {
-	return nil
-}
-
-func (templateFinder) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
-	return nil, false, nil
-}
-
-func (templateFinder) Execute(t tpl.Template, wr io.Writer, data any) error {
-	return nil
-}
-
-func (templateFinder) ExecuteWithContext(ctx context.Context, t tpl.Template, wr io.Writer, data any) error {
-	return nil
-}
-
-func (templateFinder) GetFunc(name string) (reflect.Value, bool) {
-	if name == "dobedobedo" {
-		return reflect.Value{}, false
-	}
-
-	return reflect.ValueOf(fmt.Sprint), true
-}
-
-func TestApply(t *testing.T) {
-	t.Parallel()
-	c := qt.New(t)
-	d := testconfig.GetTestDeps(nil, nil)
-	d.SetTempl(&tpl.TemplateHandlers{
-		Tmpl: new(templateFinder),
-	})
-	ns := New(d)
-
-	strings := []any{"a\n", "b\n"}
-
-	ctx := context.Background()
-
-	result, err := ns.Apply(ctx, strings, "print", "a", "b", "c")
-	c.Assert(err, qt.IsNil)
-	c.Assert(result, qt.DeepEquals, []any{"abc", "abc"})
-
-	_, err = ns.Apply(ctx, strings, "apply", ".")
-	c.Assert(err, qt.Not(qt.IsNil))
-
-	var nilErr *error
-	_, err = ns.Apply(ctx, nilErr, "chomp", ".")
-	c.Assert(err, qt.Not(qt.IsNil))
-
-	_, err = ns.Apply(ctx, strings, "dobedobedo", ".")
-	c.Assert(err, qt.Not(qt.IsNil))
-
-	_, err = ns.Apply(ctx, strings, "foo.Chomp", "c\n")
-	if err == nil {
-		t.Errorf("apply with unknown func should fail")
-	}
-}
diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go
index 295a810b853..975ceea9306 100644
--- a/tpl/internal/go_templates/texttemplate/example_test.go
+++ b/tpl/internal/go_templates/texttemplate/example_test.go
@@ -35,7 +35,7 @@ Josie
 		Name, Gift string
 		Attended   bool
 	}
-	var recipients = []Recipient{
+	recipients := []Recipient{
 		{"Aunt Mildred", "bone china tea set", true},
 		{"Uncle John", "moleskin pants", false},
 		{"Cousin Rodney", "", false},
diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go
index 67abbc27e0b..d45a2aace70 100644
--- a/tpl/math/math_test.go
+++ b/tpl/math/math_test.go
@@ -24,7 +24,7 @@ func TestBasicNSArithmetic(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	type TestCase struct {
 		fn     func(inputs ...any) (any, error)
@@ -66,7 +66,7 @@ func TestBasicNSArithmetic(t *testing.T) {
 func TestAbs(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -93,7 +93,7 @@ func TestAbs(t *testing.T) {
 func TestCeil(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -126,7 +126,7 @@ func TestFloor(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -159,7 +159,7 @@ func TestLog(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -200,7 +200,7 @@ func TestSqrt(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -239,7 +239,7 @@ func TestMod(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -279,7 +279,7 @@ func TestModBool(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -325,7 +325,7 @@ func TestRound(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -358,7 +358,7 @@ func TestPow(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -398,7 +398,7 @@ func TestMax(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	type TestCase struct {
 		values []any
@@ -452,7 +452,7 @@ func TestMin(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	type TestCase struct {
 		values []any
@@ -507,7 +507,7 @@ func TestSum(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	mustSum := func(values ...any) any {
 		result, err := ns.Sum(values...)
@@ -530,7 +530,7 @@ func TestProduct(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	mustProduct := func(values ...any) any {
 		result, err := ns.Product(values...)
@@ -554,7 +554,7 @@ func TestPi(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	expect := 3.1415
 	result := ns.Pi()
@@ -570,7 +570,7 @@ func TestSin(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -604,7 +604,7 @@ func TestCos(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -638,7 +638,7 @@ func TestTan(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		a      any
@@ -680,7 +680,7 @@ func TestTan(t *testing.T) {
 func TestAsin(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -715,7 +715,7 @@ func TestAsin(t *testing.T) {
 func TestAcos(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -751,7 +751,7 @@ func TestAcos(t *testing.T) {
 func TestAtan(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -782,7 +782,7 @@ func TestAtan(t *testing.T) {
 func TestAtan2(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -821,7 +821,7 @@ func TestAtan2(t *testing.T) {
 func TestToDegrees(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
@@ -852,7 +852,7 @@ func TestToDegrees(t *testing.T) {
 func TestToRadians(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New()
+	ns := New(nil)
 
 	for _, test := range []struct {
 		x      any
diff --git a/tpl/partials/partials_integration_test.go b/tpl/partials/partials_integration_test.go
index f2bde29c314..6fab3abd827 100644
--- a/tpl/partials/partials_integration_test.go
+++ b/tpl/partials/partials_integration_test.go
@@ -170,7 +170,7 @@ D1
 	got := buf.String()
 
 	// Get rid of all the durations, they are never the same.
-	durationRe := regexp.MustCompile(`\b[\.\d]*(ms|µs|s)\b`)
+	durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
 
 	normalize := func(s string) string {
 		s = durationRe.ReplaceAllString(s, "")
@@ -193,10 +193,10 @@ D1
 
 	expect := `
 	0        0       0      1  index.html
-	100        0       0      1  partials/static2.html
-	100       50       1      2  partials/static1.html
-	25       50       2      4  partials/dynamic1.html
-	66       33       1      3  partials/halfdynamic1.html
+	100        0       0      1  _partials/static2.html
+	100       50       1      2  _partials/static1.html
+	25       50       2      4  _partials/dynamic1.html
+	66       33       1      3  _partials/halfdynamic1.html
 	`
 
 	b.Assert(got, hqt.IsSameString, expect)
diff --git a/tpl/template_test.go b/tpl/template_test.go
index 333513a3d56..4c68f8132d4 100644
--- a/tpl/template_test.go
+++ b/tpl/template_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2025 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.
@@ -15,20 +15,8 @@ package tpl
 
 import (
 	"testing"
-
-	qt "github.com/frankban/quicktest"
 )
 
-func TestExtractBaseof(t *testing.T) {
-	c := qt.New(t)
-
-	replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
-
-	c.Assert(replaced, qt.Equals, "_default/baseof.html")
-	c.Assert(extractBaseOf("not baseof for you"), qt.Equals, "")
-	c.Assert(extractBaseOf("template: blog/baseof.html:23:11:"), qt.Equals, "blog/baseof.html")
-}
-
 func TestStripHTML(t *testing.T) {
 	type test struct {
 		input, expected string
diff --git a/tpl/templates/defer_integration_test.go b/tpl/templates/defer_integration_test.go
index 27b8fbf8c57..f5ee09c06d6 100644
--- a/tpl/templates/defer_integration_test.go
+++ b/tpl/templates/defer_integration_test.go
@@ -71,6 +71,81 @@ AMP.
 
 `
 
+func TestDeferNoBaseof(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/index.html --
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+ Defer
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferBaseof(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferMain(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
 func TestDeferBasic(t *testing.T) {
 	t.Parallel()
 
diff --git a/tpl/tplimpl/render_hook_integration_test.go b/tpl/tplimpl/render_hook_integration_test.go
index 000470f8710..45c6cced114 100644
--- a/tpl/tplimpl/render_hook_integration_test.go
+++ b/tpl/tplimpl/render_hook_integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
+// Copyright 2025 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.
diff --git a/tpl/tplimpl/shortcodes_test.go b/tpl/tplimpl/shortcodes_test.go
deleted file mode 100644
index f97c7f27890..00000000000
--- a/tpl/tplimpl/shortcodes_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-// 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 tplimpl
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-)
-
-func TestShortcodesTemplate(t *testing.T) {
-	t.Run("isShortcode", func(t *testing.T) {
-		c := qt.New(t)
-		c.Assert(isShortcode("shortcodes/figures.html"), qt.Equals, true)
-		c.Assert(isShortcode("_internal/shortcodes/figures.html"), qt.Equals, true)
-		c.Assert(isShortcode("shortcodes\\figures.html"), qt.Equals, false)
-		c.Assert(isShortcode("myshortcodes"), qt.Equals, false)
-	})
-
-	t.Run("variantsFromName", func(t *testing.T) {
-		c := qt.New(t)
-		c.Assert(templateVariants("figure.html"), qt.DeepEquals, []string{"", "html", "html"})
-		c.Assert(templateVariants("figure.no.html"), qt.DeepEquals, []string{"no", "no", "html"})
-		c.Assert(templateVariants("figure.no.amp.html"), qt.DeepEquals, []string{"no", "amp", "html"})
-		c.Assert(templateVariants("figure.amp.html"), qt.DeepEquals, []string{"amp", "amp", "html"})
-
-		name, variants := templateNameAndVariants("figure.html")
-		c.Assert(name, qt.Equals, "figure")
-		c.Assert(variants, qt.DeepEquals, []string{"", "html", "html"})
-	})
-
-	t.Run("compareVariants", func(t *testing.T) {
-		c := qt.New(t)
-		var s *shortcodeTemplates
-
-		tests := []struct {
-			name     string
-			name1    string
-			name2    string
-			expected int
-		}{
-			{"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},
-		}
-
-		for _, test := range tests {
-			w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2))
-			c.Assert(w, qt.Equals, test.expected)
-		}
-	})
-
-	t.Run("indexOf", func(t *testing.T) {
-		c := qt.New(t)
-
-		s := &shortcodeTemplates{
-			variants: []shortcodeVariant{
-				{variants: []string{"a", "b", "c"}},
-				{variants: []string{"a", "b", "d"}},
-			},
-		}
-
-		c.Assert(s.indexOf([]string{"a", "b", "c"}), qt.Equals, 0)
-		c.Assert(s.indexOf([]string{"a", "b", "d"}), qt.Equals, 1)
-		c.Assert(s.indexOf([]string{"a", "b", "x"}), qt.Equals, -1)
-	})
-
-	t.Run("Name", func(t *testing.T) {
-		c := qt.New(t)
-
-		c.Assert(templateBaseName(templateShortcode, "shortcodes/foo.html"), qt.Equals, "foo.html")
-		c.Assert(templateBaseName(templateShortcode, "_internal/shortcodes/foo.html"), qt.Equals, "foo.html")
-		c.Assert(templateBaseName(templateShortcode, "shortcodes/test/foo.html"), qt.Equals, "test/foo.html")
-
-		c.Assert(true, qt.Equals, true)
-	})
-}
diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go
deleted file mode 100644
index 630415dac03..00000000000
--- a/tpl/tplimpl/template_ast_transformers_test.go
+++ /dev/null
@@ -1,161 +0,0 @@
-// 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 tplimpl
-
-import (
-	"testing"
-
-	template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/tpl"
-)
-
-// Issue #2927
-func TestTransformRecursiveTemplate(t *testing.T) {
-	c := qt.New(t)
-
-	recursive := `
-{{ define "menu-nodes" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ define "menu-node" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ template "menu-nodes" }}
-`
-
-	templ, err := template.New("foo").Parse(recursive)
-	c.Assert(err, qt.IsNil)
-	ts := newTestTemplate(templ)
-
-	ctx := newTemplateContext(
-		ts,
-		newTestTemplateLookup(ts),
-	)
-	ctx.applyTransformations(templ.Tree.Root)
-}
-
-func newTestTemplate(templ tpl.Template) *templateState {
-	return newTemplateState(nil,
-		templ,
-		templateInfo{
-			name: templ.Name(),
-		},
-		nil,
-	)
-}
-
-func newTestTemplateLookup(in *templateState) func(name string) *templateState {
-	m := make(map[string]*templateState)
-	return func(name string) *templateState {
-		if in.Name() == name {
-			return in
-		}
-
-		if ts, found := m[name]; found {
-			return ts
-		}
-
-		if templ, found := findTemplateIn(name, in); found {
-			ts := newTestTemplate(templ)
-			m[name] = ts
-			return ts
-		}
-
-		return nil
-	}
-}
-
-func TestCollectInfo(t *testing.T) {
-	configStr := `{ "version": 42 }`
-
-	tests := []struct {
-		name      string
-		tplString string
-		expected  tpl.ParseInfo
-	}{
-		{"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 any) any {
-		return in
-	}
-
-	funcs := template.FuncMap{
-		"highlight": echo,
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			c := qt.New(t)
-
-			templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
-			c.Assert(err, qt.IsNil)
-			ts := newTestTemplate(templ)
-			ts.typ = templateShortcode
-			ctx := newTemplateContext(
-				ts,
-				newTestTemplateLookup(ts),
-			)
-			ctx.applyTransformations(templ.Tree.Root)
-			c.Assert(ctx.t.parseInfo, qt.DeepEquals, test.expected)
-		})
-	}
-}
-
-func TestPartialReturn(t *testing.T) {
-	tests := []struct {
-		name      string
-		tplString string
-		expected  bool
-	}{
-		{"Basic", `
-{{ $a := "Hugo Rocks!" }}
-{{ return $a }}
-`, true},
-		{"Expression", `
-{{ return add 32 }}
-`, true},
-	}
-
-	echo := func(in any) any {
-		return in
-	}
-
-	funcs := template.FuncMap{
-		"return": echo,
-		"add":    echo,
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			c := qt.New(t)
-
-			templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
-			c.Assert(err, qt.IsNil)
-			ts := newTestTemplate(templ)
-			ctx := newTemplateContext(
-				ts,
-				newTestTemplateLookup(ts),
-			)
-
-			_, err = ctx.applyTransformations(templ.Tree.Root)
-
-			// Just check that it doesn't fail in this test. We have functional tests
-			// in hugoblib.
-			c.Assert(err, qt.IsNil)
-		})
-	}
-}
diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go
index 2c3cc541a84..639db909acc 100644
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 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.
diff --git a/tpl/tplimpl/template_test.go b/tpl/tplimpl/template_test.go
deleted file mode 100644
index 5e372d986a8..00000000000
--- a/tpl/tplimpl/template_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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 tplimpl
-
-import (
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-)
-
-func TestNeedsBaseTemplate(t *testing.T) {
-	c := qt.New(t)
-
-	c.Assert(needsBaseTemplate(`{{ define "main" }}`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`{{define "main" }}`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`{{-  define "main" }}`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`{{-define "main" }}`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`
-	
-	{{-define "main" }}
-	
-	`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`    {{ define "main" }}`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`
-	{{ define "main" }}`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`  A  {{ define "main" }}`), qt.Equals, false)
-	c.Assert(needsBaseTemplate(`  {{ printf "foo" }}`), qt.Equals, false)
-	c.Assert(needsBaseTemplate(`{{/* comment */}}    {{ define "main" }}`), qt.Equals, true)
-	c.Assert(needsBaseTemplate(`     {{/* comment */}}  A  {{ define "main" }}`), qt.Equals, false)
-}
diff --git a/tpl/tplimpl/templatedescriptor_test.go b/tpl/tplimpl/templatedescriptor_test.go
new file mode 100644
index 00000000000..76201287d46
--- /dev/null
+++ b/tpl/tplimpl/templatedescriptor_test.go
@@ -0,0 +1,104 @@
+package tplimpl
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/output"
+	"github.com/gohugoio/hugo/resources/kinds"
+)
+
+func TestTemplateDescriptorCompare(t *testing.T) {
+	c := qt.New(t)
+
+	dh := descriptorHandler{
+		opts: StoreOptions{
+			OutputFormats:       output.DefaultFormats,
+			DefaultOutputFormat: "html",
+		},
+	}
+
+	less := func(category Category, this, other1, other2 TemplateDescriptor) {
+		c.Helper()
+		result1 := dh.compareDescriptors(category, this, other1)
+		result2 := dh.compareDescriptors(category, this, other2)
+		c.Assert(result1.w1 < result2.w1, qt.IsTrue, qt.Commentf("%d < %d", result1, result2))
+	}
+
+	check := func(category Category, this, other TemplateDescriptor, less bool) {
+		c.Helper()
+		result := dh.compareDescriptors(category, this, other)
+		if less {
+			c.Assert(result.w1 < 0, qt.IsTrue, qt.Commentf("%d", result))
+		} else {
+			c.Assert(result.w1 >= 0, qt.IsTrue, qt.Commentf("%d", result))
+		}
+	}
+
+	check(
+
+		CategoryBaseof,
+		TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "404", MediaType: "text/html"},
+		TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "html", MediaType: "text/html"},
+		false,
+	)
+
+	check(
+		CategoryLayout,
+		TemplateDescriptor{Kind: "", Lang: "en", OutputFormat: "404", MediaType: "text/html"},
+		TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"},
+		true,
+	)
+
+	less(
+		CategoryLayout,
+		TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html"},
+		TemplateDescriptor{Layout: "list", OutputFormat: "html"},
+		TemplateDescriptor{Kind: kinds.KindHome, OutputFormat: "html"},
+	)
+
+	check(
+		CategoryLayout,
+		TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html", MediaType: "text/html"},
+		TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "myformat", MediaType: "text/html"},
+		false,
+	)
+}
+
+// INFO  timer:  name resolveTemplate count 779 duration 5.482274ms average 7.037µs median 4µs
+func BenchmarkCompareDescriptors(b *testing.B) {
+	dh := descriptorHandler{
+		opts: StoreOptions{
+			OutputFormats:       output.DefaultFormats,
+			DefaultOutputFormat: "html",
+		},
+	}
+
+	pairs := []struct {
+		d1, d2 TemplateDescriptor
+	}{
+		{
+			TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+			TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+		},
+		{
+			TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+			TemplateDescriptor{Kind: "", Layout: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+		},
+		{
+			TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+			TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+		},
+		{
+			TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+			TemplateDescriptor{Kind: "", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false},
+		},
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		for _, pair := range pairs {
+			_ = dh.compareDescriptors(CategoryLayout, pair.d1, pair.d2)
+		}
+	}
+}
diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go
new file mode 100644
index 00000000000..b0c30b98a53
--- /dev/null
+++ b/tpl/tplimpl/templatestore.go
@@ -0,0 +1,1854 @@
+package tplimpl
+
+import (
+	"bytes"
+	"context"
+	"embed"
+	"fmt"
+	"io"
+	"io/fs"
+	"iter"
+	"os"
+	"path"
+	"path/filepath"
+	"reflect"
+	"regexp"
+	"sort"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/gohugoio/hugo/common/herrors"
+	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/common/paths"
+	"github.com/gohugoio/hugo/helpers"
+	"github.com/gohugoio/hugo/hugofs"
+	"github.com/gohugoio/hugo/hugofs/files"
+	"github.com/gohugoio/hugo/hugolib/doctree"
+	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/media"
+	"github.com/gohugoio/hugo/metrics"
+	"github.com/gohugoio/hugo/output"
+	"github.com/gohugoio/hugo/resources/kinds"
+	"github.com/gohugoio/hugo/resources/page"
+	"github.com/gohugoio/hugo/tpl"
+	htmltemplate "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/spf13/afero"
+)
+
+const (
+	CategoryLayout Category = iota + 1
+	CategoryBaseof
+	CategoryMarkup
+	CategoryShortcode
+	CategoryPartial
+	// Internal categories
+	CategoryServer
+	CategoryHugo
+)
+
+const (
+	SubCategoryMain     SubCategory = iota
+	SubCategoryEmbedded             // Internal Hugo templates
+	SubCategoryInline               // Inline partials
+)
+
+const (
+	containerMarkup          = "_markup"
+	containerShortcodes      = "_shortcodes"
+	shortcodesPathIdentifier = "/_shortcodes/"
+	containerPartials        = "_partials"
+)
+
+const (
+	layoutAll    = "all"
+	layoutList   = "list"
+	layoutSingle = "single"
+)
+
+var (
+	_ identity.IdentityProvider             = (*TemplInfo)(nil)
+	_ identity.IsProbablyDependentProvider  = (*TemplInfo)(nil)
+	_ identity.IsProbablyDependencyProvider = (*TemplInfo)(nil)
+)
+
+const (
+	processingStateInitial processingState = iota
+	processingStateTransformed
+)
+
+// The identifiers may be truncated in the log, e.g.
+// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
+// We need this to identify position in templates with base templates applied.
+var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
+
+var weightNoMatch = weight{w1: -1}
+
+//
+//go:embed all:embedded/templates/*
+var embeddedTemplatesFs embed.FS
+
+func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
+	html, ok := opts.OutputFormats.GetByName("html")
+	if !ok {
+		panic("HTML output format not found")
+	}
+	s := &TemplateStore{
+		opts:            opts,
+		siteOpts:        siteOpts,
+		optsOrig:        opts,
+		siteOptsOrig:    siteOpts,
+		htmlFormat:      html,
+		storeSite:       configureSiteStorage(siteOpts, opts.Watching),
+		treeMain:        doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
+		treeShortcodes:  doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
+		templatesByPath: maps.NewCache[string, *TemplInfo](),
+
+		// Note that the funcs passed below is just for name validation.
+		tns: newTemplateNamespace(siteOpts.TemplateFuncs),
+
+		dh: descriptorHandler{
+			opts: opts,
+		},
+	}
+
+	if err := s.init(); err != nil {
+		return nil, err
+	}
+	if err := s.insertTemplates(nil, false); err != nil {
+		return nil, err
+	}
+	if err := s.insertEmbedded(); err != nil {
+		return nil, err
+	}
+	if err := s.parseTemplates(); err != nil {
+		return nil, err
+	}
+	if err := s.extractInlinePartials(); err != nil {
+		return nil, err
+	}
+	if err := s.transformTemplates(); err != nil {
+		return nil, err
+	}
+	if err := s.tns.createPrototypes(true); err != nil {
+		return nil, err
+	}
+	if err := s.prepareTemplates(); err != nil {
+		return nil, err
+	}
+	return s, nil
+}
+
+//go:generate stringer -type Category
+
+type Category int
+
+type SiteOptions struct {
+	Site          page.Site
+	TemplateFuncs map[string]any
+}
+
+type StoreOptions struct {
+	// The filesystem to use.
+	Fs afero.Fs
+
+	// The path parser to use.
+	PathParser *paths.PathParser
+
+	// Set when --enableTemplateMetrics is set.
+	Metrics metrics.Provider
+
+	// All configured output formats.
+	OutputFormats output.Formats
+
+	// All configured media types.
+	MediaTypes media.Types
+
+	// The default content language.
+	DefaultContentLanguage string
+
+	// The default output format.
+	DefaultOutputFormat string
+
+	// Taxonomy config.
+	TaxonomySingularPlural map[string]string
+
+	// Whether we are in watch or server mode.
+	Watching bool
+
+	// compiled.
+	legacyMappingTaxonomy map[string]legacyOrdinalMapping
+	legacyMappingTerm     map[string]legacyOrdinalMapping
+	legacyMappingSection  map[string]legacyOrdinalMapping
+}
+
+//go:generate stringer -type SubCategory
+
+type SubCategory int
+
+type TemplInfo struct {
+	// The category of this template.
+	Category Category
+
+	SubCategory SubCategory
+
+	// PathInfo info.
+	PathInfo *paths.Path
+
+	// Set when backed by a file.
+	Fi hugofs.FileMetaInfo
+
+	// The template content with any leading BOM removed.
+	Content string
+
+	// The parsed template.
+	// Note that any baseof template will be applied later.
+	Template tpl.Template
+
+	// If no baseof is needed, this will be set to true.
+	// E.g. shortcode templates do not need a baseof.
+	NoBaseOf bool
+
+	// If NoBaseOf is false, we will look for the final template in this tree.
+	BaseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]
+
+	// The template variants that are based on this template.
+	Overlays []*TemplInfo
+
+	// The base template used, if any.
+	Base *TemplInfo
+
+	// The descriptior that this template represents.
+	D TemplateDescriptor
+
+	// Parser state.
+	ParseInfo ParseInfo
+
+	// The execution counter for this template.
+	ExecutionCounter atomic.Uint64
+
+	// processing state.
+	state          processingState
+	isLegacyMapped bool
+}
+
+func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] {
+	return func(yield func(*TemplWithBaseApplied) bool) {
+		ti.BaseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+			for _, vv := range v {
+				if !yield(vv) {
+					return true, nil
+				}
+			}
+			return false, nil
+		})
+	}
+}
+
+func (t *TemplInfo) IdentifierBase() string {
+	if t.PathInfo == nil {
+		return t.Name()
+	}
+	return t.PathInfo.IdentifierBase()
+}
+
+func (t *TemplInfo) GetIdentity() identity.Identity {
+	return t
+}
+
+func (ti *TemplInfo) Name() string {
+	return ti.Template.Name()
+}
+
+func (ti *TemplInfo) Prepare() (*texttemplate.Template, error) {
+	return ti.Template.Prepare()
+}
+
+func (t *TemplInfo) IsProbablyDependency(other identity.Identity) bool {
+	return t.isProbablyTheSameIDAs(other)
+}
+
+func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool {
+	for _, overlay := range t.Overlays {
+		if overlay.isProbablyTheSameIDAs(other) {
+			return true
+		}
+	}
+	return t.isProbablyTheSameIDAs(other)
+}
+
+func (ti *TemplInfo) String() string {
+	if ti == nil {
+		return "<nil>"
+	}
+	return ti.PathInfo.String()
+}
+
+func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) {
+	if ti.BaseVariants == nil {
+		return
+	}
+
+	ti.BaseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+		slashCountK2 := strings.Count(k2, "/")
+		distance := slashCountK1 - slashCountK2
+
+		for d, vv := range v {
+			weight := s.dh.compareDescriptors(CategoryBaseof, ti.D, d)
+			weight.distance = distance
+			if best.isBetter(weight, vv.Template) {
+				best.updateValues(weight, k2, d, vv.Template)
+			}
+		}
+		return false, nil
+	})
+}
+
+func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool {
+	if t.IdentifierBase() == other.IdentifierBase() {
+		return true
+	}
+
+	if t.Fi != nil && t.Fi.Meta().PathInfo != t.PathInfo {
+		return other.IdentifierBase() == t.Fi.Meta().PathInfo.IdentifierBase()
+	}
+
+	return false
+}
+
+type TemplWithBaseApplied struct {
+	// The template that's overlaid on top of the base template.
+	Overlay *TemplInfo
+	// The base template.
+	Base *TemplInfo
+	// This is the final template that can be used to render a page.
+	Template *TemplInfo
+}
+
+// TemplateQuery is used in LookupPagesLayout to find the best matching template.
+type TemplateQuery struct {
+	// The path to walk down to.
+	Path string
+
+	// The name to look for. Used for shortcode queries.
+	Name string
+
+	// The category to look in.
+	Category Category
+
+	// The template descriptor to match against.
+	Desc TemplateDescriptor
+
+	// Whether to even consider this candidate.
+	Consider func(candidate *TemplInfo) bool
+}
+
+func (q *TemplateQuery) init() {
+	if q.Desc.Kind == kinds.KindTemporary {
+		q.Desc.Kind = ""
+	} else if kinds.GetKindMain(q.Desc.Kind) == "" {
+		q.Desc.Kind = ""
+	}
+	if q.Desc.Layout == "" && q.Desc.Kind != "" {
+		if q.Desc.Kind == kinds.KindPage {
+			q.Desc.Layout = layoutSingle
+		} else {
+			q.Desc.Layout = layoutList
+		}
+	}
+
+	if q.Consider == nil {
+		q.Consider = func(match *TemplInfo) bool {
+			return true
+		}
+	}
+
+	q.Name = strings.ToLower(q.Name)
+
+	if q.Category == 0 {
+		panic("category not set")
+	}
+}
+
+type TemplateStore struct {
+	opts       StoreOptions
+	siteOpts   SiteOptions
+	htmlFormat output.Format
+
+	treeMain        *doctree.SimpleTree[map[nodeKey]*TemplInfo]
+	treeShortcodes  *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
+	templatesByPath *maps.Cache[string, *TemplInfo]
+
+	dh descriptorHandler
+
+	// The template namespace.
+	tns *templateNamespace
+
+	// Site specific state.
+	// All above this is reused.
+	storeSite *storeSite
+
+	// For testing benchmarking.
+	optsOrig     StoreOptions
+	siteOptsOrig SiteOptions
+}
+
+// NewFromOpts creates a new store with the same configuration as the original.
+// Used for testing/benchmarking.
+func (s *TemplateStore) NewFromOpts() (*TemplateStore, error) {
+	return NewStore(s.optsOrig, s.siteOptsOrig)
+}
+
+// In the previous implementation of base templates in Hugo, we parsed and applied these base templates on
+// request, e.g. in the middle of rendering. The idea was that we coulnd't know upfront which layoyt/base template
+// combination that would be used.
+// This, however, added a lot of complexity involving a careful dance of template cloning and parsing
+// (Go HTML tenplates cannot be parsed after any of the templates in the tree have been executed).
+// FindAllBaseTemplateCandidates finds all base template candidates for the given descriptor so we can apply them upfront.
+// In this setup we may end up with unused base templates, but not having to do the cloning should more than make up for that.
+func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc TemplateDescriptor) []keyTemplateInfo {
+	var result []keyTemplateInfo
+	descBaseof := desc
+	s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) {
+		for _, vv := range v {
+			if vv.Category != CategoryBaseof {
+				continue
+			}
+
+			if vv.D.isKindInLayout(desc.Layout) && s.dh.compareDescriptors(CategoryBaseof, descBaseof, vv.D).w1 > 0 {
+				result = append(result, keyTemplateInfo{Key: k, Info: vv})
+			}
+		}
+		return false, nil
+	})
+
+	return result
+}
+
+func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
+	defer func() {
+		ti.ExecutionCounter.Add(1)
+		if ti.Base != nil {
+			ti.Base.ExecutionCounter.Add(1)
+		}
+	}()
+
+	templ := ti.Template
+
+	if t.opts.Metrics != nil {
+		defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
+	}
+
+	execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data)
+	if execErr != nil {
+		return t.addFileContext(ti, execErr)
+	}
+	return nil
+}
+
+func (t *TemplateStore) GetFunc(name string) (reflect.Value, bool) {
+	v, found := t.storeSite.execHelper.funcs[name]
+	return v, found
+}
+
+func (s *TemplateStore) GetIdentity(p string) identity.Identity {
+	p = paths.AddLeadingSlash(p)
+	v, found := s.templatesByPath.Get(p)
+	if !found {
+		return nil
+	}
+	return v.GetIdentity()
+}
+
+func (t *TemplateStore) LookupByPath(templatePath string) *TemplInfo {
+	v, _ := t.templatesByPath.Get(templatePath)
+	return v
+}
+
+var bestPool = sync.Pool{
+	New: func() any {
+		return &bestMatch{}
+	},
+}
+
+func (s *TemplateStore) getBest() *bestMatch {
+	v := bestPool.Get()
+	b := v.(*bestMatch)
+	b.defaultOutputformat = s.opts.DefaultOutputFormat
+	return b
+}
+
+func (s *TemplateStore) putBest(b *bestMatch) {
+	b.reset()
+	bestPool.Put(b)
+}
+
+func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
+	q.init()
+	key := s.key(q.Path)
+
+	slashCountKey := strings.Count(key, "/")
+	best1 := s.getBest()
+	defer s.putBest(best1)
+	s.findBestMatchWalkPath(q, key, slashCountKey, best1)
+	if best1.w.w1 <= 0 {
+		return nil
+	}
+	m := best1.templ
+	if m.NoBaseOf {
+		return m
+	}
+	best1.reset()
+	m.findBestMatchBaseof(s, key, slashCountKey, best1)
+	if best1.w.w1 <= 0 {
+		return nil
+	}
+	return best1.templ
+}
+
+func (s *TemplateStore) LookupPartial(pth string, desc TemplateDescriptor) *TemplInfo {
+	if desc.Layout != "" {
+		panic("shortcode template descriptor must not have a layout")
+	}
+	best := s.getBest()
+	defer s.putBest(best)
+	s.findBestMatchGet(s.key(path.Join(containerPartials, pth)), CategoryPartial, nil, desc, best)
+	return best.templ
+}
+
+func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
+	q.init()
+	k1 := s.key(q.Path)
+
+	slashCountK1 := strings.Count(k1, "/")
+
+	best := s.getBest()
+	defer s.putBest(best)
+
+	s.treeShortcodes.WalkPath(k1, func(k2 string, m map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+		slashCountK2 := strings.Count(k2, "/")
+		distance := slashCountK1 - slashCountK2
+
+		v, found := m[q.Name]
+		if !found {
+			return false, nil
+		}
+
+		for k, vv := range v {
+			if !q.Consider(vv) {
+				continue
+			}
+
+			weight := s.dh.compareDescriptors(q.Category, q.Desc, k)
+			weight.distance = distance
+			if best.isBetter(weight, vv) {
+				best.updateValues(weight, k2, k, vv)
+			}
+		}
+
+		return false, nil
+	})
+
+	// Any match will do.
+	return best.templ
+}
+
+// PrintDebug is for testing/debugging only.
+func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer) {
+	if w == nil {
+		w = os.Stdout
+	}
+
+	printOne := func(key string, vv *TemplInfo) {
+		level := strings.Count(key, "/")
+		if category != vv.Category {
+			return
+		}
+		s := strings.ReplaceAll(strings.TrimSpace(vv.Content), "\n", " ")
+		ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s)
+		fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts)
+	}
+	s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+		for _, vv := range v {
+			printOne(key, vv)
+		}
+		return false, nil
+	})
+	s.treeShortcodes.WalkPrefix(prefix, func(key string, v map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+		for _, vv := range v {
+			for _, vv2 := range vv {
+				printOne(key, vv2)
+			}
+		}
+		return false, nil
+	})
+}
+
+// RefreshFiles refreshes this store for the files matching the given predicate.
+func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
+	if err := s.tns.createPrototypesParse(); err != nil {
+		return err
+	}
+	if err := s.insertTemplates(include, true); err != nil {
+		return err
+	}
+	if err := s.parseTemplates(); err != nil {
+		return err
+	}
+	if err := s.extractInlinePartials(); err != nil {
+		return err
+	}
+	if err := s.transformTemplates(); err != nil {
+		return err
+	}
+	if err := s.tns.createPrototypes(false); err != nil {
+		return err
+	}
+	if err := s.prepareTemplates(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *TemplateStore) HasTemplate(templatePath string) bool {
+	templatePath = paths.AddLeadingSlash(templatePath)
+	return s.templatesByPath.Contains(templatePath)
+}
+
+func (t *TemplateStore) TextLookup(name string) *TemplInfo {
+	templ := t.tns.standaloneText.Lookup(name)
+	if templ == nil {
+		return nil
+	}
+	return &TemplInfo{
+		Template: templ,
+	}
+}
+
+func (t *TemplateStore) TextParse(name, tpl string) (*TemplInfo, error) {
+	templ, err := t.tns.standaloneText.New(name).Parse(tpl)
+	if err != nil {
+		return nil, err
+	}
+	return &TemplInfo{
+		Template: templ,
+	}, nil
+}
+
+func (t *TemplateStore) UnusedTemplates() []*TemplInfo {
+	var unused []*TemplInfo
+
+	for vv := range t.templates() {
+		if vv.SubCategory != SubCategoryMain {
+			// Skip inline partials and internal templates.
+			continue
+		}
+		if vv.NoBaseOf {
+			if vv.ExecutionCounter.Load() == 0 {
+				unused = append(unused, vv)
+			}
+		} else {
+			for vvv := range vv.BaseVariantsSeq() {
+				if vvv.Template.ExecutionCounter.Load() == 0 {
+					unused = append(unused, vvv.Template)
+				}
+			}
+		}
+	}
+
+	sort.Sort(byPath(unused))
+	return unused
+}
+
+// WithSiteOpts creates a new store with the given site options.
+// This is used to create per site template store, all sharing the same templates,
+// but with a different template function execution context.
+func (s TemplateStore) WithSiteOpts(opts SiteOptions) *TemplateStore {
+	s.siteOpts = opts
+	s.storeSite = configureSiteStorage(opts, s.opts.Watching)
+	return &s
+}
+
+func (s *TemplateStore) findBestMatchGet(key string, category Category, consider func(candidate *TemplInfo) bool, desc TemplateDescriptor, best *bestMatch) {
+	key = strings.ToLower(key)
+
+	v := s.treeMain.Get(key)
+	if v == nil {
+		return
+	}
+
+	for k, vv := range v {
+		if vv.Category != category {
+			continue
+		}
+
+		if consider != nil && !consider(vv) {
+			continue
+		}
+
+		weight := s.dh.compareDescriptors(category, desc, k.d)
+		if best.isBetter(weight, vv) {
+			best.updateValues(weight, key, k.d, vv)
+		}
+	}
+}
+
+func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashCountK1 int, best *bestMatch) {
+	s.treeMain.WalkPath(k1, func(k2 string, v map[nodeKey]*TemplInfo) (bool, error) {
+		slashCountK2 := strings.Count(k2, "/")
+		distance := slashCountK1 - slashCountK2
+
+		for k, vv := range v {
+			if vv.Category != q.Category {
+				continue
+			}
+
+			if !q.Consider(vv) {
+				continue
+			}
+
+			weight := s.dh.compareDescriptors(q.Category, q.Desc, k.d)
+
+			weight.distance = distance
+			isBetter := best.isBetter(weight, vv)
+
+			if isBetter {
+				best.updateValues(weight, k2, k.d, vv)
+			}
+		}
+
+		return false, nil
+	})
+}
+
+func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *parse.ListNode) error {
+	if _, found := t.templatesByPath.Get(name); found {
+		return nil
+	}
+
+	var templ tpl.Template
+
+	if owner.D.IsPlainText {
+		prototype := t.tns.parseText
+		tt, err := prototype.New(name).Parse("")
+		if err != nil {
+			return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
+		}
+		tt.Tree.Root = n
+		templ = tt
+	} else {
+		prototype := t.tns.parseHTML
+		tt, err := prototype.New(name).Parse("")
+		if err != nil {
+			return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
+		}
+		tt.Tree.Root = n
+		templ = tt
+	}
+
+	t.templatesByPath.Set(name, &TemplInfo{
+		Fi:       owner.Fi,
+		PathInfo: owner.PathInfo,
+		D:        owner.D,
+		Template: templ,
+	})
+
+	return nil
+}
+
+func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error {
+	if ti.Fi == nil {
+		return inerr
+	}
+
+	identifiers := s.extractIdentifiers(inerr.Error())
+
+	checkFilename := func(fi hugofs.FileMetaInfo, inErr error) (error, bool) {
+		lineMatcher := func(m herrors.LineMatcher) int {
+			if m.Position.LineNumber != m.LineNumber {
+				return -1
+			}
+
+			for _, id := range identifiers {
+				if strings.Contains(m.Line, id) {
+					// We found the line, but return a 0 to signal to
+					// use the column from the error message.
+					return 0
+				}
+			}
+			return -1
+		}
+
+		f, err := fi.Meta().Open()
+		if err != nil {
+			return inErr, false
+		}
+		defer f.Close()
+
+		fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename)
+		fe.UpdateContent(f, lineMatcher)
+
+		if !fe.ErrorContext().Position.IsValid() {
+			return inErr, false
+		}
+		return fe, true
+	}
+
+	inerr = fmt.Errorf("execute of template failed: %w", inerr)
+
+	if err, ok := checkFilename(ti.Fi, inerr); ok {
+		return err
+	}
+
+	if ti.Base != nil {
+		if err, ok := checkFilename(ti.Base.Fi, inerr); ok {
+			return err
+		}
+	}
+
+	return inerr
+}
+
+func (s *TemplateStore) extractIdentifiers(line string) []string {
+	m := identifiersRe.FindAllStringSubmatch(line, -1)
+	identifiers := make([]string, len(m))
+	for i := range m {
+		identifiers[i] = m[i][1]
+	}
+	return identifiers
+}
+
+func (s *TemplateStore) extractInlinePartials() error {
+	isPartialName := func(s string) bool {
+		return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
+	}
+
+	p := s.tns
+	// We may find both inline and external partials in the current template namespaces,
+	// so only add the ones we have not seen before.
+	addIfNotSeen := func(isText bool, templs ...tpl.Template) error {
+		for _, templ := range templs {
+			if templ.Name() == "" || !isPartialName(templ.Name()) {
+				continue
+			}
+			name := templ.Name()
+			if !paths.HasExt(name) {
+				// Assume HTML. This in line with how the lookup works.
+				name = name + ".html"
+			}
+			if !strings.HasPrefix(name, "_") {
+				name = "_" + name
+			}
+			pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+			ti, err := s.insertTemplate(pi, nil, false, s.treeMain)
+			if err != nil {
+				return err
+			}
+
+			if ti != nil {
+				ti.Template = templ
+				ti.NoBaseOf = true
+				ti.SubCategory = SubCategoryInline
+				ti.D.IsPlainText = isText
+			}
+
+		}
+		return nil
+	}
+	addIfNotSeen(false, p.templatesIn(p.parseHTML)...)
+	addIfNotSeen(true, p.templatesIn(p.parseText)...)
+
+	for _, t := range p.baseofHtmlClones {
+		if err := addIfNotSeen(false, p.templatesIn(t)...); err != nil {
+			return err
+		}
+	}
+	for _, t := range p.baseofTextClones {
+		if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *TemplateStore) insertEmbedded() error {
+	return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		if d == nil || d.IsDir() || strings.HasPrefix(d.Name(), ".") {
+			return nil
+		}
+
+		templb, err := embeddedTemplatesFs.ReadFile(path)
+		if err != nil {
+			return err
+		}
+
+		// Get the newlines on Windows in line with how we had it back when we used Go Generate
+		// to write the templates to Go files.
+		templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
+		name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
+
+		insertOne := func(name, content string) error {
+			pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+			var (
+				ti  *TemplInfo
+				err error
+			)
+			if pi.Section() == containerShortcodes {
+				ti, err = s.insertShortcode(pi, nil, false, s.treeShortcodes)
+				if err != nil {
+					return err
+				}
+			} else {
+				ti, err = s.insertTemplate(pi, nil, false, s.treeMain)
+				if err != nil {
+					return err
+				}
+			}
+
+			if ti != nil {
+				// Currently none of the embedded templates need a baseof template.
+				ti.NoBaseOf = true
+				ti.Content = content
+				ti.SubCategory = SubCategoryEmbedded
+			}
+
+			return nil
+		}
+
+		if err := insertOne(name, templ); err != nil {
+			return err
+		}
+
+		if aliases, found := embeddedTemplatesAliases[name]; found {
+			for _, alias := range aliases {
+				if err := insertOne(alias, templ); err != nil {
+					return err
+				}
+			}
+		}
+
+		return nil
+	})
+}
+
+func (s *TemplateStore) setTemplateByPath(p string, ti *TemplInfo) {
+	s.templatesByPath.Set(p, ti)
+}
+
+func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[string]map[TemplateDescriptor]*TemplInfo]) (*TemplInfo, error) {
+	k1, k2, _, d := s.toKeyCategoryAndDescriptor(pi)
+	m := tree.Get(k1)
+	if m == nil {
+		m = make(map[string]map[TemplateDescriptor]*TemplInfo)
+		tree.Insert(k1, m)
+	}
+
+	m1, found := m[k2]
+	if found {
+		if _, found := m1[d]; found {
+			if !replace {
+				return nil, nil
+			}
+		}
+	} else {
+		m1 = make(map[TemplateDescriptor]*TemplInfo)
+		m[k2] = m1
+	}
+
+	ti := &TemplInfo{
+		PathInfo: pi,
+		Fi:       fi,
+		D:        d,
+		Category: CategoryShortcode,
+		NoBaseOf: true,
+	}
+
+	m1[d] = ti
+
+	s.setTemplateByPath(pi.Path(), ti)
+
+	if fi != nil {
+		if pi2 := fi.Meta().PathInfo; pi2 != pi {
+			s.setTemplateByPath(pi2.Path(), ti)
+		}
+	}
+
+	return ti, nil
+}
+
+func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
+	key, _, category, d := s.toKeyCategoryAndDescriptor(pi)
+
+	return s.insertTemplate2(pi, fi, key, category, d, replace, false, tree)
+}
+
+func (s *TemplateStore) insertTemplate2(
+	pi *paths.Path,
+	fi hugofs.FileMetaInfo,
+	key string,
+	category Category,
+	d TemplateDescriptor,
+	replace, isLegacyMapped bool,
+	tree doctree.Tree[map[nodeKey]*TemplInfo],
+) (*TemplInfo, error) {
+	if category == 0 {
+		panic("category not set")
+	}
+
+	m := tree.Get(key)
+	nk := nodeKey{c: category, d: d}
+
+	if m == nil {
+		m = make(map[nodeKey]*TemplInfo)
+		tree.Insert(key, m)
+	}
+
+	if !replace {
+		if v, found := m[nk]; found {
+			if len(pi.IdentifiersUnknown()) >= len(v.PathInfo.IdentifiersUnknown()) {
+				// e.g. /pages/home.foo.html and  /pages/home.html where foo may be a valid language name in another site.
+				return nil, nil
+			}
+		}
+	}
+
+	ti := &TemplInfo{
+		PathInfo:       pi,
+		Fi:             fi,
+		D:              d,
+		Category:       category,
+		NoBaseOf:       category > CategoryLayout,
+		isLegacyMapped: isLegacyMapped,
+	}
+
+	m[nk] = ti
+
+	if !isLegacyMapped {
+		s.setTemplateByPath(pi.Path(), ti)
+		if fi != nil {
+			if pi2 := fi.Meta().PathInfo; pi2 != pi {
+				s.setTemplateByPath(pi2.Path(), ti)
+			}
+		}
+	}
+
+	return ti, nil
+}
+
+func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, replace bool) error {
+	if include == nil {
+		include = func(fi hugofs.FileMetaInfo) bool {
+			return true
+		}
+	}
+
+	// Set if we need to reset the base variants.
+	var (
+		resetBaseVariants bool
+	)
+
+	legacyOrdinalMappings := map[legacyTargetPathIdentifiers]legacyOrdinalMappingFi{}
+
+	walker := func(pth string, fi hugofs.FileMetaInfo) error {
+		piOrig := fi.Meta().PathInfo
+		if fi.IsDir() {
+			return nil
+		}
+
+		if !include(fi) {
+			return nil
+		}
+
+		// Convert any legacy value to new format.
+		fromLegacyPath := func(pi *paths.Path) *paths.Path {
+			p := pi.Path()
+			p = strings.TrimPrefix(p, "/_default")
+			if strings.HasPrefix(p, "/shortcodes") || strings.HasPrefix(p, "/partials") {
+				// Insert an underscore so it becomes /_shortcodes or /_partials.
+				p = "/_" + p[1:]
+			}
+
+			if strings.Contains(p, "-"+baseNameBaseof) {
+				// Before Hugo 0.146.0 we prepended one identifier (layout, type or kind) in front of the baseof keyword,
+				// and then separated with a hyphen before the baseof keyword.
+				// This identifier needs to be moved right after the baseof keyword and the hyphen removed, e.g.
+				// /docs/list-baseof.html => /docs/baseof.list.html.
+				dir, name := path.Split(p)
+				hyphenIdx := strings.Index(name, "-")
+				if hyphenIdx > 0 {
+					id := name[:hyphenIdx]
+					name = name[hyphenIdx+1+len(baseNameBaseof):]
+					if !strings.HasPrefix(name, ".") {
+						name = "." + name
+					}
+					p = path.Join(dir, baseNameBaseof+"."+id+name)
+				}
+			}
+			if p == pi.Path() {
+				return pi
+			}
+			return s.opts.PathParser.Parse(files.ComponentFolderLayouts, p)
+		}
+
+		pi := piOrig
+		var applyLegacyMapping bool
+		switch pi.Section() {
+		case containerPartials, containerShortcodes, containerMarkup:
+			// OK.
+		default:
+			applyLegacyMapping = true
+			pi = fromLegacyPath(pi)
+		}
+
+		if applyLegacyMapping {
+			handleMapping := func(m1 legacyOrdinalMapping) {
+				key := legacyTargetPathIdentifiers{
+					targetPath:     m1.mapping.targetPath,
+					targetCategory: m1.mapping.targetCategory,
+					kind:           m1.mapping.targetDesc.Kind,
+					lang:           pi.Lang(),
+					ext:            pi.Ext(),
+					outputFormat:   pi.OutputFormat(),
+				}
+				if m2, ok := legacyOrdinalMappings[key]; ok {
+					if m1.ordinal < m2.m.ordinal {
+						// Higher up == better match.
+						legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+					}
+				} else {
+					legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+				}
+			}
+
+			if m1, ok := s.opts.legacyMappingTaxonomy[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+				handleMapping(m1)
+			}
+
+			if m1, ok := s.opts.legacyMappingTerm[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+				handleMapping(m1)
+			}
+
+			const (
+				sectionKindToken = "SECTIONKIND"
+				sectionToken     = "THESECTION"
+			)
+
+			base := piOrig.PathBeforeLangAndOutputFormatAndExt()
+			identifiers := pi.IdentifiersUnknown()
+
+			// Tokens on e.g. form /SECTIONKIND/THESECTION
+			insertSectionTokens := func(section string, kindOnly bool) string {
+				s := base
+				if !kindOnly {
+					s = strings.Replace(s, section, sectionToken, 1)
+				}
+				s = strings.Replace(s, kinds.KindSection, sectionKindToken, 1)
+				return s
+			}
+
+			for _, section := range identifiers {
+				if section == baseNameBaseof {
+					continue
+				}
+				kindOnly := isLayoutStandard(section)
+				p := insertSectionTokens(section, kindOnly)
+				if m1, ok := s.opts.legacyMappingSection[p]; ok {
+					m1.mapping.targetPath = strings.Replace(m1.mapping.targetPath, sectionToken, section, 1)
+					handleMapping(m1)
+				}
+			}
+
+		}
+
+		if replace && pi.NameNoIdentifier() == baseNameBaseof {
+			// A baseof file has changed.
+			resetBaseVariants = true
+		}
+
+		var ti *TemplInfo
+		var err error
+		if pi.Type() == paths.TypeShortcode {
+			ti, err = s.insertShortcode(pi, fi, replace, s.treeShortcodes)
+			if err != nil || ti == nil {
+				return err
+			}
+		} else {
+			ti, err = s.insertTemplate(pi, fi, replace, s.treeMain)
+			if err != nil || ti == nil {
+				return err
+			}
+		}
+
+		if err := s.tns.readTemplateInto(ti); err != nil {
+			return err
+		}
+
+		return nil
+	}
+
+	if err := helpers.Walk(s.opts.Fs, "", walker); err != nil {
+		if !herrors.IsNotExist(err) {
+			return err
+		}
+		return nil
+	}
+
+	for k, v := range legacyOrdinalMappings {
+		targetPath := k.targetPath
+		m := v.m.mapping
+		fi := v.fi
+		pi := fi.Meta().PathInfo
+		outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(k.outputFormat, k.ext)
+		category := m.targetCategory
+		desc := m.targetDesc
+		desc.Kind = k.kind
+		desc.Lang = k.lang
+		desc.OutputFormat = outputFormat.Name
+		desc.IsPlainText = outputFormat.IsPlainText
+		desc.MediaType = mediaType.Type
+
+		ti, err := s.insertTemplate2(pi, fi, targetPath, category, desc, true, true, s.treeMain)
+		if err != nil {
+			return err
+		}
+		if ti == nil {
+			continue
+		}
+		ti.isLegacyMapped = true
+		if err := s.tns.readTemplateInto(ti); err != nil {
+			return err
+		}
+	}
+
+	if resetBaseVariants {
+		s.tns.baseofHtmlClones = nil
+		s.tns.baseofTextClones = nil
+		s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+			for _, vv := range v {
+				if !vv.NoBaseOf {
+					vv.state = processingStateInitial
+				}
+			}
+			return false, nil
+		})
+	}
+
+	return nil
+}
+
+func (s *TemplateStore) key(dir string) string {
+	dir = paths.AddLeadingSlash(dir)
+	if dir == "/" {
+		return ""
+	}
+	return paths.TrimTrailing(dir)
+}
+
+func (s *TemplateStore) parseTemplates() error {
+	if err := func() error {
+		// Read and parse all templates.
+		for _, v := range s.treeMain.All() {
+			for _, vv := range v {
+				if vv.state == processingStateTransformed {
+					continue
+				}
+				if err := s.tns.parseTemplate(vv); err != nil {
+					return err
+				}
+			}
+		}
+
+		// Lookup and apply base templates where needed.
+		for key, v := range s.treeMain.All() {
+			for _, vv := range v {
+				if vv.state == processingStateTransformed {
+					continue
+				}
+				if !vv.NoBaseOf {
+					d := vv.D
+					// Find all compatible base templates.
+					baseTemplates := s.FindAllBaseTemplateCandidates(key, d)
+					if len(baseTemplates) == 0 {
+						// The regular expression used to detect if a template needs a base template has some
+						// rare false positives. Assume we don't need one.
+						vv.NoBaseOf = true
+						if err := s.tns.parseTemplate(vv); err != nil {
+							return err
+						}
+						continue
+					}
+					vv.BaseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]()
+
+					for _, base := range baseTemplates {
+						if err := s.tns.applyBaseTemplate(vv, base); err != nil {
+							return err
+						}
+					}
+
+				}
+			}
+		}
+
+		return nil
+	}(); err != nil {
+		return err
+	}
+
+	// Prese shortcodes.
+	for _, v := range s.treeShortcodes.All() {
+		for _, vv := range v {
+			for _, vvv := range vv {
+				if vvv.state == processingStateTransformed {
+					continue
+				}
+				if err := s.tns.parseTemplate(vvv); err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// prepareTemplates prepares all templates for execution.
+func (s *TemplateStore) prepareTemplates() error {
+	for t := range s.templates() {
+		if t.Category == CategoryBaseof {
+			continue
+		}
+		if _, err := t.Prepare(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// TemplateDescriptorFromPath returns a template descriptor from the given path.
+// This is currently used in partial lookups only.
+func (s *TemplateStore) TemplateDescriptorFromPath(pth string) (string, TemplateDescriptor) {
+	var (
+		mt media.Type
+		of output.Format
+	)
+
+	// Common cases.
+	dotCount := strings.Count(pth, ".")
+	if dotCount <= 1 {
+		if dotCount == 0 {
+			// Asume HTML.
+			of, mt = s.resolveOutputFormatAndOrMediaType("html", "")
+		} else {
+			pth = strings.TrimPrefix(pth, "/")
+			ext := path.Ext(pth)
+			pth = strings.TrimSuffix(pth, ext)
+			ext = ext[1:]
+			of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
+		}
+	} else {
+		path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
+		pth = path.PathNoIdentifier()
+		of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
+	}
+
+	return pth, TemplateDescriptor{
+		OutputFormat: of.Name,
+		MediaType:    mt.Type,
+		IsPlainText:  of.IsPlainText,
+	}
+}
+
+// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
+// based on the given output format suffix and media type suffix.
+// Either of the suffixes can be empty, and the function will try to find a match
+// based on the other suffix. If both are empty, the function will return zero values.
+func (s *TemplateStore) resolveOutputFormatAndOrMediaType(ofs, mns string) (output.Format, media.Type) {
+	var outputFormat output.Format
+	var mediaType media.Type
+
+	if ofs != "" {
+		if of, found := s.opts.OutputFormats.GetByName(ofs); found {
+			outputFormat = of
+			mediaType = of.MediaType
+		}
+	}
+
+	if mns != "" && mediaType.IsZero() {
+		if of, found := s.opts.OutputFormats.GetBySuffix(mns); found {
+			outputFormat = of
+			mediaType = of.MediaType
+		} else {
+			if mt, _, found := s.opts.MediaTypes.GetFirstBySuffix(mns); found {
+				mediaType = mt
+				if outputFormat.IsZero() {
+					// For e.g. index.xml we will in the default confg now have the application/rss+xml  media type.
+					// Try a last time to find the output format using the SubType as the name.
+					// As to template resolution, this value is currently only used to
+					// decide if this is a text or HTML template.
+					outputFormat, _ = s.opts.OutputFormats.GetByName(mt.SubType)
+				}
+			}
+		}
+	}
+
+	return outputFormat, mediaType
+}
+
+func (s *TemplateStore) templates() iter.Seq[*TemplInfo] {
+	return func(yield func(*TemplInfo) bool) {
+		for _, v := range s.treeMain.All() {
+			for _, vv := range v {
+				if !vv.NoBaseOf {
+					for vvv := range vv.BaseVariantsSeq() {
+						if !yield(vvv.Template) {
+							return
+						}
+					}
+				} else {
+					if !yield(vv) {
+						return
+					}
+				}
+			}
+		}
+		for _, v := range s.treeShortcodes.All() {
+			for _, vv := range v {
+				for _, vvv := range vv {
+					if !yield(vvv) {
+						return
+					}
+				}
+			}
+		}
+	}
+}
+
+func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, string, Category, TemplateDescriptor) {
+	k1 := p.Dir()
+	k2 := ""
+
+	outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(p.OutputFormat(), p.Ext())
+	nameNoIdentifier := p.NameNoIdentifier()
+
+	var layout string
+	unknownids := p.IdentifiersUnknown()
+	if p.Type() == paths.TypeShortcode {
+		if len(unknownids) > 1 {
+			// The name is the last identifier.
+			layout = unknownids[len(unknownids)-2]
+		}
+	} else if len(unknownids) > 0 {
+		// Pick the last, closest to the base name.
+		layout = unknownids[len(unknownids)-1]
+	}
+
+	d := TemplateDescriptor{
+		Lang:         p.Lang(),
+		OutputFormat: p.OutputFormat(),
+		MediaType:    mediaType.Type,
+		Kind:         p.Kind(),
+		Layout:       layout,
+		IsPlainText:  outputFormat.IsPlainText,
+	}
+
+	d.normalizeFromFile()
+
+	section := p.Section()
+
+	var category Category
+	switch p.Type() {
+	case paths.TypeShortcode:
+		category = CategoryShortcode
+	case paths.TypePartial:
+		category = CategoryPartial
+	case paths.TypeMarkup:
+		category = CategoryMarkup
+	}
+
+	if category == 0 {
+		if nameNoIdentifier == baseNameBaseof {
+			category = CategoryBaseof
+		} else {
+			switch section {
+			case "_hugo":
+				category = CategoryHugo
+			case "_server":
+				category = CategoryServer
+			default:
+				category = CategoryLayout
+			}
+		}
+	}
+
+	if category == CategoryPartial {
+		d.Layout = ""
+		k1 = p.PathNoIdentifier()
+	}
+
+	if category == CategoryShortcode {
+		k1 = p.PathNoIdentifier()
+		parts := strings.Split(k1, "/"+containerShortcodes+"/")
+		k1 = parts[0]
+		if len(parts) > 1 {
+			k2 = parts[1]
+		}
+		k1 = s.key(k1)
+	}
+
+	// Legacy layout for home page.
+	if d.Layout == "index" {
+		if d.Kind == "" {
+			d.Kind = kinds.KindHome
+		}
+		d.Layout = ""
+	}
+
+	if d.Layout == d.Kind {
+		d.Layout = ""
+	}
+
+	k1 = strings.TrimPrefix(k1, "/_default")
+	if k1 == "/" {
+		k1 = ""
+	}
+
+	if category == CategoryMarkup {
+		// We store all template nodes for a given directory on the same level.
+		k1 = strings.TrimSuffix(k1, "/_markup")
+		parts := strings.Split(d.Layout, "-")
+		if len(parts) < 2 {
+			panic("markup template must have at least 2 parts")
+		}
+		// Either 2 or 3 parts, e.g. render-codeblock-go.
+		d.Variant1 = parts[1]
+		if len(parts) > 2 {
+			d.Variant2 = parts[2]
+		}
+		d.Layout = "" // This allows using page layout as part of the key for lookups.
+	}
+
+	return k1, k2, category, d
+}
+
+func (s *TemplateStore) transformTemplates() error {
+	lookup := func(name string, in *TemplInfo) *TemplInfo {
+		if in.D.IsPlainText {
+			templ := in.Template.(*texttemplate.Template).Lookup(name)
+			if templ != nil {
+				return &TemplInfo{
+					Template: templ,
+				}
+			}
+		} else {
+			templ := in.Template.(*htmltemplate.Template).Lookup(name)
+			if templ != nil {
+				return &TemplInfo{
+					Template: templ,
+				}
+			}
+		}
+
+		return nil
+	}
+
+	for vv := range s.templates() {
+		if vv.state == processingStateTransformed {
+			continue
+		}
+		vv.state = processingStateTransformed
+		if vv.Category == CategoryBaseof {
+			continue
+		}
+		if !vv.NoBaseOf {
+			for vvv := range vv.BaseVariantsSeq() {
+				tctx, err := applyTemplateTransformers(vvv.Template, lookup)
+				if err != nil {
+					return err
+				}
+
+				for name, node := range tctx.deferNodes {
+					if err := s.addDeferredTemplate(vvv.Overlay, name, node); err != nil {
+						return err
+					}
+				}
+			}
+		} else {
+			tctx, err := applyTemplateTransformers(vv, lookup)
+			if err != nil {
+				return err
+			}
+
+			for name, node := range tctx.deferNodes {
+				if err := s.addDeferredTemplate(vv, name, node); err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func (s *TemplateStore) init() error {
+	// Before Hugo 0.146 we had a very elaborate template lookup system, especially for
+	// terms and taxonomies. This is a way of preserving backwards compatibility
+	// by mapping old paths into the new tree.
+	s.opts.legacyMappingTaxonomy = make(map[string]legacyOrdinalMapping)
+	s.opts.legacyMappingTerm = make(map[string]legacyOrdinalMapping)
+	s.opts.legacyMappingSection = make(map[string]legacyOrdinalMapping)
+
+	// Placeholders.
+	const singular = "SINGULAR"
+	const plural = "PLURAL"
+
+	replaceTokens := func(s, singularv, pluralv string) string {
+		s = strings.Replace(s, singular, singularv, -1)
+		s = strings.Replace(s, plural, pluralv, -1)
+		return s
+	}
+
+	hasSingularOrPlural := func(s string) bool {
+		return strings.Contains(s, singular) || strings.Contains(s, plural)
+	}
+
+	expand := func(v layoutLegacyMapping) []layoutLegacyMapping {
+		var result []layoutLegacyMapping
+
+		if hasSingularOrPlural(v.sourcePath) || hasSingularOrPlural(v.target.targetPath) {
+			for s, p := range s.opts.TaxonomySingularPlural {
+				target := v.target
+				target.targetPath = replaceTokens(target.targetPath, s, p)
+				vv := replaceTokens(v.sourcePath, s, p)
+				result = append(result, layoutLegacyMapping{sourcePath: vv, target: target})
+			}
+		} else {
+			result = append(result, v)
+		}
+		return result
+	}
+
+	expandSections := func(v layoutLegacyMapping) []layoutLegacyMapping {
+		var result []layoutLegacyMapping
+		result = append(result, v)
+		baseofVariant := v
+		baseofVariant.sourcePath += "-" + baseNameBaseof
+		baseofVariant.target.targetCategory = CategoryBaseof
+		result = append(result, baseofVariant)
+		return result
+	}
+
+	var terms []layoutLegacyMapping
+	for _, v := range legacyTermMappings {
+		terms = append(terms, expand(v)...)
+	}
+	var taxonomies []layoutLegacyMapping
+	for _, v := range legacyTaxonomyMappings {
+		taxonomies = append(taxonomies, expand(v)...)
+	}
+	var sections []layoutLegacyMapping
+	for _, v := range legacySectionMappings {
+		sections = append(sections, expandSections(v)...)
+	}
+
+	for i, m := range terms {
+		s.opts.legacyMappingTerm[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+	}
+	for i, m := range taxonomies {
+		s.opts.legacyMappingTaxonomy[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+	}
+	for i, m := range sections {
+		s.opts.legacyMappingSection[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+	}
+
+	return nil
+}
+
+type TemplateStoreProvider interface {
+	GetTemplateStore() *TemplateStore
+}
+
+type TextTemplatHandler interface {
+	ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error
+	TextLookup(name string) *TemplInfo
+	TextParse(name, tpl string) (*TemplInfo, error)
+}
+
+type bestMatch struct {
+	templ *TemplInfo
+	desc  TemplateDescriptor
+	w     weight
+	key   string
+
+	// settings.
+	defaultOutputformat string
+}
+
+func (best *bestMatch) reset() {
+	best.templ = nil
+	best.w = weight{}
+	best.desc = TemplateDescriptor{}
+	best.key = ""
+}
+
+func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
+	if best.templ == nil {
+		// Anything is better than nothing.
+		return true
+	}
+	if w.w1 <= 0 {
+		if best.w.w1 <= 0 {
+			return ti.PathInfo.Path() < best.templ.PathInfo.Path()
+		}
+		return false
+	}
+
+	if best.w.w1 > 0 {
+		currentBestIsEmbedded := best.templ.SubCategory == SubCategoryEmbedded
+		if currentBestIsEmbedded {
+			if ti.SubCategory != SubCategoryEmbedded {
+				return true
+			}
+		} else {
+			if ti.SubCategory == SubCategoryEmbedded {
+				// Prefer user provided template.
+				return false
+			}
+		}
+	}
+
+	if w.distance < best.w.distance {
+		if w.w2 < best.w.w2 {
+			return false
+		}
+		if w.w3 < best.w.w3 {
+			return false
+		}
+	} else {
+		if w.w1 < best.w.w1 {
+			return false
+		}
+	}
+
+	if w.isEqualWeights(best.w) {
+		// Tie breakers.
+		if w.distance < best.w.distance {
+			return true
+		}
+
+		if ti.D.Layout != "" && best.desc.Layout != "" {
+			return ti.D.Layout != layoutAll
+		}
+
+		return w.distance < best.w.distance || ti.PathInfo.Path() < best.templ.PathInfo.Path()
+	}
+
+	return true
+}
+
+func (best *bestMatch) updateValues(w weight, key string, k TemplateDescriptor, vv *TemplInfo) {
+	best.w = w
+	best.templ = vv
+	best.desc = k
+	best.key = key
+}
+
+type byPath []*TemplInfo
+
+func (a byPath) Len() int { return len(a) }
+func (a byPath) Less(i, j int) bool {
+	return a[i].PathInfo.Path() < a[j].PathInfo.Path()
+}
+
+func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+type keyTemplateInfo struct {
+	Key  string
+	Info *TemplInfo
+}
+
+type nodeKey struct {
+	c Category
+	d TemplateDescriptor
+}
+
+type processingState int
+
+// the parts of a template store that's set per site.
+type storeSite struct {
+	opts       SiteOptions
+	execHelper *templateExecHelper
+	executer   texttemplate.Executer
+}
+
+type weight struct {
+	w1       int
+	w2       int
+	w3       int
+	distance int
+}
+
+func (w weight) isEqualWeights(other weight) bool {
+	return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3
+}
+
+func isLayoutCustom(s string) bool {
+	if s == "" || isLayoutStandard(s) {
+		return false
+	}
+	return true
+}
+
+func isLayoutStandard(s string) bool {
+	switch s {
+	case layoutAll, layoutList, layoutSingle:
+		return true
+	default:
+		return false
+	}
+}
+
+func configureSiteStorage(opts SiteOptions, watching bool) *storeSite {
+	funcsv := make(map[string]reflect.Value)
+
+	for k, v := range opts.TemplateFuncs {
+		vv := reflect.ValueOf(v)
+		funcsv[k] = vv
+	}
+
+	// Duplicate Go's internal funcs here for faster lookups.
+	for k, v := range htmltemplate.GoFuncs {
+		if _, exists := funcsv[k]; !exists {
+			vv, ok := v.(reflect.Value)
+			if !ok {
+				vv = reflect.ValueOf(v)
+			}
+			funcsv[k] = vv
+		}
+	}
+
+	for k, v := range texttemplate.GoFuncs {
+		if _, exists := funcsv[k]; !exists {
+			funcsv[k] = v
+		}
+	}
+
+	s := &storeSite{
+		opts: opts,
+		execHelper: &templateExecHelper{
+			watching:   watching,
+			funcs:      funcsv,
+			site:       reflect.ValueOf(opts.Site),
+			siteParams: reflect.ValueOf(opts.Site.Params()),
+		},
+	}
+
+	s.executer = texttemplate.NewExecuter(s.execHelper)
+
+	return s
+}
diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go
new file mode 100644
index 00000000000..c109959ce59
--- /dev/null
+++ b/tpl/tplimpl/templatestore_integration_test.go
@@ -0,0 +1,842 @@
+package tplimpl_test
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/hugolib"
+	"github.com/gohugoio/hugo/resources/kinds"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
+)
+
+// Old as in before Hugo v0.146.0.
+func TestLayoutsOldSetup(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/index.html --
+Home.
+{{ template "_internal/twitter_cards.html" . }}
+-- layouts/_default/single.html --
+Single.
+-- layouts/_default/single.nn.html --
+Single NN.
+-- layouts/_default/list.html --
+List HTML.
+-- layouts/docs/list-baseof.html --
+Docs Baseof List HTML.
+{{ block "main" . }}Docs Baseof List HTML main block.{{ end }}
+-- layouts/docs/list.section.html --
+{{ define "main" }}
+Docs List HTML.
+{{ end }}
+-- layouts/_default/list.json --
+List JSON.
+-- layouts/_default/list.rss.xml --
+List RSS.
+-- layouts/_default/list.nn.rss.xml --
+List NN RSS.
+-- layouts/_default/baseof.html --
+Base.
+-- layouts/partials/mypartial.html --
+Partial.
+-- layouts/shortcodes/myshortcode.html --
+Shortcode.
+-- content/docs/p1.md --
+---
+title: "P1"
+---
+
+	`
+
+	b := hugolib.Test(t, files)
+
+	//	b.DebugPrint("", tplimpl.CategoryBaseof)
+
+	b.AssertFileContent("public/en/docs/index.html", "Docs Baseof List HTML.\n\nDocs List HTML.")
+}
+
+func TestLayoutsOldSetupBaseofPrefix(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/_default/layout1-baseof.html --
+Baseof layout1. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout2-baseof.html --
+Baseof layout2. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout1.html --
+{{ define "main" }}Layout1. {{ .Title }}{{ end }}
+-- layouts/_default/layout2.html --
+{{ define "main" }}Layout2. {{ .Title }}{{ end }}
+-- content/p1.md --
+---
+title: "P1"
+layout: "layout1"
+---
+-- content/p2.md --
+---
+title: "P2"
+layout: "layout2"
+---
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/p1/index.html", "Baseof layout1. Layout1. P1")
+	b.AssertFileContent("public/p2/index.html", "Baseof layout2. Layout2. P2")
+}
+
+func TestLayoutsOldSetupTaxonomyAndTerm(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+[taxonomies]
+cat = 'cats'
+dog = 'dogs'
+# Templates for term taxonomy, old setup.
+-- layouts/dogs/terms.html --
+Dogs Terms. Most specific taxonomy template.
+-- layouts/taxonomy/terms.html --
+Taxonomy Terms. Down the list.
+# Templates for term term, old setup.
+-- layouts/dogs/term.html --
+Dogs Term. Most specific term template.
+-- layouts/term/term.html --
+Term Term. Down the list.
+-- layouts/dogs/max/list.html --
+max: {{ .Title }}
+-- layouts/_default/list.html --
+Default list.
+-- layouts/_default/single.html --
+Default single.
+-- content/p1.md --
+---
+title: "P1"
+dogs: ["luna", "daisy", "max"]
+---
+
+`
+	b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+	b.AssertLogContains("! WARN")
+
+	b.AssertFileContent("public/dogs/index.html", "Dogs Terms. Most specific taxonomy template.")
+	b.AssertFileContent("public/dogs/luna/index.html", "Dogs Term. Most specific term template.")
+	b.AssertFileContent("public/dogs/max/index.html", "max: Max") // layouts/dogs/max/list.html wins over layouts/term/term.html because of distance.
+}
+
+func TestLayoutsOldSetupCustomRSS(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "page"]
+[outputs]
+home = ["rss"]
+-- layouts/_default/list.rss.xml --
+List RSS.
+`
+	b := hugolib.Test(t, files)
+	b.AssertFileContent("public/index.xml", "List RSS.")
+}
+
+var newSetupTestSites = `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+[languages.fr]
+title = "Titre en français"
+weight = 3
+
+[outputs]
+home     = ["html", "rss", "redir"]
+
+[outputFormats]
+[outputFormats.redir]
+mediatype   = "text/plain"
+baseName    = "_redirects"
+isPlainText = true
+-- layouts/404.html --
+{{ define "main" }}
+404.
+{{ end }}
+-- layouts/home.html --
+{{ define "main" }}
+Home: {{ .Title }}|{{ .Content }}|
+Inline Partial: {{ partial "my-inline-partial.html" . }}
+{{ end }}
+{{ define "hero" }}
+Home hero.
+{{ end }}
+{{ define "partials/my-inline-partial.html" }}
+{{ $value := 32 }}
+{{ return $value }}
+{{ end }}
+-- layouts/index.redir --
+Redir.
+-- layouts/single.html --
+{{ define "main" }}
+Single needs base.
+{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+Single sub path.
+{{ end }}
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.
+-- layouts/_markup/render-blockquote.html --
+Render blockquote.
+-- layouts/_markup/render-codeblock-go.html --
+ Render codeblock go.
+-- layouts/_markup/render-link.html --
+Link: {{ .Destination | safeURL }}
+-- layouts/foo/baseof.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/foo/bar/baseof.page.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/list.html --
+{{ define "main" }}
+List needs base.
+{{ end }}
+-- layouts/section.html --
+Section.
+-- layouts/mysectionlayout.section.fr.amp.html --
+Section with layout.
+-- layouts/baseof.html --
+Base.{{ block "main" . }}{{ end }}
+Hero:{{ block "hero" . }}{{ end }}:
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer Block.
+{{ end }}
+-- layouts/baseof.fr.html --
+Base fr.{{ block "main" . }}{{ end }}
+-- layouts/baseof.term.html --
+Base term.
+-- layouts/baseof.section.fr.amp.html --
+Base with identifiers.{{ block "main" . }}{{ end }}
+-- layouts/partials/mypartial.html --
+Partial. {{ partial "_inline/my-inline-partial-in-partial-with-no-ext" . }}
+{{ define "partials/_inline/my-inline-partial-in-partial-with-no-ext" }}
+Partial in partial.
+{{ end }}
+-- layouts/partials/returnfoo.html --
+{{ $v := "foo" }}
+{{ return $v }}
+-- layouts/shortcodes/myshortcode.html --
+Shortcode. {{ partial "mypartial.html" . }}|return:{{ partial "returnfoo.html" . }}|
+-- content/_index.md --
+---
+title: Home sweet home!
+---
+
+{{< myshortcode >}}
+
+> My blockquote.
+
+
+Markdown link: [Foo](/foo)
+-- content/p1.md --
+---
+title: "P1"
+---
+-- content/foo/bar/index.md --
+---
+title: "Foo Bar"
+---
+
+{{< myshortcode >}}
+
+-- content/single-list.md --
+---
+title: "Single List"
+layout: "list"
+---
+
+`
+
+func TestLayoutsType(t *testing.T) {
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/list.html --
+List.
+-- layouts/mysection/single.html --
+mysection/single|{{ .Title }}
+-- layouts/mytype/single.html --
+mytype/single|{{ .Title }}
+-- content/mysection/_index.md --
+-- content/mysection/mysubsection/_index.md --
+-- content/mysection/mysubsection/p1.md --
+---
+title: "P1"
+---
+-- content/mysection/mysubsection/p2.md --
+---
+title: "P2"
+type: "mytype"
+---
+
+`
+
+	b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+	b.AssertLogContains("! WARN")
+
+	b.AssertFileContent("public/mysection/mysubsection/p1/index.html", "mysection/single|P1")
+	b.AssertFileContent("public/mysection/mysubsection/p2/index.html", "mytype/single|P2")
+}
+
+// New, as in from Hugo v0.146.0.
+func TestLayoutsNewSetup(t *testing.T) {
+	const numIterations = 1
+	for range numIterations {
+
+		b := hugolib.Test(t, newSetupTestSites, hugolib.TestOptWarn())
+
+		b.AssertLogContains("! WARN")
+
+		b.AssertFileContent("public/en/index.html",
+			"Base.\nHome: Home sweet home!|",
+			"|Shortcode.\n|",
+			"<p>Markdown link: Link: /foo</p>",
+			"|return:foo|",
+			"Defer Block.",
+			"Home hero.",
+			"Render blockquote.",
+		)
+
+		b.AssertFileContent("public/en/p1/index.html", "Base.\nSingle needs base.\n\nHero::\n\nDefer Block.")
+		b.AssertFileContent("public/en/404.html", "404.")
+		b.AssertFileContent("public/nn/404.html", "404.")
+		b.AssertFileContent("public/fr/404.html", "404.")
+
+	}
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyShortcode(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/shortcodes/myshortcode.html --
+Myshortcode: Count: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+{{< myshortcode >}}
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/p1/index.html", "Single: P1|Myshortcode: Count: 1|")
+	b.AssertFileContent("public/index.xml", "Myshortcode: Count: 1")
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyRenderHook(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-link.html --
+Render Link: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+Link: [Foo](/foo)
+`
+
+	for range 2 {
+		b := hugolib.Test(t, files)
+		b.AssertFileContent("public/index.xml", "Link: Render Link: 1|")
+		b.AssertFileContent("public/p1/index.html", "Single: P1|<p>Link: Render Link: 1|<")
+	}
+}
+
+func TestRenderCodeblockSpecificity(t *testing.T) {
+	files := `
+-- hugo.toml --
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.|{{ .Inner }}|
+-- layouts/_markup/render-codeblock-go.html --
+Render codeblock go.|{{ .Inner }}|
+-- layouts/single.html --
+{{ .Title }}|{{ .Content }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+§§§
+Basic
+§§§
+
+§§§ go
+Go
+§§§
+
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/p1/index.html", "P1|Render codeblock.|Basic|Render codeblock go.|Go|")
+}
+
+func TestPrintUnusedTemplates(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+printUnusedTemplates=true
+-- content/p1.md --
+---
+title: "P1"
+---
+{{< usedshortcode >}}
+-- layouts/baseof.html --
+{{ block "main" . }}{{ end }}
+-- layouts/baseof.json --
+{{ block "main" . }}{{ end }}
+-- layouts/index.html --
+{{ define "main" }}FOO{{ end }}
+-- layouts/_default/single.json --
+-- layouts/_default/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/post/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/_partials/usedpartial.html --
+-- layouts/_partials/unusedpartial.html --
+-- layouts/_shortcodes/usedshortcode.html --
+{{ partial "usedpartial.html" }}
+-- layouts/shortcodes/unusedshortcode.html --
+
+	`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   true,
+		},
+	)
+	b.Build()
+
+	unused := b.H.GetTemplateStore().UnusedTemplates()
+	var names []string
+	for _, tmpl := range unused {
+		if fi := tmpl.Fi; fi != nil {
+			names = append(names, fi.Meta().PathInfo.PathNoLeadingSlash())
+		}
+	}
+	b.Assert(len(unused), qt.Equals, 5, qt.Commentf("%#v", names))
+	b.Assert(names, qt.DeepEquals, []string{"_partials/unusedpartial.html", "shortcodes/unusedshortcode.html", "baseof.json", "post/single.html", "_default/single.json"})
+}
+
+func TestCreateManyTemplateStores(t *testing.T) {
+	t.Parallel()
+	b := hugolib.Test(t, newSetupTestSites)
+	store := b.H.TemplateStore
+
+	for range 70 {
+		newStore, err := store.NewFromOpts()
+		b.Assert(err, qt.IsNil)
+		b.Assert(newStore, qt.Not(qt.IsNil))
+	}
+}
+
+func BenchmarkLookupPagesLayout(b *testing.B) {
+	files := `
+-- hugo.toml --
+-- layouts/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+-- layouts/baseof.html --
+baseof: {{ block "main" . }}{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+
+`
+	bb := hugolib.Test(b, files)
+	store := bb.H.TemplateStore
+
+	b.ResetTimer()
+	b.Run("Single root", func(b *testing.B) {
+		q := tplimpl.TemplateQuery{
+			Path:     "/baz",
+			Category: tplimpl.CategoryLayout,
+			Desc:     tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
+		}
+		for i := 0; i < b.N; i++ {
+			store.LookupPagesLayout(q)
+		}
+	})
+
+	b.Run("Single sub folder", func(b *testing.B) {
+		q := tplimpl.TemplateQuery{
+			Path:     "/foo/bar",
+			Category: tplimpl.CategoryLayout,
+			Desc:     tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"},
+		}
+		for i := 0; i < b.N; i++ {
+			store.LookupPagesLayout(q)
+		}
+	})
+}
+
+func BenchmarkNewTemplateStore(b *testing.B) {
+	bb := hugolib.Test(b, newSetupTestSites)
+	store := bb.H.TemplateStore
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		newStore, err := store.NewFromOpts()
+		if err != nil {
+			b.Fatal(err)
+		}
+		if newStore == nil {
+			b.Fatal("newStore is nil")
+		}
+	}
+}
+
+func TestLayoutsLookupVariants(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[outputs]
+home = ["html", "rss"]
+page = ["html", "rss",  "amp"]
+section = ["html", "rss"]
+
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/list.xml --
+layouts/list.xml
+-- layouts/_shortcodes/myshortcode.html --
+layouts/shortcodes/myshortcode.html
+-- layouts/foo/bar/_shortcodes/myshortcode.html --
+layouts/foo/bar/_shortcodes/myshortcode.html
+-- layouts/_markup/render-codeblock.html --
+layouts/_markup/render-codeblock.html|{{ .Type }}|
+-- layouts/_markup/render-codeblock-go.html --
+layouts/_markup/render-codeblock-go.html|{{ .Type }}|
+-- layouts/single.xml --
+layouts/single.xml
+-- layouts/single.rss.xml --
+layouts/single.rss.xml
+-- layouts/single.nn.rss.xml --
+layouts/single.nn.rss.xml
+-- layouts/list.html --
+layouts/list.html
+-- layouts/single.html --
+layouts/single.html
+{{ .Content }}
+-- layouts/mylayout.html --
+layouts/mylayout.html
+-- layouts/mylayout.nn.html --
+layouts/mylayout.nn.html
+-- layouts/foo/single.rss.xml --
+layouts/foo/single.rss.xml
+-- layouts/foo/single.amp.html --
+layouts/foo/single.amp.html
+-- layouts/foo/bar/page.html --
+layouts/foo/bar/page.html
+-- layouts/foo/bar/baz/single.html --
+layouts/foo/bar/baz/single.html
+{{ .Content }}
+-- layouts/qux/mylayout.html --
+layouts/qux/mylayout.html
+-- layouts/qux/single.xml --
+layouts/qux/single.xml
+-- layouts/qux/mylayout.section.html --
+layouts/qux/mylayout.section.html
+-- content/p.md --
+---
+---
+§§§
+code
+§§§
+
+§§§ go
+code
+§§§
+
+{{< myshortcode >}}
+-- content/foo/p.md --
+-- content/foo/p.nn.md --
+-- content/foo/bar/p.md --
+-- content/foo/bar/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/foo/bar/_index.md --
+-- content/foo/bar/baz/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/qux/p.md --
+-- content/qux/_index.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/p.md --
+-- content/qux/quux/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/withmylayout.nn.md --
+---
+layout: mylayout
+---
+
+
+`
+
+	b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+	// s := b.H.Sites[0].TemplateStore
+	// s.PrintDebug("", tplimpl.CategoryLayout, os.Stdout)
+
+	b.AssertLogContains("! WARN")
+
+	// Single pages.
+	// output format: html.
+	b.AssertFileContent("public/en/p/index.html", "layouts/single.html",
+		"layouts/_markup/render-codeblock.html|",
+		"layouts/_markup/render-codeblock-go.html|go|",
+		"layouts/shortcodes/myshortcode.html",
+	)
+	b.AssertFileContent("public/en/foo/p/index.html", "layouts/single.html")
+	b.AssertFileContent("public/en/foo/bar/p/index.html", "layouts/foo/bar/page.html")
+	b.AssertFileContent("public/en/foo/bar/withmylayout/index.html", "layouts/mylayout.html")
+	b.AssertFileContent("public/en/foo/bar/baz/p/index.html", "layouts/foo/bar/baz/single.html", "layouts/foo/bar/_shortcodes/myshortcode.html")
+	b.AssertFileContent("public/en/qux/quux/withmylayout/index.html", "layouts/qux/mylayout.html")
+	// output format: amp.
+	b.AssertFileContent("public/en/amp/p/index.html", "layouts/single.html")
+	b.AssertFileContent("public/en/amp/foo/p/index.html", "layouts/foo/single.amp.html")
+	// output format: rss.
+	b.AssertFileContent("public/en/p/index.xml", "layouts/single.rss.xml")
+	b.AssertFileContent("public/en/foo/p/index.xml", "layouts/foo/single.rss.xml")
+	b.AssertFileContent("public/nn/foo/p/index.xml", "layouts/single.nn.rss.xml")
+
+	// Note: There is qux/single.xml that's closer, but the one in the root is used becaulse of the output format match.
+	b.AssertFileContent("public/en/qux/p/index.xml", "layouts/single.rss.xml")
+
+	// Note.
+	b.AssertFileContent("public/nn/qux/quux/withmylayout/index.html", "layouts/mylayout.nn.html")
+
+	// Section pages.
+	// output format: html.
+	b.AssertFileContent("public/en/foo/index.html", "layouts/list.html")
+	b.AssertFileContent("public/en/qux/index.html", "layouts/qux/mylayout.section.html")
+	// output format: rss.
+	b.AssertFileContent("public/en/foo/index.xml", "layouts/list.xml")
+}
+
+func TestLookupShortcodeDepth(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.html --
+layouts/_shortcodes/myshortcode.html
+-- layouts/foo/_shortcodes/myshortcode.html --
+layouts/foo/_shortcodes/myshortcode.html
+-- layouts/single.html --
+{{ .Content }}|
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.html")
+	b.AssertFileContent("public/foo/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+	b.AssertFileContent("public/foo/bar/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+}
+
+func TestLookupShortcodeLayout(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.single.html --
+layouts/_shortcodes/myshortcode.single.html
+-- layouts/_shortcodes/myshortcode.list.html --
+layouts/_shortcodes/myshortcode.list.html
+-- layouts/single.html --
+{{ .Content }}|
+-- layouts/list.html --
+{{ .Content }}|
+-- content/_index.md --
+---
+---
+{{< myshortcode >}}
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.single.html")
+	b.AssertFileContent("public/index.html", "layouts/_shortcodes/myshortcode.list.html")
+}
+
+func TestLayoutAll(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/single.html --
+Single.
+-- layouts/all.html --
+All.
+-- content/p1.md --
+
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/p1/index.html", "Single.")
+	b.AssertFileContent("public/index.html", "All.")
+}
+
+func TestLayoutAllNested(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ['rss','sitemap','taxonomy','term']
+-- content/s1/p1.md --
+---
+title: p1
+---
+-- content/s2/p2.md --
+---
+title: p2
+---
+-- layouts/single.html --
+layouts/single.html
+-- layouts/list.html --
+layouts/list.html
+-- layouts/s1/all.html --
+layouts/s1/all.html
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "layouts/list.html")
+	b.AssertFileContent("public/s1/index.html", "layouts/s1/all.html")
+	b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/all.html")
+	b.AssertFileContent("public/s2/index.html", "layouts/list.html")
+	b.AssertFileContent("public/s2/p2/index.html", "layouts/single.html")
+}
+
+func TestPartialHTML(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/all.html --
+<html>
+<head>
+{{ partial "css.html" .}}
+</head>
+</html>
+-- layouts/partials/css.html --
+<link rel="stylesheet" href="/css/style.css">
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "<link rel=\"stylesheet\" href=\"/css/style.css\">")
+}
+
+// Issue #13515
+func TestPrintPathWarningOnDotRemoval(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+printPathWarnings = true
+-- content/v0.124.0.md --
+-- content/v0.123.0.md --
+-- layouts/all.html --
+All.
+-- layouts/_default/single.html --
+{{ .Title }}|
+`
+
+	b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+	b.AssertLogContains("Duplicate content path")
+}
diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go
index 9b30466e8c7..8b80d5b6072 100644
--- a/tpl/tplimpl/tplimpl_integration_test.go
+++ b/tpl/tplimpl/tplimpl_integration_test.go
@@ -1,66 +1,13 @@
 package tplimpl_test
 
 import (
-	"path/filepath"
 	"strings"
 	"testing"
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/hugolib"
-	"github.com/gohugoio/hugo/tpl"
 )
 
-func TestPrintUnusedTemplates(t *testing.T) {
-	t.Parallel()
-
-	files := `
--- config.toml --
-baseURL = 'http://example.com/'
-printUnusedTemplates=true
--- content/p1.md --
----
-title: "P1"
----
-{{< usedshortcode >}}
--- layouts/baseof.html --
-{{ block "main" . }}{{ end }}
--- layouts/baseof.json --
-{{ block "main" . }}{{ end }}
--- layouts/index.html --
-{{ define "main" }}FOO{{ end }}
--- layouts/_default/single.json --
--- layouts/_default/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/post/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/partials/usedpartial.html --
--- layouts/partials/unusedpartial.html --
--- layouts/shortcodes/usedshortcode.html --
-{{ partial "usedpartial.html" }}
--- layouts/shortcodes/unusedshortcode.html --
-
-	`
-
-	b := hugolib.NewIntegrationTestBuilder(
-		hugolib.IntegrationTestConfig{
-			T:           t,
-			TxtarString: files,
-			NeedsOsFS:   true,
-		},
-	)
-	b.Build()
-
-	unused := b.H.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
-
-	var names []string
-	for _, tmpl := range unused {
-		names = append(names, tmpl.Name())
-	}
-
-	b.Assert(names, qt.DeepEquals, []string{"_default/single.json", "baseof.json", "partials/unusedpartial.html", "post/single.html", "shortcodes/unusedshortcode.html"})
-	b.Assert(unused[0].Filename(), qt.Equals, filepath.Join(b.Cfg.WorkingDir, "layouts/_default/single.json"))
-}
-
 // Verify that the new keywords in Go 1.18 is available.
 func TestGo18Constructs(t *testing.T) {
 	t.Parallel()
@@ -627,9 +574,9 @@ Home!
 
 	b := hugolib.TestRunning(t, files)
 	b.AssertFileContent("public/index.html", "Home!")
-	b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof!").Build()
+	b.EditFileReplaceAll("layouts/_default/baseof.html", "baseof", "Baseof!").Build()
 	b.BuildPartial("/")
-	b.AssertFileContent("public/index.html", "Baseof!!")
+	b.AssertFileContent("public/index.html", "Baseof!")
 	b.BuildPartial("/mybundle1/")
-	b.AssertFileContent("public/mybundle1/index.html", "Baseof!!")
+	b.AssertFileContent("public/mybundle1/index.html", "Baseof!")
 }