Repository: gohugoio/hugo (language: go)

Issue:
Template execution fails if no suffix is given

<!-- Please answer these questions before submitting your issue. Thanks! -->

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.146.3+extended+withdeploy darwin/arm64 BuildDate=2025-04-12T17:21:50Z VendorInfo=brew
</pre>

### Does this issue reproduce with the latest release?

Yes

## Description

Up until 0.145 it was possible to call a partial without an file extension from another. My use case were partials 
 that act as output format agnostic templates function, those might return just strings.
To make it obvious that those aren't really bound to formats, I just used them without any file extension, see `default-lang` in the example. This wont work anymore with the new templating (at least on existing sites).

In short: Executing a template (in this case for output format HTML) that doesn't have the file suffix `.html` fails now.

## Message
```
Error: error building site: render: failed to render pages: render of "/Users/cmahnke/Blogs/hugo-0.146.3/content/post/post-1.md" failed: "/Users/cmahnke/Blogs/hugo-0.146.3/layouts/_default/single.html:4:32": execute of template failed: template: single.html:4:32: executing "main" at <partialCached "functions/i18n/default-lang" .>: error calling partialCached: template: :1:29: executing "_partials/functions/i18n/default-lang" at <.Arg>: can't evaluate field Arg in type *hugolib.pageState
```

## Example

https://github.com/cmahnke/hugo-13601

# Updates
* It seems to be not an issue of template resolution, since there issn't a message that the template wasn't found.
* Since it used to work with 0.145.0 it's related to backwards compatibility similar to #13599.
* Renaming the template to have a suffix (`.html`) makes it work, even if I don't change the caller. My expectation would have been that this should fail, since the template file don't actually exist under the given file name.


Discussion / hints:
> Executing a template (in this case for output format HTML) that doesn't have the file suffix .html fails now

I was not able to reproduce this:

```text
git clone --single-branch -b hugo-github-issue-13601 https://github.com/jmooring/hugo-testing hugo-github-issue-13601
cd hugo-github-issue-13601
hugo
```

In the example above, `single.html` calls `partials/foo.html` which calls `partials/bar`... no problems.

But the behavior _has_ changed...

#### v0.145.0

template path|template call|notes
:--|:--|:--
`layouts/partials/foo.html`|`{{ partial "foo.html" . }}`|this works
`layouts/partials/foo.html`|`{{ partial "foo" . }}`|this works (provides backwards compatibility)
`layouts/partials/foo`|`{{ partial "foo.html" . }}`|this throws an error (template not found)
`layouts/partials/foo`|`{{ partial "foo" . }}`|this works

#### v0.146.0 

template path|template call|notes
:--|:--|:--
`layouts/partials/foo.html`|`{{ partial "foo.html" . }}`|this works
`layouts/partials/foo.html`|`{{ partial "foo" . }}`|this works (provides backwards compatibility)
`layouts/partials/foo`|`{{ partial "foo.html" . }}`|this works (<mark>new behavior</mark>)
`layouts/partials/foo`|`{{ partial "foo" . }}`|this works

You get the same result in v0.146.0 using `layouts/_partials` instead of `layouts/partials`.

As you know, partial templates are agnostic with respect to language and output format. So if you needed both HTML and JSON:

```
layouts/
└── _partials/
    ├── foo.html
    └── foo.json

{{ partial "foo.html" . }}
{{ partial "foo.json" . }}
```

If you have a partial template you intended to use with _all_ output formats, and you want to visually indicate your intent, use a different extension, the value of which is irrelevant.

```text
layouts/
└── _partials/
    ├── foo.agnostic <-- extension is irrelevant
    ├── foo.html
    └── foo.json

{{ partial "foo.html" . }}
{{ partial "foo.json" . }}
{{ partial "foo.agnositic" . }}
```










@cmahnke Also, your test site worked fine for me:

