gohugoio/hugo · error
startLevel
Error message
startLevel: %w
What it means
Returned by Fragments.ToHTML when cast.ToIntE(startLevel) fails, i.e. the startLevel argument cannot be converted to an int. startLevel is typed as `any`, so template authors can pass a non-numeric string and trigger this. The error wraps the cast error with %w so the original cast error is unwrappable.
Solutions
- Pass startLevel as an int literal or cast it: ToHTML (intVal, stop, ordered).
- If the value comes from params, coerce with int or a default: {{ $start := int (.Params.start_level | default 2) }}.
- Validate the param type in your template before calling ToHTML.
Example fix
{{/* before */}}
{{ .Fragments.ToHTML .Params.start_level .Params.stop_level false }}
{{/* after */}}
{{ .Fragments.ToHTML (int (.Params.start_level | default 2)) (int (.Params.stop_level | default 3)) false }} Defensive patterns
Strategy: type-guard
Validate before calling
// Coerce before calling ToHTML.
func toIntLevel(v any, def int) int {
switch t := v.(type) {
case int:
return t
case float64:
return int(t)
case string:
if n, err := strconv.Atoi(t); err == nil {
return n
}
}
return def
} Type guard
func isIntLike(v any) bool {
switch v.(type) {
case int, int64, float64:
return true
case string:
_, err := strconv.Atoi(v.(string))
return err == nil
}
return false
} Try / catch
html, err := frags.ToHTML(start, stop, ordered)
if err != nil && strings.HasPrefix(err.Error(), "startLevel:") {
html, err = frags.ToHTML(2, stop, ordered) // sane default
} Prevention
- Always pass integer literals/typed ints to ToHTML, never raw params.
- Coerce front-matter values in the template before calling ToHTML.
- Default startLevel to 2 to match DefaultConfig.
When it happens
Trigger: Calling .Fragments.ToHTML(...) (or the {{.TableOfContents}}-style path) with startLevel set to a non-integer string, e.g. ToHTML("abc", 3, false). Common via a template that interpolates a config value as a string instead of an int.
Common situations: A custom layout passing .Site.Params.toc.start_level (a string from front matter) straight into ToHTML; partials that receive parameters from a shortcode as strings; upgrading where an old template passed a stringified level.
Related errors
- stopLevel
- config needs a filename
- failed to convert content to string
- failed to convert s to string
- failed to convert substr to string
AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09).
Data as JSON: /api/errors/f986e8ff26d2a15e.
Report an issue: GitHub.
Appendix: source
Thrown at markup/tableofcontents/tableofcontents.go:158
for i := 1; i < level; i++ {
if len(heading.Headings) == 0 {
heading.Headings = append(heading.Headings, &Heading{})
}
heading = heading.Headings[len(heading.Headings)-1]
}
heading.Headings = append(heading.Headings, h)
}
// ToHTML renders the ToC as HTML.
func (toc *Fragments) ToHTML(startLevel, stopLevel any, ordered bool) (template.HTML, error) {
if toc == nil {
return "", nil
}
iStartLevel, err := cast.ToIntE(startLevel)
if err != nil {
return "", fmt.Errorf("startLevel: %w", err)
}
iStopLevel, err := cast.ToIntE(stopLevel)
if err != nil {
return "", fmt.Errorf("stopLevel: %w", err)
}
b := &tocBuilder{
s: strings.Builder{},
h: toc.Headings,
startLevel: iStartLevel,
stopLevel: iStopLevel,
ordered: ordered,
}
b.Build()
return template.HTML(b.s.String()), nil
}
View on GitHub (pinned to 52c9bd7908)