Hi, I was debugging an issue with geekblog theme thegeeklab/hugo-geekblog#827 and came across what's potentially an issue with hugo itself? The following bug report is generated by Gemini, but after I made it discover the exact issue in this conversation. I've verified the content of this bug report and I stand by it except of course I do not know if this is intended behavior. I'll create a PR with a suggested fix in case this is unintended behavior.
What version of Hugo are you using (hugo version)?
$ hugo version
hugo v0.159.2 linux/amd64 BuildDate=unknown
Does this issue reproduce with the latest release?
Yes. Reproduced with CGO_ENABLED=0 go install github.com/gohugoio/hugo@latest.
Description
When using {{ with partial "name" . }} inside a <script> tag (such as when building JSON-LD schema), if the partial evaluates to an empty string, Hugo renders a literal pair of double-quotes ("") instead of rendering nothing.
This happens because Hugo's internal AST transformer (tplimpl) secretly injects an else block into with partial statements, which collides with Go's html/template context-aware JavaScript escaper.
The Bug Mechanism
In tplimpl/template.go, the function handleWithPartial intercepts with partial calls to manage the partial decorator stack. It automatically appends a fallback else branch that calls _popPartialDecorator:
// From tplimpl/template.go -> handleWithPartial()
// When the partial returns a falsy value, the with block is skipped,
// but we still need to pop the decorator ID from the stack.
// Add a pop call to the else branch.
popElse := popPartialDecoratorElse.CopyList()
// ...
if withNode.ElseList == nil {
withNode.ElseList = popElse // <-- INJECTED ELSE BLOCK
}
The function that this else block executes is defined in the init() function, and it returns a primitive Go string:
"_popPartialDecorator": func(string) string { return "" }
The Collision:
- We call
{{ with partial "empty" . }} inside a <script> tag.
- The partial returns
"". The with condition evaluates as falsy.
- The engine falls back to the injected
else block, executing _popPartialDecorator, which yields the primitive string "".
- Because the engine is inside a
<script> tag (stateJS), Go's html/template engine aggressively escapes the output to ensure it forms a valid JS token. It passes the "" through jsValEscaper, which explicitly wraps it in byte quotes, yielding the literal output "".
Note: This bug is bypassed entirely if the partial is evaluated into a variable first ({{ $var := partial ... }}) because Hugo's AST parser only injects the else block if the condition is a direct partial call.
Minimal Reproduction
Create a completely barebones site with just these two files:
layouts/partials/empty.html
layouts/index.html
<script>
{{- with partial "empty" . }}
"this-should-be-skipped": "{{ . }}"
{{- end }}
</script>
Expected Output:
The block evaluates to empty, so nothing should be printed inside the script tag.
Actual Output (The Bug):
The literal string quotes from the injected else block are forced into the template by the JS escaper.
Hi, I was debugging an issue with geekblog theme thegeeklab/hugo-geekblog#827 and came across what's potentially an issue with hugo itself? The following bug report is generated by Gemini, but after I made it discover the exact issue in this conversation. I've verified the content of this bug report and I stand by it except of course I do not know if this is intended behavior. I'll create a PR with a suggested fix in case this is unintended behavior.
What version of Hugo are you using (
hugo version)?Does this issue reproduce with the latest release?
Yes. Reproduced with
CGO_ENABLED=0 go install github.com/gohugoio/hugo@latest.Description
When using
{{ with partial "name" . }}inside a<script>tag (such as when building JSON-LD schema), if the partial evaluates to an empty string, Hugo renders a literal pair of double-quotes ("") instead of rendering nothing.This happens because Hugo's internal AST transformer (
tplimpl) secretly injects anelseblock intowith partialstatements, which collides with Go'shtml/templatecontext-aware JavaScript escaper.The Bug Mechanism
In
tplimpl/template.go, the functionhandleWithPartialinterceptswith partialcalls to manage the partial decorator stack. It automatically appends a fallbackelsebranch that calls_popPartialDecorator:The function that this
elseblock executes is defined in theinit()function, and it returns a primitive Gostring:The Collision:
{{ with partial "empty" . }}inside a<script>tag."". Thewithcondition evaluates as falsy.elseblock, executing_popPartialDecorator, which yields the primitive string"".<script>tag (stateJS), Go'shtml/templateengine aggressively escapes the output to ensure it forms a valid JS token. It passes the""throughjsValEscaper, which explicitly wraps it in byte quotes, yielding the literal output"".Note: This bug is bypassed entirely if the partial is evaluated into a variable first (
{{ $var := partial ... }}) because Hugo's AST parser only injects theelseblock if the condition is a direct partial call.Minimal Reproduction
Create a completely barebones site with just these two files:
layouts/partials/empty.html{{ return "" }}layouts/index.htmlExpected Output:
The block evaluates to empty, so nothing should be printed inside the script tag.
Actual Output (The Bug):
The literal string quotes from the injected
elseblock are forced into the template by the JS escaper.