![Image](https://github.com/user-attachments/assets/598bc5ad-4abf-4d0b-8e09-707f997b9b6c)

Note the "en" at the end.
Thanks @jmooring for having a look. I still see the issue but get what you mean, it's very strage:
Running:
```
seq 5 |xargs -Iz hugo
```
gives:
```
Start building sites … 
hugo v0.146.3+extended+withdeploy darwin/arm64 BuildDate=2025-04-12T17:21:50Z VendorInfo=brew


                   | EN  
-------------------+-----
  Pages            | 10  
  Paginator pages  |  0  
  Non-page files   |  0  
  Static files     |  0  
  Processed images |  0  
  Aliases          |  0  
  Cleaned          |  0  

Total in 9 ms
Start building sites … 
hugo v0.146.3+extended+withdeploy darwin/arm64 BuildDate=2025-04-12T17:21:50Z VendorInfo=brew

Total in 8 ms
Error: error building site: render: failed to render pages: render of "/Users/cmahnke/Blogs/hugo-0.146.3/content/post/post-1.md" failed: "/Users/cmahnke/Blogs/hugo-0.146.3/layouts/_default/single.html:4:32": execute of template failed: template: single.html:4:32: executing "main" at <partialCached "functions/i18n/default-lang" .>: error calling partialCached: template: :1:29: executing "_partials/functions/i18n/default-lang" at <.Arg>: can't evaluate field Arg in type *hugolib.pageState
Start building sites … 
hugo v0.146.3+extended+withdeploy darwin/arm64 BuildDate=2025-04-12T17:21:50Z VendorInfo=brew

Total in 8 ms
Error: error building site: render: failed to render pages: render of "/Users/cmahnke/Blogs/hugo-0.146.3/content/post/post-1.md" failed: "/Users/cmahnke/Blogs/hugo-0.146.3/layouts/_default/single.html:4:32": execute of template failed: template: single.html:4:32: executing "main" at <partialCached "functions/i18n/default-lang" .>: error calling partialCached: template: :1:29: executing "_partials/functions/i18n/default-lang" at <.Arg>: can't evaluate field Arg in type *hugolib.pageState
Start building sites … 
hugo v0.146.3+extended+withdeploy darwin/arm64 BuildDate=2025-04-12T17:21:50Z VendorInfo=brew

Total in 8 ms
Error: error building site: render: failed to render pages: render of "/Users/cmahnke/Blogs/hugo-0.146.3/content/post/post-1.md" failed: "/Users/cmahnke/Blogs/hugo-0.146.3/layouts/_default/single.html:4:32": execute of template failed: template: single.html:4:32: executing "main" at <partialCached "functions/i18n/default-lang" .>: error calling partialCached: template: :1:29: executing "_partials/functions/i18n/default-lang" at <.Arg>: can't evaluate field Arg in type *hugolib.pageState
Start building sites … 
hugo v0.146.3+extended+withdeploy darwin/arm64 BuildDate=2025-04-12T17:21:50Z VendorInfo=brew

Total in 8 ms
Error: error building site: render: failed to render pages: render of "/Users/cmahnke/Blogs/hugo-0.146.3/content/post/post-1.md" failed: "/Users/cmahnke/Blogs/hugo-0.146.3/layouts/_default/single.html:4:32": execute of template failed: template: single.html:4:32: executing "main" at <partialCached "functions/i18n/default-lang" .>: error calling partialCached: template: :1:29: executing "_partials/functions/i18n/default-lang" at <.Arg>: can't evaluate field Arg in type *hugolib.pageState
```
OK, the failure is intermittent when the partial file does not have an extension, but works every time when it has an extension. So... give it an extension?

This is related to the `return` statement.
Well, yes, already did that. But it's still an strange result, maybe a race condition, that should be addressed. Or at least looked into. It might be an indicator for broken logic. But it has a lower priority right now.  
Thanks!

Just in case anyone else get's a:
```
at <.Arg>: can't evaluate field Arg in type *hugolib.pageState
```
See also https://discourse.gohugo.io/t/cant-evaluate-field-arg-since-0-146/54368
I will have a look at this. 

Fix patch:
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index 3ac0940e2cd..cc8a145d995 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -677,13 +677,7 @@ Loop:
 
 			// 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)
+			templ := s.s.TemplateStore.LookupShortcodeByName(sc.name)
 			if templ == nil {
 				return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
 			}
diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go
index 4c3ad3be1ab..19de48e3801 100644
--- a/tpl/tplimpl/templates.go
+++ b/tpl/tplimpl/templates.go
@@ -44,7 +44,16 @@ var embeddedTemplatesAliases = map[string][]string{
 	"_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
 }
 
-func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
+func (s *TemplateStore) parseTemplate(ti *TemplInfo) error {
+	err := s.tns.doParseTemplate(ti)
+	if err != nil {
+		return s.addFileContext(ti, "parse of template failed", err)
+	}
+
+	return err
+}
+
+func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error {
 	if !ti.noBaseOf || ti.category == CategoryBaseof {
 		// Delay parsing until we have the base template.
 		return nil
@@ -99,7 +108,14 @@ func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
 					return err
 				}
 			}
+		}
 
