gohugoio/hugo · error
: shortcode must be closed or self-closed
Error message
%s: shortcode %q must be closed or self-closed
What it means
A shortcode that expects inner content (its template evaluates .Inner or .InnerDeindent, i.e. needsInner() is true) was opened but never closed, nor was it self-closed. Hugo requires such shortcodes to appear as a matched pair {{< name >}}...{{< /name >}} or as a self-closing tag {{< name />}}.
Solutions
- Add the matching closing tag {{< /shortcodeName >}} after the inner content.
- If the shortcode should not wrap content, use the self-closing form {{< shortcodeName />}}.
- Verify the shortcode template truly needs inner content; if it does not use .Inner, the opening/closing pairing is unnecessary.
- Search the reported content file for the shortcode name to locate the unbalanced tag.
Example fix
// before
{{< highlight go >}}
func main() {}
// after
{{< highlight go >}}
func main() {}
{{< /highlight >}} Defensive patterns
Strategy: validation
Validate before calling
// Lint content for unclosed wrapping shortcodes by pairing open/close tokens.
func validateShortcodeBalance(tokens []scToken) error {
var stack []string
for _, t := range tokens {
switch {
case t.isOpen():
if !t.isSelfClosing() { stack = append(stack, t.name) }
case t.isClose():
if len(stack) == 0 || stack[len(stack)-1] != t.name {
return fmt.Errorf("unbalanced closing tag %q", t.name)
}
stack = stack[:len(stack)-1]
}
}
if len(stack) > 0 {
return fmt.Errorf("shortcode %q must be closed or self-closed", stack[len(stack)-1])
}
return nil
} Prevention
- Always close wrapping shortcodes; prefer self-closing form when no inner content is needed.
- Use an editor/CI linter that checks shortcode tag balance.
- Keep a short list of shortcodes that require closing tags next to your content guide.
When it happens
Trigger: Writing {{< highlight go >}} without the corresponding {{< /highlight >}}, or forgetting the trailing slash on a self-closing form. The check at shortcode.go:710 fires when parsing reaches the IsDone state while closed==false and sc.needsInner()==true.
Common situations: Closing a code block with ``` but forgetting {{< /highlight >}}; renaming a shortcode and leaving an orphan opening tag; content pasted from a Markdown source that stripped the closing tag.
Related errors
- failed to process shortcode
- no compatible template found for shortcode
- : invalid state: invalid param type %T for shortcode …
- : invalid state: invalid param type %T for shortcode …
- argument must be a slice
AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09).
Data as JSON: /api/errors/d1875dac9d7c1f94.
Report an issue: GitHub.
Appendix: source
Thrown at hugolib/shortcode.go:711
} else {
// positional params
if sc.params == nil {
var params []any
params = append(params, currItem.ValTyped(source))
sc.params = params
} else {
if params, ok := sc.params.([]any); ok {
params = append(params, currItem.ValTyped(source))
sc.params = params
} else {
return sc, fmt.Errorf("%s: invalid state: invalid param type %T for shortcode %q, expected a slice", errorPrefix, params, sc.name)
}
}
}
case currItem.IsDone():
if !currItem.IsError() {
if !closed && sc.needsInner() {
return sc, fmt.Errorf("%s: shortcode %q must be closed or self-closed", errorPrefix, sc.name)
}
}
// handled by caller
pt.Backup()
break Loop
}
}
return sc, nil
}
// Replace prefixed shortcode tokens with the real content.
// Note: This function will rewrite the input slice.
func expandShortcodeTokens(
ctx context.Context,
source []byte,
tokenHandler func(ctx context.Context, token string) ([]byte, error),
) ([]byte, error) {View on GitHub (pinned to 52c9bd7908)