Repository: gohugoio/hugo (language: go)

Issue:
Upgrade to Go 1.23
[<!-- Describe this new feature. Think about if it really belongs in the Hugo core module; you may want to discuss it on https://discourse.gohugo.io/ first.  -->](https://tip.golang.org/doc/go1.23)


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index beee3b5366d..0ff9559365e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,7 +4,7 @@ parameters:
 defaults: &defaults
   resource_class: large
   docker:
-      - image: bepsays/ci-hugoreleaser:1.22200.20501
+      - image: bepsays/ci-hugoreleaser:1.22300.20000
 environment: &buildenv
       GOMODCACHE: /root/project/gomodcache
 version: 2
@@ -60,7 +60,7 @@ jobs:
     environment:
         <<: [*buildenv]
     docker:
-    - image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20501
+    - image: bepsays/ci-hugoreleaser-linux-arm64:1.22300.20000
     steps:
       - *restore-cache
       - &attach-workspace
diff --git a/go.mod b/go.mod
index f9776279e42..40f90c45f0e 100644
--- a/go.mod
+++ b/go.mod
@@ -170,4 +170,4 @@ require (
 	software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
 )
 
-go 1.21.8
+go 1.22.6
diff --git a/hugofs/fs.go b/hugofs/fs.go
index fc0ea71c666..fab0d388656 100644
--- a/hugofs/fs.go
+++ b/hugofs/fs.go
@@ -147,7 +147,8 @@ func isWrite(flag int) bool {
 // TODO(bep) move this to a more suitable place.
 func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
 	// Safe guard
-	if !strings.Contains(dir, "pkg") {
+	// Note that the base directory changed from pkg to gomod_cache in Go 1.23.
+	if !strings.Contains(dir, "pkg") && !strings.Contains(dir, "gomod") {
 		panic(fmt.Sprint("invalid dir:", dir))
 	}
 
diff --git a/modules/client.go b/modules/client.go
index a6caec23c90..dce40d2db9b 100644
--- a/modules/client.go
+++ b/modules/client.go
@@ -365,18 +365,6 @@ func (c *Client) Get(args ...string) error {
 }
 
 func (c *Client) get(args ...string) error {
-	var hasD bool
-	for _, arg := range args {
-		if arg == "-d" {
-			hasD = true
-			break
-		}
-	}
-	if !hasD {
-		// go get without the -d flag does not make sense to us, as
-		// it will try to build and install go packages.
-		args = append([]string{"-d"}, args...)
-	}
 	if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil {
 		return fmt.Errorf("failed to get %q: %w", args, err)
 	}
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
index 476fe480be6..c4dad322405 100644
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -16,7 +16,7 @@ import (
 )
 
 func main() {
-	// The current is built with 8e1fdea8316d840fd07e9d6e026048e53290948b go1.22.5
+	// The current is built with  6885bad7dd86880be6929c02085e5c7a67ff2887 go1.23.0
 	// TODO(bep) preserve the staticcheck.conf file.
 	fmt.Println("Forking ...")
 	defer fmt.Println("Done ...")
diff --git a/tpl/internal/go_templates/cfg/cfg.go b/tpl/internal/go_templates/cfg/cfg.go
index 2af0ec70786..08d210b7973 100644
--- a/tpl/internal/go_templates/cfg/cfg.go
+++ b/tpl/internal/go_templates/cfg/cfg.go
@@ -36,6 +36,7 @@ const KnownEnv = `
 	GOAMD64
 	GOARCH
 	GOARM
+	GOARM64
 	GOBIN
 	GOCACHE
 	GOCACHEPROG
@@ -57,6 +58,7 @@ const KnownEnv = `
 	GOPPC64
 	GOPRIVATE
 	GOPROXY
+	GORISCV64
 	GOROOT
 	GOSUMDB
 	GOTMPDIR
diff --git a/tpl/internal/go_templates/fmtsort/sort.go b/tpl/internal/go_templates/fmtsort/sort.go
index 278a89bd75d..f51cdc7083a 100644
--- a/tpl/internal/go_templates/fmtsort/sort.go
+++ b/tpl/internal/go_templates/fmtsort/sort.go
@@ -9,25 +9,23 @@
 package fmtsort
 
 import (
+	"cmp"
 	"reflect"
-	"sort"
+	"slices"
 )
 
 // Note: Throughout this package we avoid calling reflect.Value.Interface as
 // it is not always legal to do so and it's easier to avoid the issue than to face it.
 
-// SortedMap represents a map's keys and values. The keys and values are
-// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
-type SortedMap struct {
-	Key   []reflect.Value
-	Value []reflect.Value
-}
+// SortedMap is a slice of KeyValue pairs that simplifies sorting
+// and iterating over map entries.
+//
+// Each KeyValue pair contains a map key and its corresponding value.
+type SortedMap []KeyValue
 
-func (o *SortedMap) Len() int           { return len(o.Key) }
-func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
-func (o *SortedMap) Swap(i, j int) {
-	o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
-	o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
+// KeyValue holds a single key and value pair found in a map.
+type KeyValue struct {
+	Key, Value reflect.Value
 }
 
 // Sort accepts a map and returns a SortedMap that has the same keys and
@@ -48,7 +46,7 @@ func (o *SortedMap) Swap(i, j int) {
 //     Otherwise identical arrays compare by length.
 //   - interface values compare first by reflect.Type describing the concrete type
 //     and then by concrete value as described in the previous rules.
-func Sort(mapValue reflect.Value) *SortedMap {
+func Sort(mapValue reflect.Value) SortedMap {
 	if mapValue.Type().Kind() != reflect.Map {
 		return nil
 	}
@@ -56,18 +54,14 @@ func Sort(mapValue reflect.Value) *SortedMap {
 	// of a concurrent map update. The runtime is responsible for
 	// yelling loudly if that happens. See issue 33275.
 	n := mapValue.Len()
-	key := make([]reflect.Value, 0, n)
-	value := make([]reflect.Value, 0, n)
+	sorted := make(SortedMap, 0, n)
 	iter := mapValue.MapRange()
 	for iter.Next() {
-		key = append(key, iter.Key())
-		value = append(value, iter.Value())
-	}
-	sorted := &SortedMap{
-		Key:   key,
-		Value: value,
+		sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
 	}
-	sort.Stable(sorted)
+	slices.SortStableFunc(sorted, func(a, b KeyValue) int {
+		return compare(a.Key, b.Key)
+	})
 	return sorted
 }
 
@@ -82,43 +76,19 @@ func compare(aVal, bVal reflect.Value) int {
 	}
 	switch aVal.Kind() {
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		a, b := aVal.Int(), bVal.Int()
-		switch {
-		case a < b:
-			return -1
-		case a > b:
-			return 1
-		default:
-			return 0
-		}
+		return cmp.Compare(aVal.Int(), bVal.Int())
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
-		a, b := aVal.Uint(), bVal.Uint()
-		switch {
-		case a < b:
-			return -1
-		case a > b:
-			return 1
-		default:
-			return 0
-		}
+		return cmp.Compare(aVal.Uint(), bVal.Uint())
 	case reflect.String:
-		a, b := aVal.String(), bVal.String()
-		switch {
-		case a < b:
-			return -1
-		case a > b:
-			return 1
-		default:
-			return 0
-		}
+		return cmp.Compare(aVal.String(), bVal.String())
 	case reflect.Float32, reflect.Float64:
-		return floatCompare(aVal.Float(), bVal.Float())
+		return cmp.Compare(aVal.Float(), bVal.Float())
 	case reflect.Complex64, reflect.Complex128:
 		a, b := aVal.Complex(), bVal.Complex()
-		if c := floatCompare(real(a), real(b)); c != 0 {
+		if c := cmp.Compare(real(a), real(b)); c != 0 {
 			return c
 		}
-		return floatCompare(imag(a), imag(b))
+		return cmp.Compare(imag(a), imag(b))
 	case reflect.Bool:
 		a, b := aVal.Bool(), bVal.Bool()
 		switch {
@@ -130,28 +100,12 @@ func compare(aVal, bVal reflect.Value) int {
 			return -1
 		}
 	case reflect.Pointer, reflect.UnsafePointer:
-		a, b := aVal.Pointer(), bVal.Pointer()
-		switch {
-		case a < b:
-			return -1
-		case a > b:
-			return 1
-		default:
-			return 0
-		}
+		return cmp.Compare(aVal.Pointer(), bVal.Pointer())
 	case reflect.Chan:
 		if c, ok := nilCompare(aVal, bVal); ok {
 			return c
 		}
-		ap, bp := aVal.Pointer(), bVal.Pointer()
-		switch {
-		case ap < bp:
-			return -1
-		case ap > bp:
-			return 1
-		default:
-			return 0
-		}
+		return cmp.Compare(aVal.Pointer(), bVal.Pointer())
 	case reflect.Struct:
 		for i := 0; i < aVal.NumField(); i++ {
 			if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
@@ -198,22 +152,3 @@ func nilCompare(aVal, bVal reflect.Value) (int, bool) {
 	}
 	return 0, false
 }
-
-// floatCompare compares two floating-point values. NaNs compare low.
-func floatCompare(a, b float64) int {
-	switch {
-	case isNaN(a):
-		return -1 // No good answer if b is a NaN so don't bother checking.
-	case isNaN(b):
-		return 1
-	case a < b:
-		return -1
-	case a > b:
-		return 1
-	}
-	return 0
-}
-
-func isNaN(a float64) bool {
-	return a != a
-}
diff --git a/tpl/internal/go_templates/htmltemplate/content.go b/tpl/internal/go_templates/htmltemplate/content.go
index 9c61cfac0a3..d19b1ec1274 100644
--- a/tpl/internal/go_templates/htmltemplate/content.go
+++ b/tpl/internal/go_templates/htmltemplate/content.go
@@ -29,7 +29,6 @@ const (
 
 // indirect returns the value, after dereferencing as many times
 // as necessary to reach the base type (or nil).
-// Signature modified by Hugo. TODO(bep) script this.
 func doIndirect(a any) any {
 	if a == nil {
 		return nil
@@ -46,8 +45,8 @@ func doIndirect(a any) any {
 }
 
 var (
-	errorType       = reflect.TypeOf((*error)(nil)).Elem()
-	fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
+	errorType       = reflect.TypeFor[error]()
+	fmtStringerType = reflect.TypeFor[fmt.Stringer]()
 )
 
 // indirectToStringerOrError returns the value, after dereferencing as many times
diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go
index 6be5e0f84e5..7442194f79c 100644
--- a/tpl/internal/go_templates/htmltemplate/doc.go
+++ b/tpl/internal/go_templates/htmltemplate/doc.go
@@ -232,11 +232,9 @@ Least Surprise Property:
 knows that contextual autoescaping happens should be able to look at a {{.}}
 and correctly infer what sanitization happens."
 
-As a consequence of the Least Surprise Property, template actions within an
-ECMAScript 6 template literal are disabled by default.
-Handling string interpolation within these literals is rather complex resulting
-in no clear safe way to support it.
-To re-enable template actions within ECMAScript 6 template literals, use the
-GODEBUG=jstmpllitinterp=1 environment variable.
+Previously, ECMAScript 6 template literal were disabled by default, and could be
+enabled with the GODEBUG=jstmpllitinterp=1 environment variable. Template
+literals are now supported by default, and setting jstmpllitinterp has no
+effect.
 */
 package template
diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go
index cc80d2b64e1..1b56abeb108 100644
--- a/tpl/internal/go_templates/htmltemplate/js.go
+++ b/tpl/internal/go_templates/htmltemplate/js.go
@@ -125,7 +125,7 @@ var regexpPrecederKeywords = map[string]bool{
 	"void":       true,
 }
 
-var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
+var jsonMarshalType = reflect.TypeFor[json.Marshaler]()
 
 // indirectToJSONMarshaler returns the value, after dereferencing as many times
 // as necessary to reach the base type (or nil) or an implementation of json.Marshal.
@@ -172,7 +172,7 @@ func jsValEscaper(args ...any) string {
 	// cyclic data. This may be an unacceptable DoS risk.
 	b, err := json.Marshal(a)
 	if err != nil {
-		// While the standard JSON marshaller does not include user controlled
+		// While the standard JSON marshaler does not include user controlled
 		// information in the error message, if a type has a MarshalJSON method,
 		// the content of the error message is not guaranteed. Since we insert
 		// the error into the template, as part of a comment, we attempt to
@@ -393,7 +393,6 @@ var jsStrNormReplacementTable = []string{
 	'<':  `\u003c`,
 	'>':  `\u003e`,
 }
-
 var jsRegexpReplacementTable = []string{
 	0:    `\u0000`,
 	'\t': `\t`,
diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go
index d7454f101d1..4582ddd5fd1 100644
--- a/tpl/internal/go_templates/htmltemplate/template.go
+++ b/tpl/internal/go_templates/htmltemplate/template.go
@@ -179,7 +179,7 @@ func (t *Template) DefinedTemplates() string {
 // definition of t itself.
 //
 // Templates can be redefined in successive calls to Parse,
-// before the first use of Execute on t or any associated template.
+// before the first use of [Template.Execute] on t or any associated template.
 // A template definition with a body containing only white space and comments
 // is considered empty and will not replace an existing template's body.
 // This allows using Parse to add new named template definitions without
@@ -238,8 +238,8 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error
 
 // Clone returns a duplicate of the template, including all associated
 // templates. The actual representation is not copied, but the name space of
-// associated templates is, so further calls to Parse in the copy will add
-// templates to the copy but not to the original. Clone can be used to prepare
+// associated templates is, so further calls to [Template.Parse] in the copy will add
+// templates to the copy but not to the original. [Template.Clone] can be used to prepare
 // common templates and use them with variant definitions for other templates
 // by adding the variants after the clone is made.
 //
@@ -342,7 +342,7 @@ func (t *Template) Funcs(funcMap FuncMap) *Template {
 }
 
 // Delims sets the action delimiters to the specified strings, to be used in
-// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
+// subsequent calls to [Template.Parse], [ParseFiles], or [ParseGlob]. Nested template
 // definitions will inherit the settings. An empty delimiter stands for the
 // corresponding default: {{ or }}.
 // The return value is the template, so calls can be chained.
@@ -359,7 +359,7 @@ func (t *Template) Lookup(name string) *Template {
 	return t.set[name]
 }
 
-// Must is a helper that wraps a call to a function returning (*Template, error)
+// Must is a helper that wraps a call to a function returning ([*Template], error)
 // and panics if the error is non-nil. It is intended for use in variable initializations
 // such as
 //
@@ -371,10 +371,10 @@ func Must(t *Template, err error) *Template {
 	return t
 }
 
-// ParseFiles creates a new Template and parses the template definitions from
+// ParseFiles creates a new [Template] and parses the template definitions from
 // the named files. The returned template's name will have the (base) name and
 // (parsed) contents of the first file. There must be at least one file.
-// If an error occurs, parsing stops and the returned *Template is nil.
+// If an error occurs, parsing stops and the returned [*Template] is nil.
 //
 // When parsing multiple files with the same name in different directories,
 // the last one mentioned will be the one that results.
@@ -436,12 +436,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
 	return t, nil
 }
 
-// ParseGlob creates a new Template and parses the template definitions from
+// ParseGlob creates a new [Template] and parses the template definitions from
 // the files identified by the pattern. The files are matched according to the
 // semantics of filepath.Match, and the pattern must match at least one file.
 // The returned template will have the (base) name and (parsed) contents of the
 // first file matched by the pattern. ParseGlob is equivalent to calling
-// ParseFiles with the list of files matched by the pattern.
+// [ParseFiles] with the list of files matched by the pattern.
 //
 // When parsing multiple files with the same name in different directories,
 // the last one mentioned will be the one that results.
@@ -485,7 +485,7 @@ func IsTrue(val any) (truth, ok bool) {
 	return template.IsTrue(val)
 }
 
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
+// ParseFS is like [ParseFiles] or [ParseGlob] but reads from the file system fs
 // instead of the host operating system's file system.
 // It accepts a list of glob patterns.
 // (Note that most file names serve as glob patterns matching only themselves.)
@@ -493,7 +493,7 @@ func ParseFS(fs fs.FS, patterns ...string) (*Template, error) {
 	return parseFS(nil, fs, patterns)
 }
 
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fs
 // instead of the host operating system's file system.
 // It accepts a list of glob patterns.
 // (Note that most file names serve as glob patterns matching only themselves.)
diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go
index d5a05f66da4..c430389a345 100644
--- a/tpl/internal/go_templates/htmltemplate/transition.go
+++ b/tpl/internal/go_templates/htmltemplate/transition.go
@@ -414,7 +414,7 @@ func tJSDelimited(c context, s []byte) (context, int) {
 			// If "</script" appears in a regex literal, the '/' should not
 			// close the regex literal, and it will later be escaped to
 			// "\x3C/script" in escapeText.
-			if i > 0 && i+7 <= len(s) && bytes.Compare(bytes.ToLower(s[i-1:i+7]), []byte("</script")) == 0 {
+			if i > 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte("</script")) {
 				i++
 			} else if !inCharset {
 				c.state, c.jsCtx = stateJS, jsCtxDivOp
diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go
index 032784bc3f0..b3ffaabb153 100644
--- a/tpl/internal/go_templates/texttemplate/doc.go
+++ b/tpl/internal/go_templates/texttemplate/doc.go
@@ -144,6 +144,13 @@ data, defined in detail in the corresponding sections that follow.
 		is executed; otherwise, dot is set to the value of the pipeline
 		and T1 is executed.
 
+	{{with pipeline}} T1 {{else with pipeline}} T0 {{end}}
+		To simplify the appearance of with-else chains, the else action
+		of a with may include another with directly; the effect is exactly
+		the same as writing
+			{{with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}}
+
+
 Arguments
 
 An argument is a simple value, denoted by one of the following.
diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go
index 73153c7648d..bd8c82bd705 100644
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -7,13 +7,12 @@ package template
 import (
 	"errors"
 	"fmt"
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 	"io"
 	"reflect"
 	"runtime"
 	"strings"
-
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 )
 
 // maxExecDepth specifies the maximum stack depth of templates within
@@ -95,7 +94,7 @@ type missingValType struct{}
 
 var missingVal = reflect.ValueOf(missingValType{})
 
-var missingValReflectType = reflect.TypeOf(missingValType{})
+var missingValReflectType = reflect.TypeFor[missingValType]()
 
 func isMissing(v reflect.Value) bool {
 	return v.IsValid() && v.Type() == missingValReflectType
@@ -202,8 +201,8 @@ func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error {
 // A template may be executed safely in parallel, although if parallel
 // executions share a Writer the output may be interleaved.
 //
-// If data is a reflect.Value, the template applies to the concrete
-// value that the reflect.Value holds, as in fmt.Print.
+// If data is a [reflect.Value], the template applies to the concrete
+// value that the reflect.Value holds, as in [fmt.Print].
 func (t *Template) Execute(wr io.Writer, data any) error {
 	return t.execute(wr, data)
 }
@@ -229,7 +228,7 @@ func (t *Template) execute(wr io.Writer, data any) (err error) {
 // DefinedTemplates returns a string listing the defined templates,
 // prefixed by the string "; defined templates are: ". If there are none,
 // it returns the empty string. For generating an error message here
-// and in html/template.
+// and in [html/template].
 func (t *Template) DefinedTemplates() string {
 	if t.common == nil {
 		return ""
@@ -409,8 +408,8 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
 			break
 		}
 		om := fmtsort.Sort(val)
-		for i, key := range om.Key {
-			oneIteration(key, om.Value[i])
+		for _, m := range om {
+			oneIteration(m.Key, m.Value)
 		}
 		return
 	case reflect.Chan:
@@ -480,7 +479,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
 		value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
 		// If the object has type interface{}, dig down one level to the thing inside.
 		if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
-			value = reflect.ValueOf(value.Interface()) // lovely!
+			value = value.Elem()
 		}
 	}
 	for _, variable := range pipe.Decl {
@@ -709,9 +708,9 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
 }
 
 var (
-	errorType        = reflect.TypeOf((*error)(nil)).Elem()
-	fmtStringerType  = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
-	reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
+	errorType        = reflect.TypeFor[error]()
+	fmtStringerType  = reflect.TypeFor[fmt.Stringer]()
+	reflectValueType = reflect.TypeFor[reflect.Value]()
 )
 
 // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
@@ -735,9 +734,8 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
 	} else if numIn != typ.NumIn() {
 		s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
 	}
-	if !goodFunc(typ) {
-		// TODO: This could still be a confusing error; maybe goodFunc should provide info.
-		s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
+	if err := goodFunc(name, typ); err != nil {
+		s.errorf("%v", err)
 	}
 
 	unwrap := func(v reflect.Value) reflect.Value {
@@ -801,6 +799,15 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
 		}
 		argv[i] = s.validateType(final, t)
 	}
+
+	// Special case for the "call" builtin.
+	// Insert the name of the callee function as the first argument.
+	if isBuiltin && name == "call" {
+		calleeName := args[0].String()
+		argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
+		fun = reflect.ValueOf(call)
+	}
+
 	v, err := safeCall(fun, argv)
 	// If we have an error that is not nil, stop execution and return that
 	// error to the caller.
diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go
index a949f896fa3..7d63cf8b7bb 100644
--- a/tpl/internal/go_templates/texttemplate/funcs.go
+++ b/tpl/internal/go_templates/texttemplate/funcs.go
@@ -22,14 +22,14 @@ import (
 // return value evaluates to non-nil during execution, execution terminates and
 // Execute returns that error.
 //
-// Errors returned by Execute wrap the underlying error; call errors.As to
+// Errors returned by Execute wrap the underlying error; call [errors.As] to
 // unwrap them.
 //
 // When template execution invokes a function with an argument list, that list
 // must be assignable to the function's parameter types. Functions meant to
 // apply to arguments of arbitrary type can use parameters of type interface{} or
-// of type reflect.Value. Similarly, functions meant to return a result of arbitrary
-// type can return interface{} or reflect.Value.
+// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
+// type can return interface{} or [reflect.Value].
 type FuncMap map[string]any
 
 // builtins returns the FuncMap.
@@ -39,7 +39,7 @@ type FuncMap map[string]any
 func builtins() FuncMap {
 	return FuncMap{
 		"and":      and,
-		"call":     call,
+		"call":     emptyCall,
 		"html":     HTMLEscaper,
 		"index":    index,
 		"slice":    slice,
@@ -93,8 +93,8 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
 		if v.Kind() != reflect.Func {
 			panic("value for " + name + " not a function")
 		}
-		if !goodFunc(v.Type()) {
-			panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
+		if err := goodFunc(name, v.Type()); err != nil {
+			panic(err)
 		}
 		out[name] = v
 	}
@@ -109,15 +109,18 @@ func addFuncs(out, in FuncMap) {
 }
 
 // goodFunc reports whether the function or method has the right result signature.
-func goodFunc(typ reflect.Type) bool {
+func goodFunc(name string, typ reflect.Type) error {
 	// We allow functions with 1 result or 2 results where the second is an error.
-	switch {
-	case typ.NumOut() == 1:
-		return true
-	case typ.NumOut() == 2 && typ.Out(1) == errorType:
-		return true
+	switch numOut := typ.NumOut(); {
+	case numOut == 1:
+		return nil
+	case numOut == 2 && typ.Out(1) == errorType:
+		return nil
+	case numOut == 2:
+		return fmt.Errorf("invalid function signature for %s: second return value should be error; is %s", name, typ.Out(1))
+	default:
+		return fmt.Errorf("function %s has %d return values; should be 1 or 2", name, typ.NumOut())
 	}
-	return false
 }
 
 // goodName reports whether the function name is a valid identifier.
@@ -309,30 +312,35 @@ func length(item reflect.Value) (int, error) {
 
 // Function invocation
 
+func emptyCall(fn reflect.Value, args ...reflect.Value) reflect.Value {
+	panic("unreachable") // implemented as a special case in evalCall
+}
+
 // call returns the result of evaluating the first argument as a function.
 // The function must return 1 result, or 2 results, the second of which is an error.
-func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
+func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
 	fn = indirectInterface(fn)
 	if !fn.IsValid() {
 		return reflect.Value{}, fmt.Errorf("call of nil")
 	}
 	typ := fn.Type()
 	if typ.Kind() != reflect.Func {
-		return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
+		return reflect.Value{}, fmt.Errorf("non-function %s of type %s", name, typ)
 	}
-	if !goodFunc(typ) {
-		return reflect.Value{}, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
+
+	if err := goodFunc(name, typ); err != nil {
+		return reflect.Value{}, err
 	}
 	numIn := typ.NumIn()
 	var dddType reflect.Type
 	if typ.IsVariadic() {
 		if len(args) < numIn-1 {
-			return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
+			return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want at least %d", name, len(args), numIn-1)
 		}
 		dddType = typ.In(numIn - 1).Elem()
 	} else {
 		if len(args) != numIn {
-			return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
+			return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want %d", name, len(args), numIn)
 		}
 	}
 	argv := make([]reflect.Value, len(args))
diff --git a/tpl/internal/go_templates/texttemplate/helper.go b/tpl/internal/go_templates/texttemplate/helper.go
index 48af3928b39..81b55538e57 100644
--- a/tpl/internal/go_templates/texttemplate/helper.go
+++ b/tpl/internal/go_templates/texttemplate/helper.go
@@ -16,7 +16,7 @@ import (
 
 // Functions and methods to parse templates.
 
-// Must is a helper that wraps a call to a function returning (*Template, error)
+// Must is a helper that wraps a call to a function returning ([*Template], error)
 // and panics if the error is non-nil. It is intended for use in variable
 // initializations such as
 //
@@ -28,7 +28,7 @@ func Must(t *Template, err error) *Template {
 	return t
 }
 
-// ParseFiles creates a new Template and parses the template definitions from
+// ParseFiles creates a new [Template] and parses the template definitions from
 // the named files. The returned template's name will have the base name and
 // parsed contents of the first file. There must be at least one file.
 // If an error occurs, parsing stops and the returned *Template is nil.
@@ -45,9 +45,9 @@ func ParseFiles(filenames ...string) (*Template, error) {
 // t. If an error occurs, parsing stops and the returned template is nil;
 // otherwise it is t. There must be at least one file.
 // Since the templates created by ParseFiles are named by the base
-// names of the argument files, t should usually have the name of one
-// of the (base) names of the files. If it does not, depending on t's
-// contents before calling ParseFiles, t.Execute may fail. In that
+// (see [filepath.Base]) names of the argument files, t should usually have the
+// name of one of the (base) names of the files. If it does not, depending on
+// t's contents before calling ParseFiles, t.Execute may fail. In that
 // case use t.ExecuteTemplate to execute a valid template.
 //
 // When parsing multiple files with the same name in different directories,
@@ -93,12 +93,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
 	return t, nil
 }
 
-// ParseGlob creates a new Template and parses the template definitions from
+// ParseGlob creates a new [Template] and parses the template definitions from
 // the files identified by the pattern. The files are matched according to the
-// semantics of filepath.Match, and the pattern must match at least one file.
-// The returned template will have the (base) name and (parsed) contents of the
-// first file matched by the pattern. ParseGlob is equivalent to calling
-// ParseFiles with the list of files matched by the pattern.
+// semantics of [filepath.Match], and the pattern must match at least one file.
+// The returned template will have the [filepath.Base] name and (parsed)
+// contents of the first file matched by the pattern. ParseGlob is equivalent to
+// calling [ParseFiles] with the list of files matched by the pattern.
 //
 // When parsing multiple files with the same name in different directories,
 // the last one mentioned will be the one that results.
@@ -108,9 +108,9 @@ func ParseGlob(pattern string) (*Template, error) {
 
 // ParseGlob parses the template definitions in the files identified by the
 // pattern and associates the resulting templates with t. The files are matched
-// according to the semantics of filepath.Match, and the pattern must match at
-// least one file. ParseGlob is equivalent to calling t.ParseFiles with the
-// list of files matched by the pattern.
+// according to the semantics of [filepath.Match], and the pattern must match at
+// least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with
+// the list of files matched by the pattern.
 //
 // When parsing multiple files with the same name in different directories,
 // the last one mentioned will be the one that results.
@@ -131,17 +131,17 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
 	return parseFiles(t, readFileOS, filenames...)
 }
 
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
 // instead of the host operating system's file system.
-// It accepts a list of glob patterns.
+// It accepts a list of glob patterns (see [path.Match]).
 // (Note that most file names serve as glob patterns matching only themselves.)
 func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
 	return parseFS(nil, fsys, patterns)
 }
 
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
 // instead of the host operating system's file system.
-// It accepts a list of glob patterns.
+// It accepts a list of glob patterns (see [path.Match]).
 // (Note that most file names serve as glob patterns matching only themselves.)
 func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
 	t.init()
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
index 276367a7c38..12dbe041217 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -278,9 +278,8 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
 	} else if numIn != typ.NumIn() {
 		s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
 	}
-	if !goodFunc(typ) {
-		// TODO: This could still be a confusing error; maybe goodFunc should provide info.
-		s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
+	if err := goodFunc(name, typ); err != nil {
+		s.errorf("%v", err)
 	}
 
 	unwrap := func(v reflect.Value) reflect.Value {
@@ -345,6 +344,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
 		argv[i] = s.validateType(final, t)
 	}
 
+	// Special case for the "call" builtin.
+	// Insert the name of the callee function as the first argument.
+	if isBuiltin && name == "call" {
+		calleeName := args[0].String()
+		argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
+		fun = reflect.ValueOf(call)
+	}
+
 	// Added for Hugo
 	for i := 0; i < len(first); i++ {
 		argv[i] = s.validateType(first[i], typ.In(i))
diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go
index c36688825c2..a31309874d9 100644
--- a/tpl/internal/go_templates/texttemplate/parse/node.go
+++ b/tpl/internal/go_templates/texttemplate/parse/node.go
@@ -217,7 +217,11 @@ func (p *PipeNode) writeTo(sb *strings.Builder) {
 			}
 			v.writeTo(sb)
 		}
-		sb.WriteString(" := ")
+		if p.IsAssign {
+			sb.WriteString(" = ")
+		} else {
+			sb.WriteString(" := ")
+		}
 	}
 	for i, c := range p.Cmds {
 		if i > 0 {
@@ -346,12 +350,12 @@ type IdentifierNode struct {
 	Ident string // The identifier's name.
 }
 
-// NewIdentifier returns a new IdentifierNode with the given identifier name.
+// NewIdentifier returns a new [IdentifierNode] with the given identifier name.
 func NewIdentifier(ident string) *IdentifierNode {
 	return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
 }
 
-// SetPos sets the position. NewIdentifier is a public method so we can't modify its signature.
+// SetPos sets the position. [NewIdentifier] is a public method so we can't modify its signature.
 // Chained for convenience.
 // TODO: fix one day?
 func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
@@ -359,7 +363,7 @@ func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
 	return i
 }
 
-// SetTree sets the parent tree for the node. NewIdentifier is a public method so we can't modify its signature.
+// SetTree sets the parent tree for the node. [NewIdentifier] is a public method so we can't modify its signature.
 // Chained for convenience.
 // TODO: fix one day?
 func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go
index d43d5334ba0..27c84f31eb3 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse.go
@@ -42,7 +42,7 @@ const (
 	SkipFuncCheck                  // do not check that functions are defined
 )
 
-// Copy returns a copy of the Tree. Any parsing state is discarded.
+// Copy returns a copy of the [Tree]. Any parsing state is discarded.
 func (t *Tree) Copy() *Tree {
 	if t == nil {
 		return nil
@@ -55,7 +55,7 @@ func (t *Tree) Copy() *Tree {
 	}
 }
 
-// Parse returns a map from template name to parse.Tree, created by parsing the
+// Parse returns a map from template name to [Tree], created by parsing the
 // templates described in the argument string. The top-level template will be
 // given the specified name. If an error is encountered, parsing stops and an
 // empty map is returned with the error.
@@ -521,7 +521,7 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
 	}
 }
 
-func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
+func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
 	defer t.popVars(len(t.vars))
 	pipe = t.pipeline(context, itemRightDelim)
 	if context == "range" {
@@ -535,27 +535,30 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
 	switch next.Type() {
 	case nodeEnd: //done
 	case nodeElse:
-		if allowElseIf {
-			// Special case for "else if". If the "else" is followed immediately by an "if",
-			// the elseControl will have left the "if" token pending. Treat
-			//	{{if a}}_{{else if b}}_{{end}}
-			// as
-			//	{{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
-			// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
-			// is assumed. This technique works even for long if-else-if chains.
-			// TODO: Should we allow else-if in with and range?
-			if t.peek().typ == itemIf {
-				t.next() // Consume the "if" token.
-				elseList = t.newList(next.Position())
-				elseList.append(t.ifControl())
-				// Do not consume the next item - only one {{end}} required.
-				break
+		// Special case for "else if" and "else with".
+		// If the "else" is followed immediately by an "if" or "with",
+		// the elseControl will have left the "if" or "with" token pending. Treat
+		//	{{if a}}_{{else if b}}_{{end}}
+		//  {{with a}}_{{else with b}}_{{end}}
+		// as
+		//	{{if a}}_{{else}}{{if b}}_{{end}}{{end}}
+		//  {{with a}}_{{else}}{{with b}}_{{end}}{{end}}.
+		// To do this, parse the "if" or "with" as usual and stop at it {{end}};
+		// the subsequent{{end}} is assumed. This technique works even for long if-else-if chains.
+		if context == "if" && t.peek().typ == itemIf {
+			t.next() // Consume the "if" token.
+			elseList = t.newList(next.Position())
+			elseList.append(t.ifControl())
+		} else if context == "with" && t.peek().typ == itemWith {
+			t.next()
+			elseList = t.newList(next.Position())
+			elseList.append(t.withControl())
+		} else {
+			elseList, next = t.itemList()
+			if next.Type() != nodeEnd {
+				t.errorf("expected end; found %s", next)
 			}
 		}
-		elseList, next = t.itemList()
-		if next.Type() != nodeEnd {
-			t.errorf("expected end; found %s", next)
-		}
 	}
 	return pipe.Position(), pipe.Line, pipe, list, elseList
 }
@@ -567,7 +570,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
 //
 // If keyword is past.
 func (t *Tree) ifControl() Node {
-	return t.newIf(t.parseControl(true, "if"))
+	return t.newIf(t.parseControl("if"))
 }
 
 // Range:
@@ -577,7 +580,7 @@ func (t *Tree) ifControl() Node {
 //
 // Range keyword is past.
 func (t *Tree) rangeControl() Node {
-	r := t.newRange(t.parseControl(false, "range"))
+	r := t.newRange(t.parseControl("range"))
 	return r
 }
 
@@ -588,7 +591,7 @@ func (t *Tree) rangeControl() Node {
 //
 // If keyword is past.
 func (t *Tree) withControl() Node {
-	return t.newWith(t.parseControl(false, "with"))
+	return t.newWith(t.parseControl("with"))
 }
 
 // End:
@@ -606,10 +609,11 @@ func (t *Tree) endControl() Node {
 //
 // Else keyword is past.
 func (t *Tree) elseControl() Node {
-	// Special case for "else if".
 	peek := t.peekNonSpace()
-	if peek.typ == itemIf {
-		// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
+	// The "{{else if ... " and "{{else with ..." will be
+	// treated as "{{else}}{{if ..." and "{{else}}{{with ...".
+	// So return the else node here.
+	if peek.typ == itemIf || peek.typ == itemWith {
 		return t.newElse(peek.pos, peek.line)
 	}
 	token := t.expect(itemRightDelim, "else")
diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go
index 1ba72c194d0..536932a60bd 100644
--- a/tpl/internal/go_templates/texttemplate/template.go
+++ b/tpl/internal/go_templates/texttemplate/template.go
@@ -24,7 +24,7 @@ type common struct {
 }
 
 // Template is the representation of a parsed template. The *parse.Tree
-// field is exported only for use by html/template and should be treated
+// field is exported only for use by [html/template] and should be treated
 // as unexported by all other clients.
 type Template struct {
 	name string
@@ -79,7 +79,7 @@ func (t *Template) init() {
 
 // Clone returns a duplicate of the template, including all associated
 // templates. The actual representation is not copied, but the name space of
-// associated templates is, so further calls to Parse in the copy will add
+// associated templates is, so further calls to [Template.Parse] in the copy will add
 // templates to the copy but not to the original. Clone can be used to prepare
 // common templates and use them with variant definitions for other templates
 // by adding the variants after the clone is made.
@@ -157,7 +157,7 @@ func (t *Template) Templates() []*Template {
 }
 
 // Delims sets the action delimiters to the specified strings, to be used in
-// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
+// subsequent calls to [Template.Parse], [Template.ParseFiles], or [Template.ParseGlob]. Nested template
 // definitions will inherit the settings. An empty delimiter stands for the
 // corresponding default: {{ or }}.
 // The return value is the template, so calls can be chained.


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b5491090e66..1202620986c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,7 +16,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.21.x, 1.22.x]
+        go-version: [1.22.x, 1.23.x]
         os: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/testscripts/commands/mod.txt b/testscripts/commands/mod.txt
index 56cea2c001a..2fa17dbbe5e 100644
--- a/testscripts/commands/mod.txt
+++ b/testscripts/commands/mod.txt
@@ -18,7 +18,8 @@ hugo mod clean
 ! stderr .
 stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
 hugo mod clean --all
-stdout 'Deleted 2\d{2} files from module cache\.'
+# Currently this is 299 on MacOS and 301 on Linux.
+stdout 'Deleted (2|3)\d{2} files from module cache\.'
 cd submod
 hugo mod init testsubmod
 cmpenv go.mod $WORK/golden/go.mod.testsubmod
diff --git a/testscripts/commands/mod_npm.txt b/testscripts/commands/mod_npm.txt
index 32cc37f06ad..3d8903e6a35 100644
--- a/testscripts/commands/mod_npm.txt
+++ b/testscripts/commands/mod_npm.txt
@@ -2,6 +2,7 @@
 
 dostounix golden/package.json
 
+
 hugo mod npm pack
 cmp package.json golden/package.json
 
@@ -41,3 +42,4 @@ path="github.com/gohugoio/hugoTestModule2"
 }
 -- go.mod --
 module github.com/gohugoio/hugoTestModule
+go 1.20
diff --git a/testscripts/commands/mod_npm_withexisting.txt b/testscripts/commands/mod_npm_withexisting.txt
index e92eba3fd9d..073af0f077f 100644
--- a/testscripts/commands/mod_npm_withexisting.txt
+++ b/testscripts/commands/mod_npm_withexisting.txt
@@ -55,3 +55,4 @@ path="github.com/gohugoio/hugoTestModule2"
 }
 -- go.mod --
 module github.com/gohugoio/hugoTestModule
+go 1.20
diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go
index e86b4c673d4..0986dbb6df9 100644
--- a/tpl/internal/go_templates/fmtsort/sort_test.go
+++ b/tpl/internal/go_templates/fmtsort/sort_test.go
@@ -5,12 +5,13 @@
 package fmtsort_test
 
 import (
+	"cmp"
 	"fmt"
 	"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
 	"math"
 	"reflect"
 	"runtime"
-	"sort"
+	"slices"
 	"strings"
 	"testing"
 	"unsafe"
@@ -67,10 +68,6 @@ func TestCompare(t *testing.T) {
 				switch {
 				case i == j:
 					expect = 0
-					// NaNs are tricky.
-					if typ := v0.Type(); (typ.Kind() == reflect.Float32 || typ.Kind() == reflect.Float64) && math.IsNaN(v0.Float()) {
-						expect = -1
-					}
 				case i < j:
 					expect = -1
 				case i > j:
@@ -142,13 +139,13 @@ func sprint(data any) string {
 		return "nil"
 	}
 	b := new(strings.Builder)
-	for i, key := range om.Key {
+	for i, m := range om {
 		if i > 0 {
 			b.WriteRune(' ')
 		}
-		b.WriteString(sprintKey(key))
+		b.WriteString(sprintKey(m.Key))
 		b.WriteRune(':')
-		fmt.Fprint(b, om.Value[i])
+		fmt.Fprint(b, m.Value)
 	}
 	return b.String()
 }
@@ -200,8 +197,8 @@ func makeChans() []chan int {
 	for i := range cs {
 		pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
 	}
-	sort.Slice(cs, func(i, j int) bool {
-		return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
+	slices.SortFunc(cs, func(a, b chan int) int {
+		return cmp.Compare(reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
 	})
 	return cs
 }
diff --git a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go
index 43cc3bf011f..24b22d984a2 100644
--- a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go
+++ b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go
@@ -2,9 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build go1.13
-// +build go1.13
-
 package template_test
 
 import (
diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go
index 428cddc0d28..e01813e6852 100644
--- a/tpl/internal/go_templates/htmltemplate/exec_test.go
+++ b/tpl/internal/go_templates/htmltemplate/exec_test.go
@@ -273,8 +273,8 @@ type execTest struct {
 // of the max int boundary.
 // We do it this way so the test doesn't depend on ints being 32 bits.
 var (
-	bigInt  = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
-	bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
+	bigInt  = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
+	bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
 )
 
 var execTests = []execTest{
@@ -580,6 +580,8 @@ var execTests = []execTest{
 	{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
 	{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
 	{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
+	{"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
+	{"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
 
 	// Range.
 	{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
index 277df2255f8..e8577d2eae8 100644
--- a/tpl/internal/go_templates/testenv/testenv.go
+++ b/tpl/internal/go_templates/testenv/testenv.go
@@ -132,15 +132,13 @@ func findGOROOT() (string, error) {
 			// If runtime.GOROOT() is non-empty, assume that it is valid.
 			//
 			// (It might not be: for example, the user may have explicitly set GOROOT
-			// to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
-			// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
+			// to the wrong directory. But this case is
 			// rare, and if that happens the user can fix what they broke.)
 			return
 		}
 
 		// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
-		// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
-		// without GOROOT and the tree hasn't been moved there yet).
+		// binary was built with -trimpath).
 		//
 		// Since this is internal/testenv, we can cheat and assume that the caller
 		// is a test of some package in a subdirectory of GOROOT/src. ('go test'
@@ -315,12 +313,18 @@ func MustInternalLink(t testing.TB, withCgo bool) {
 	}
 }
 
+// MustInternalLinkPIE checks whether the current system can link PIE binary using
+// internal linking.
+// If not, MustInternalLinkPIE calls t.Skip with an explanation.
+// Modified by Hugo (not needed)
+func MustInternalLinkPIE(t testing.TB) {
+}
+
 // MustHaveBuildMode reports whether the current system can build programs in
 // the given build mode.
 // If not, MustHaveBuildMode calls t.Skip with an explanation.
 // Modified by Hugo (not needed)
 func MustHaveBuildMode(t testing.TB, buildmode string) {
-	return
 }
 
 // HasSymlink reports whether the current system can use os.Symlink.
@@ -447,3 +451,10 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
 func SyscallIsNotSupported(err error) bool {
 	return syscallIsNotSupported(err)
 }
+
+// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel.
+// This function should be used when it is necessary to avoid t.Parallel on
+// 32-bit machines, typically because the test uses lots of memory.
+// Disabled by Hugo.
+func ParallelOn64Bit(t *testing.T) {
+}
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/internal/go_templates/texttemplate/examplefiles_test.go b/tpl/internal/go_templates/texttemplate/examplefiles_test.go
index bc91e87f99b..6534ee33158 100644
--- a/tpl/internal/go_templates/texttemplate/examplefiles_test.go
+++ b/tpl/internal/go_templates/texttemplate/examplefiles_test.go
@@ -2,9 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build go1.13
-// +build go1.13
-
 package template_test
 
 import (
diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
index ed75adacb6a..efbaa9eec47 100644
--- a/tpl/internal/go_templates/texttemplate/exec_test.go
+++ b/tpl/internal/go_templates/texttemplate/exec_test.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build !windows
+// +build !windows
+
 package template
 
 import (
@@ -75,12 +78,15 @@ type T struct {
 	PSI *[]int
 	NIL *int
 	// Function (not method)
-	BinaryFunc      func(string, string) string
-	VariadicFunc    func(...string) string
-	VariadicFuncInt func(int, ...string) string
-	NilOKFunc       func(*int) bool
-	ErrFunc         func() (string, error)
-	PanicFunc       func() string
+	BinaryFunc             func(string, string) string
+	VariadicFunc           func(...string) string
+	VariadicFuncInt        func(int, ...string) string
+	NilOKFunc              func(*int) bool
+	ErrFunc                func() (string, error)
+	PanicFunc              func() string
+	TooFewReturnCountFunc  func()
+	TooManyReturnCountFunc func() (string, error, int)
+	InvalidReturnTypeFunc  func() (string, bool)
 	// Template to test evaluation of templates.
 	Tmpl *Template
 	// Unexported field; cannot be accessed by template.
@@ -168,6 +174,9 @@ var tVal = &T{
 	NilOKFunc:                 func(s *int) bool { return s == nil },
 	ErrFunc:                   func() (string, error) { return "bla", nil },
 	PanicFunc:                 func() string { panic("test panic") },
+	TooFewReturnCountFunc:     func() {},
+	TooManyReturnCountFunc:    func() (string, error, int) { return "", nil, 0 },
+	InvalidReturnTypeFunc:     func() (string, bool) { return "", false },
 	Tmpl:                      Must(New("x").Parse("test template")), // "x" is the value of .X
 }
 
@@ -265,8 +274,8 @@ type execTest struct {
 // of the max int boundary.
 // We do it this way so the test doesn't depend on ints being 32 bits.
 var (
-	bigInt  = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
-	bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
+	bigInt  = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
+	bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
 )
 
 var execTests = []execTest{
@@ -583,6 +592,8 @@ var execTests = []execTest{
 	{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
 	{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
 	{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
+	{"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
+	{"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
 
 	// Range.
 	{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
@@ -1723,6 +1734,81 @@ func TestExecutePanicDuringCall(t *testing.T) {
 	}
 }
 
+func TestFunctionCheckDuringCall(t *testing.T) {
+	tests := []struct {
+		name    string
+		input   string
+		data    any
+		wantErr string
+	}{
+		{
+			name:    "call nothing",
+			input:   `{{call}}`,
+			data:    tVal,
+			wantErr: "wrong number of args for call: want at least 1 got 0",
+		},
+		{
+			name:    "call non-function",
+			input:   "{{call .True}}",
+			data:    tVal,
+			wantErr: "error calling call: non-function .True of type bool",
+		},
+		{
+			name:    "call func with wrong argument",
+			input:   "{{call .BinaryFunc 1}}",
+			data:    tVal,
+			wantErr: "error calling call: wrong number of args for .BinaryFunc: got 1 want 2",
+		},
+		{
+			name:    "call variadic func with wrong argument",
+			input:   `{{call .VariadicFuncInt}}`,
+			data:    tVal,
+			wantErr: "error calling call: wrong number of args for .VariadicFuncInt: got 0 want at least 1",
+		},
+		{
+			name:    "call too few return number func",
+			input:   `{{call .TooFewReturnCountFunc}}`,
+			data:    tVal,
+			wantErr: "error calling call: function .TooFewReturnCountFunc has 0 return values; should be 1 or 2",
+		},
+		{
+			name:    "call too many return number func",
+			input:   `{{call .TooManyReturnCountFunc}}`,
+			data:    tVal,
+			wantErr: "error calling call: function .TooManyReturnCountFunc has 3 return values; should be 1 or 2",
+		},
+		{
+			name:    "call invalid return type func",
+			input:   `{{call .InvalidReturnTypeFunc}}`,
+			data:    tVal,
+			wantErr: "error calling call: invalid function signature for .InvalidReturnTypeFunc: second return value should be error; is bool",
+		},
+		{
+			name:    "call pipeline",
+			input:   `{{call (len "test")}}`,
+			data:    nil,
+			wantErr: "error calling call: non-function len \"test\" of type int",
+		},
+	}
+
+	for _, tc := range tests {
+		b := new(bytes.Buffer)
+		tmpl, err := New("t").Parse(tc.input)
+		if err != nil {
+			t.Fatalf("parse error: %s", err)
+		}
+		err = tmpl.Execute(b, tc.data)
+		if err == nil {
+			t.Errorf("%s: expected error; got none", tc.name)
+		} else if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) {
+			if *debug {
+				fmt.Printf("%s: test execute error: %s\n", tc.name, err)
+			}
+			t.Errorf("%s: expected error:\n%s\ngot:\n%s", tc.name, tc.wantErr, err)
+		}
+	}
+}
+
 // Issue 31810. Check that a parenthesized first argument behaves properly.
 func TestIssue31810(t *testing.T) {
 	// A simple value with no arguments is fine.
diff --git a/tpl/internal/go_templates/texttemplate/link_test.go b/tpl/internal/go_templates/texttemplate/link_test.go
index 23f6a31fade..63418cd90ce 100644
--- a/tpl/internal/go_templates/texttemplate/link_test.go
+++ b/tpl/internal/go_templates/texttemplate/link_test.go
@@ -2,18 +2,16 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build go1.13
-// +build go1.13
-
 package template_test
 
 import (
 	"bytes"
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"testing"
+
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
 )
 
 // Issue 36021: verify that text/template doesn't prevent the linker from removing
@@ -44,7 +42,7 @@ func main() {
 `
 	td := t.TempDir()
 
-	if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
 		t.Fatal(err)
 	}
 	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
index 59e0a174122..80e7f53fadd 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
@@ -33,9 +33,9 @@ var numberTests = []numberTest{
 	{"7_3", true, true, true, false, 73, 73, 73, 0},
 	{"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
 	{"0B10_010_01", true, true, true, false, 73, 73, 73, 0},
-	{"073", true, true, true, false, 073, 073, 073, 0},
-	{"0o73", true, true, true, false, 073, 073, 073, 0},
-	{"0O73", true, true, true, false, 073, 073, 073, 0},
+	{"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
+	{"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
+	{"0O73", true, true, true, false, 0o73, 0o73, 0o73, 0},
 	{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
 	{"0X73", true, true, true, false, 0x73, 0x73, 0x73, 0},
 	{"0x7_3", true, true, true, false, 0x73, 0x73, 0x73, 0},
@@ -61,7 +61,7 @@ var numberTests = []numberTest{
 	{"-12+0i", true, false, true, true, -12, 0, -12, -12},
 	{"13+0i", true, true, true, true, 13, 13, 13, 13},
 	// funny bases
-	{"0123", true, true, true, false, 0123, 0123, 0123, 0},
+	{"0123", true, true, true, false, 0o123, 0o123, 0o123, 0},
 	{"-0x0", true, true, true, false, 0, 0, 0, 0},
 	{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
 	// character constants
@@ -176,74 +176,150 @@ const (
 )
 
 var parseTests = []parseTest{
-	{"empty", "", noError,
-		``},
-	{"comment", "{{/*\n\n\n*/}}", noError,
-		``},
-	{"spaces", " \t\n", noError,
-		`" \t\n"`},
-	{"text", "some text", noError,
-		`"some text"`},
-	{"emptyAction", "{{}}", hasError,
-		`{{}}`},
-	{"field", "{{.X}}", noError,
-		`{{.X}}`},
-	{"simple command", "{{printf}}", noError,
-		`{{printf}}`},
-	{"$ invocation", "{{$}}", noError,
-		"{{$}}"},
-	{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
-		"{{with $x := 3}}{{$x 23}}{{end}}"},
-	{"variable with fields", "{{$.I}}", noError,
-		"{{$.I}}"},
-	{"multi-word command", "{{printf `%d` 23}}", noError,
-		"{{printf `%d` 23}}"},
-	{"pipeline", "{{.X|.Y}}", noError,
-		`{{.X | .Y}}`},
-	{"pipeline with decl", "{{$x := .X|.Y}}", noError,
-		`{{$x := .X | .Y}}`},
-	{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
-		`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
-	{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
-		`{{(.Y .Z).Field}}`},
-	{"simple if", "{{if .X}}hello{{end}}", noError,
-		`{{if .X}}"hello"{{end}}`},
-	{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
-		`{{if .X}}"true"{{else}}"false"{{end}}`},
-	{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
-		`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
-	{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
-		`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
-	{"simple range", "{{range .X}}hello{{end}}", noError,
-		`{{range .X}}"hello"{{end}}`},
-	{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
-		`{{range .X.Y.Z}}"hello"{{end}}`},
-	{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
-		`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
-	{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
-		`{{range .X}}"true"{{else}}"false"{{end}}`},
-	{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
-		`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
-	{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
-		`{{range .SI}}{{.}}{{end}}`},
-	{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
-		`{{range $x := .SI}}{{.}}{{end}}`},
-	{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
-		`{{range $x, $y := .SI}}{{.}}{{end}}`},
-	{"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
-		`{{range .SI}}{{.}}{{break}}{{end}}`},
-	{"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
-		`{{range .SI}}{{.}}{{continue}}{{end}}`},
-	{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
-		`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
-	{"template", "{{template `x`}}", noError,
-		`{{template "x"}}`},
-	{"template with arg", "{{template `x` .Y}}", noError,
-		`{{template "x" .Y}}`},
-	{"with", "{{with .X}}hello{{end}}", noError,
-		`{{with .X}}"hello"{{end}}`},
-	{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
-		`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
+	{
+		"empty", "", noError,
+		``,
+	},
+	{
+		"comment", "{{/*\n\n\n*/}}", noError,
+		``,
+	},
+	{
+		"spaces", " \t\n", noError,
+		`" \t\n"`,
+	},
+	{
+		"text", "some text", noError,
+		`"some text"`,
+	},
+	{
+		"emptyAction", "{{}}", hasError,
+		`{{}}`,
+	},
+	{
+		"field", "{{.X}}", noError,
+		`{{.X}}`,
+	},
+	{
+		"simple command", "{{printf}}", noError,
+		`{{printf}}`,
+	},
+	{
+		"$ invocation", "{{$}}", noError,
+		"{{$}}",
+	},
+	{
+		"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
+		"{{with $x := 3}}{{$x 23}}{{end}}",
+	},
+	{
+		"variable with fields", "{{$.I}}", noError,
+		"{{$.I}}",
+	},
+	{
+		"multi-word command", "{{printf `%d` 23}}", noError,
+		"{{printf `%d` 23}}",
+	},
+	{
+		"pipeline", "{{.X|.Y}}", noError,
+		`{{.X | .Y}}`,
+	},
+	{
+		"pipeline with decl", "{{$x := .X|.Y}}", noError,
+		`{{$x := .X | .Y}}`,
+	},
+	{
+		"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
+		`{{.X (.Y .Z) (.A | .B .C) (.E)}}`,
+	},
+	{
+		"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
+		`{{(.Y .Z).Field}}`,
+	},
+	{
+		"simple if", "{{if .X}}hello{{end}}", noError,
+		`{{if .X}}"hello"{{end}}`,
+	},
+	{
+		"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
+		`{{if .X}}"true"{{else}}"false"{{end}}`,
+	},
+	{
+		"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
+		`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`,
+	},
+	{
+		"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
+		`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`,
+	},
+	{
+		"simple range", "{{range .X}}hello{{end}}", noError,
+		`{{range .X}}"hello"{{end}}`,
+	},
+	{
+		"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
+		`{{range .X.Y.Z}}"hello"{{end}}`,
+	},
+	{
+		"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
+		`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`,
+	},
+	{
+		"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
+		`{{range .X}}"true"{{else}}"false"{{end}}`,
+	},
+	{
+		"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
+		`{{range .X | .M}}"true"{{else}}"false"{{end}}`,
+	},
+	{
+		"range []int", "{{range .SI}}{{.}}{{end}}", noError,
+		`{{range .SI}}{{.}}{{end}}`,
+	},
+	{
+		"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
+		`{{range $x := .SI}}{{.}}{{end}}`,
+	},
+	{
+		"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
+		`{{range $x, $y := .SI}}{{.}}{{end}}`,
+	},
+	{
+		"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
+		`{{range .SI}}{{.}}{{break}}{{end}}`,
+	},
+	{
+		"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
+		`{{range .SI}}{{.}}{{continue}}{{end}}`,
+	},
+	{
+		"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
+		`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`,
+	},
+	{
+		"template", "{{template `x`}}", noError,
+		`{{template "x"}}`,
+	},
+	{
+		"template with arg", "{{template `x` .Y}}", noError,
+		`{{template "x" .Y}}`,
+	},
+	{
+		"with", "{{with .X}}hello{{end}}", noError,
+		`{{with .X}}"hello"{{end}}`,
+	},
+	{
+		"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
+		`{{with .X}}"hello"{{else}}"goodbye"{{end}}`,
+	},
+	{
+		"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
+		`{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`,
+	},
+	{
+		"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
+		`{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`,
+	},
 	// Trimming spaces.
 	{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
 	{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
@@ -252,18 +328,24 @@ var parseTests = []parseTest{
 	{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
 	{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
 	{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
-	{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
-		`{{template "foo" .}}`},
+	{
+		"block definition", `{{block "foo" .}}hello{{end}}`, noError,
+		`{{template "foo" .}}`,
+	},
 
 	{"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
 	{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
 	{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
 	{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
 	{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
-	{"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
-		`{{range .SI}}{{.}}{{continue}}{{end}}`},
-	{"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
-		`{{range .SI}}{{.}}{{break}}{{end}}`},
+	{
+		"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
+		`{{range .SI}}{{.}}{{continue}}{{end}}`,
+	},
+	{
+		"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
+		`{{range .SI}}{{.}}{{break}}{{end}}`,
+	},
 
 	// Errors.
 	{"unclosed action", "hello{{range", hasError, ""},
@@ -302,6 +384,9 @@ var parseTests = []parseTest{
 	{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""},                     // ! is just illegal here.
 	{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""},                     // $x+2 should not parse as ($x) (+2).
 	{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
+	// Check the range handles assignment vs. declaration properly.
+	{"bug2a", "{{range $x := 0}}{{$x}}{{end}}", noError, "{{range $x := 0}}{{$x}}{{end}}"},
+	{"bug2b", "{{range $x = 0}}{{$x}}{{end}}", noError, "{{range $x = 0}}{{$x}}{{end}}"},
 	// dot following a literal value
 	{"dot after integer", "{{1.E}}", hasError, ""},
 	{"dot after float", "{{0.1.E}}", hasError, ""},
@@ -402,7 +487,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
 	{
 		// 'break' is a defined function, don't treat it as a keyword: it should
 		// accept an argument successfully.
-		var funcsWithKeywordFunc = map[string]any{
+		funcsWithKeywordFunc := map[string]any{
 			"break": func(in any) any { return in },
 		}
 		tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
@@ -489,104 +574,168 @@ func TestErrorContextWithTreeCopy(t *testing.T) {
 // All failures, and the result is a string that must appear in the error message.
 var errorTests = []parseTest{
 	// Check line numbers are accurate.
-	{"unclosed1",
+	{
+		"unclosed1",
 		"line1\n{{",
-		hasError, `unclosed1:2: unclosed action`},
-	{"unclosed2",
+		hasError, `unclosed1:2: unclosed action`,
+	},
+	{
+		"unclosed2",
 		"line1\n{{define `x`}}line2\n{{",
-		hasError, `unclosed2:3: unclosed action`},
-	{"unclosed3",
+		hasError, `unclosed2:3: unclosed action`,
+	},
+	{
+		"unclosed3",
 		"line1\n{{\"x\"\n\"y\"\n",
-		hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
-	{"unclosed4",
+		hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
+	},
+	{
+		"unclosed4",
 		"{{\n\n\n\n\n",
-		hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
-	{"var1",
+		hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
+	},
+	{
+		"var1",
 		"line1\n{{\nx\n}}",
-		hasError, `var1:3: function "x" not defined`},
+		hasError, `var1:3: function "x" not defined`,
+	},
 	// Specific errors.
-	{"function",
+	{
+		"function",
 		"{{foo}}",
-		hasError, `function "foo" not defined`},
-	{"comment1",
+		hasError, `function "foo" not defined`,
+	},
+	{
+		"comment1",
 		"{{/*}}",
-		hasError, `comment1:1: unclosed comment`},
-	{"comment2",
+		hasError, `comment1:1: unclosed comment`,
+	},
+	{
+		"comment2",
 		"{{/*\nhello\n}}",
-		hasError, `comment2:1: unclosed comment`},
-	{"lparen",
+		hasError, `comment2:1: unclosed comment`,
+	},
+	{
+		"lparen",
 		"{{.X (1 2 3}}",
-		hasError, `unclosed left paren`},
-	{"rparen",
+		hasError, `unclosed left paren`,
+	},
+	{
+		"rparen",
 		"{{.X 1 2 3 ) }}",
-		hasError, "unexpected right paren"},
-	{"rparen2",
+		hasError, "unexpected right paren",
+	},
+	{
+		"rparen2",
 		"{{(.X 1 2 3",
-		hasError, `unclosed action`},
-	{"space",
+		hasError, `unclosed action`,
+	},
+	{
+		"space",
 		"{{`x`3}}",
-		hasError, `in operand`},
-	{"idchar",
+		hasError, `in operand`,
+	},
+	{
+		"idchar",
 		"{{a#}}",
-		hasError, `'#'`},
-	{"charconst",
+		hasError, `'#'`,
+	},
+	{
+		"charconst",
 		"{{'a}}",
-		hasError, `unterminated character constant`},
-	{"stringconst",
+		hasError, `unterminated character constant`,
+	},
+	{
+		"stringconst",
 		`{{"a}}`,
-		hasError, `unterminated quoted string`},
-	{"rawstringconst",
+		hasError, `unterminated quoted string`,
+	},
+	{
+		"rawstringconst",
 		"{{`a}}",
-		hasError, `unterminated raw quoted string`},
-	{"number",
+		hasError, `unterminated raw quoted string`,
+	},
+	{
+		"number",
 		"{{0xi}}",
-		hasError, `number syntax`},
-	{"multidefine",
+		hasError, `number syntax`,
+	},
+	{
+		"multidefine",
 		"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
-		hasError, `multiple definition of template`},
-	{"eof",
+		hasError, `multiple definition of template`,
+	},
+	{
+		"eof",
 		"{{range .X}}",
-		hasError, `unexpected EOF`},
-	{"variable",
+		hasError, `unexpected EOF`,
+	},
+	{
+		"variable",
 		// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
 		"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
-		hasError, `unexpected ":="`},
-	{"multidecl",
+		hasError, `unexpected ":="`,
+	},
+	{
+		"multidecl",
 		"{{$a,$b,$c := 23}}",
-		hasError, `too many declarations`},
-	{"undefvar",
+		hasError, `too many declarations`,
+	},
+	{
+		"undefvar",
 		"{{$a}}",
-		hasError, `undefined variable`},
-	{"wrongdot",
+		hasError, `undefined variable`,
+	},
+	{
+		"wrongdot",
 		"{{true.any}}",
-		hasError, `unexpected . after term`},
-	{"wrongpipeline",
+		hasError, `unexpected . after term`,
+	},
+	{
+		"wrongpipeline",
 		"{{12|false}}",
-		hasError, `non executable command in pipeline`},
-	{"emptypipeline",
+		hasError, `non executable command in pipeline`,
+	},
+	{
+		"emptypipeline",
 		`{{ ( ) }}`,
-		hasError, `missing value for parenthesized pipeline`},
-	{"multilinerawstring",
+		hasError, `missing value for parenthesized pipeline`,
+	},
+	{
+		"multilinerawstring",
 		"{{ $v := `\n` }} {{",
-		hasError, `multilinerawstring:2: unclosed action`},
-	{"rangeundefvar",
+		hasError, `multilinerawstring:2: unclosed action`,
+	},
+	{
+		"rangeundefvar",
 		"{{range $k}}{{end}}",
-		hasError, `undefined variable`},
-	{"rangeundefvars",
+		hasError, `undefined variable`,
+	},
+	{
+		"rangeundefvars",
 		"{{range $k, $v}}{{end}}",
-		hasError, `undefined variable`},
-	{"rangemissingvalue1",
+		hasError, `undefined variable`,
+	},
+	{
+		"rangemissingvalue1",
 		"{{range $k,}}{{end}}",
-		hasError, `missing value for range`},
-	{"rangemissingvalue2",
+		hasError, `missing value for range`,
+	},
+	{
+		"rangemissingvalue2",
 		"{{range $k, $v := }}{{end}}",
-		hasError, `missing value for range`},
-	{"rangenotvariable1",
+		hasError, `missing value for range`,
+	},
+	{
+		"rangenotvariable1",
 		"{{range $k, .}}{{end}}",
-		hasError, `range can only initialize variables`},
-	{"rangenotvariable2",
+		hasError, `range can only initialize variables`,
+	},
+	{
+		"rangenotvariable2",
 		"{{range $k, 123 := .}}{{end}}",
-		hasError, `range can only initialize variables`},
+		hasError, `range can only initialize variables`,
+	},
 }
 
 func TestErrors(t *testing.T) {
diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go
index 41257912d20..a8599bbade6 100644
--- a/tpl/tplimpl/tplimpl_integration_test.go
+++ b/tpl/tplimpl/tplimpl_integration_test.go
@@ -116,6 +116,20 @@ counter2: 3
 `)
 }
 
+func TestGo23ElseWith(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+title = "Hugo"
+-- layouts/index.html --
+{{ with false }}{{ else with .Site }}{{ .Title }}{{ end }}|
+`
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "Hugo|")
+}
+
 // Issue 10495
 func TestCommentsBeforeBlockDefinition(t *testing.T) {
 	t.Parallel()