+		// Issue #13599.
+		if ti.category == CategoryPartial && ti.Fi != nil && ti.Fi.Meta().PathInfo.Section() == "partials" {
+			aliasName := strings.TrimPrefix(name, "_")
+			if _, err := prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree); err != nil {
+				return err
+			}
 		}
 	}
 
diff --git a/tpl/tplimpl/templatetransform.go b/tpl/tplimpl/templatetransform.go
index cba4c6584f9..eca9fdad1d9 100644
--- a/tpl/tplimpl/templatetransform.go
+++ b/tpl/tplimpl/templatetransform.go
@@ -175,6 +175,9 @@ func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, err
 		}
 
 	case *parse.CommandNode:
+		if x == nil {
+			return true, nil
+		}
 		c.collectInner(x)
 		keep := c.collectReturnNode(x)
 


Test patch:
diff --git a/tpl/templates/templates_integration_test.go b/tpl/templates/templates_integration_test.go
index 93922b4c42b..d16333ed43e 100644
--- a/tpl/templates/templates_integration_test.go
+++ b/tpl/templates/templates_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.
@@ -17,6 +17,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/hugolib"
 )
 
@@ -229,3 +230,72 @@ layouts/section/section.html
 	b := hugolib.Test(t, files)
 	b.AssertFileContent("public/mysection/index.html", "layouts/section/section.html")
 }
+
+func TestErrorMessageParseError(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/home.html --
+Line 1.
+Line 2. {{ foo }} <- this func does not exist.
+Line 3.
+`
+
+	b, err := hugolib.TestE(t, files)
+	b.Assert(err, qt.IsNotNil)
+	b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`"/layouts/home.html:2:1": parse of template failed: template: home.html:2: function "foo" not defined`))
+}
+
+func TestErrorMessageExecuteError(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/home.html --
+Line 1.
+Line 2. {{ .Foo }} <- this method does not exist.
+Line 3.
+`
+
+	b, err := hugolib.TestE(t, files)
+	b.Assert(err, qt.IsNotNil)
+	b.Assert(err.Error(), qt.Contains, filepath.FromSlash(` "/layouts/home.html:2:11": execute of template failed`))
+}
+
+func TestPartialReturnPanicIssue13600(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/home.html --
+Partial: {{ partial "p1.html" . }}
+-- layouts/_partials/p1.html --
+P1.
+{{ return ( delimit . ", " ) | string }}
+`
+
+	b, err := hugolib.TestE(t, files)
+	b.Assert(err, qt.IsNotNil)
+	b.Assert(err.Error(), qt.Contains, "wrong number of args for string: want 1 got 0")
+}
+
+func TestPartialWithoutSuffixIssue13601(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/home.html --
+P1: {{ partial "p1" . }}
+P2: {{ partial "p2" . }}
+-- layouts/_partials/p1 --
+P1.
+-- layouts/_partials/p2 --
+P2.
+{{ return "foo bar" }}
+
+`
+
+	b := hugolib.Test(t, files)
+	b.AssertFileContent("public/index.html", "P1: P1.\nP2: foo bar")
+}
diff --git a/tpl/tplimpl/legacy_integration_test.go b/tpl/tplimpl/legacy_integration_test.go
new file mode 100644
index 00000000000..a96e35fca10
--- /dev/null
+++ b/tpl/tplimpl/legacy_integration_test.go
@@ -0,0 +1,38 @@
+// 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 tplimpl_test
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+func TestLegacyPartialIssue13599(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/partials/mypartial.html --
+Mypartial.
+-- layouts/_default/index.html --
+mypartial:   {{ template "partials/mypartial.html" . }}
+
+`
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "Mypartial.")
+}
diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go
index 8483b7df033..bb58c90831e 100644
--- a/tpl/tplimpl/templatestore.go
+++ b/tpl/tplimpl/templatestore.go
@@ -1,3 +1,18 @@
+// 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 tplimpl
 
 import (
@@ -107,6 +122,7 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
 		treeMain:            doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
 		treeShortcodes:      doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
 		templatesByPath:     maps.NewCache[string, *TemplInfo](),
+		shortcodesByName:    maps.NewCache[string, *TemplInfo](),
 		cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
 
 		// Note that the funcs passed below is just for name validation.
@@ -404,9 +420,10 @@ type TemplateStore struct {
 	siteOpts   SiteOptions
 	htmlFormat output.Format
 
-	treeMain        *doctree.SimpleTree[map[nodeKey]*TemplInfo]
-	treeShortcodes  *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
-	templatesByPath *maps.Cache[string, *TemplInfo]
+	treeMain         *doctree.SimpleTree[map[nodeKey]*TemplInfo]
+	treeShortcodes   *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
+	templatesByPath  *maps.Cache[string, *TemplInfo]
+	shortcodesByName *maps.Cache[string, *TemplInfo]
 
 	dh descriptorHandler
 
@@ -480,7 +497,7 @@ func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, w
 
 	execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data)
 	if execErr != nil {
-		return t.addFileContext(ti, execErr)
+		return t.addFileContext(ti, "execute of template failed", execErr)
 	}
 	return nil
 }
@@ -561,6 +578,15 @@ func (s *TemplateStore) LookupPartial(pth string) *TemplInfo {
 	return ti
 }
 
+func (s *TemplateStore) LookupShortcodeByName(name string) *TemplInfo {
+	name = strings.ToLower(name)
+	ti, _ := s.shortcodesByName.Get(name)
+	if ti == nil {
+		return nil
+	}
+	return ti
+}
+
 func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
 	q.init()
 	k1 := s.key(q.Path)
@@ -807,7 +833,7 @@ func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *pa
 	return nil
 }
 
-func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error {
+func (s *TemplateStore) addFileContext(ti *TemplInfo, what string, inerr error) error {
 	if ti.Fi == nil {
 		return inerr
 	}
@@ -839,25 +865,27 @@ func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error {
 		fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename)
 		fe.UpdateContent(f, lineMatcher)
 
-		if !fe.ErrorContext().Position.IsValid() {
-			return inErr, false
-		}
-		return fe, true
+		return fe, fe.ErrorContext().Position.IsValid()
 	}
 
-	inerr = fmt.Errorf("execute of template failed: %w", inerr)
+	inerr = fmt.Errorf("%s: %w", what, inerr)
 
-	if err, ok := checkFilename(ti.Fi, inerr); ok {
-		return err
+	var (
+		currentErr error
+		ok         bool
+	)
+
+	if currentErr, ok = checkFilename(ti.Fi, inerr); ok {
+		return currentErr
 	}
 
 	if ti.base != nil {
-		if err, ok := checkFilename(ti.base.Fi, inerr); ok {
-			return err
+		if currentErr, ok = checkFilename(ti.base.Fi, inerr); ok {
+			return currentErr
 		}
 	}
 
-	return inerr
+	return currentErr
 }
 
 func (s *TemplateStore) extractIdentifiers(line string) []string {
@@ -885,7 +913,7 @@ func (s *TemplateStore) extractInlinePartials() error {
 			name := templ.Name()
 			if !paths.HasExt(name) {
 				// Assume HTML. This in line with how the lookup works.
-				name = name + ".html"
+				name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix
 			}
 			if !strings.HasPrefix(name, "_") {
 				name = "_" + name
@@ -1022,6 +1050,7 @@ func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo,
 
 	m1[d] = ti
 
+	s.shortcodesByName.Set(k2, ti)
 	s.setTemplateByPath(pi.Path(), ti)
 
 	if fi != nil {
@@ -1063,6 +1092,12 @@ func (s *TemplateStore) insertTemplate2(
 		panic("category not set")
 	}
 
+	if category == CategoryPartial && d.OutputFormat == "" && d.MediaType == "" {
+		// See issue #13601.
+		d.OutputFormat = s.htmlFormat.Name
+		d.MediaType = s.htmlFormat.MediaType.Type
+	}
+
 	m := tree.Get(key)
 	nk := nodeKey{c: category, d: d}
 
@@ -1374,7 +1409,7 @@ func (s *TemplateStore) parseTemplates() error {
 				if vv.state == processingStateTransformed {
 					continue
 				}
-				if err := s.tns.parseTemplate(vv); err != nil {
+				if err := s.parseTemplate(vv); err != nil {
 					return err
 				}
 			}
@@ -1394,7 +1429,7 @@ func (s *TemplateStore) parseTemplates() error {
 						// 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 {
+						if err := s.parseTemplate(vv); err != nil {
 							return err
 						}
 						continue
@@ -1423,7 +1458,7 @@ func (s *TemplateStore) parseTemplates() error {
 				if vvv.state == processingStateTransformed {
 					continue
 				}
-				if err := s.tns.parseTemplate(vvv); err != nil {
+				if err := s.parseTemplate(vvv); err != nil {
 					return err
 				}
 			}
@@ -1690,6 +1725,7 @@ func (s *TemplateStore) transformTemplates() error {
 			continue
 		}
 		if !vv.noBaseOf {
+			// TODO(bep) I don't think this branch is ever called.
 			for vvv := range vv.BaseVariantsSeq() {
 				tctx, err := applyTemplateTransformers(vvv.Template, lookup)
 				if err != nil {
diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go
index 4644c963958..b302f5bb305 100644
--- a/tpl/tplimpl/templatestore_integration_test.go
+++ b/tpl/tplimpl/templatestore_integration_test.go
@@ -1127,6 +1127,28 @@ single.html
 	b.AssertFileContent("public/s3/index.html", "single.html") // fail
 }
 
+func TestIssue13605(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+-- content/s1/p1.md --
+---
+title: p1
+---
+{{< sc >}}
+-- layouts/s1/_shortcodes/sc.html --
+layouts/s1/_shortcodes/sc.html
+-- layouts/single.html --
+{{ .Content }}
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/_shortcodes/sc.html")
+}
+
 func TestSkipDotFiles(t *testing.T) {
 	t.Parallel()
 
diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go
index 8b80d5b6072..b628989237a 100644
--- a/tpl/tplimpl/tplimpl_integration_test.go
+++ b/tpl/tplimpl/tplimpl_integration_test.go
@@ -1,3 +1,18 @@
+// 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 tplimpl_test
 
 import (