{"id":"017ee1141d3d70dc","repo":"gohugoio/hugo","slug":"ellipsis-must-be-a-string","errorCode":null,"errorMessage":"ellipsis must be a string","messagePattern":"ellipsis must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tpl/strings/truncate.go","lineNumber":62,"sourceCode":"func (ns *Namespace) Truncate(s any, options ...any) (template.HTML, error) {\n\tlength, err := cast.ToIntE(s)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar textParam any\n\tvar ellipsis string\n\n\tswitch len(options) {\n\tcase 0:\n\t\treturn \"\", errors.New(\"truncate requires a length and a string\")\n\tcase 1:\n\t\ttextParam = options[0]\n\t\tellipsis = \" …\"\n\tcase 2:\n\t\ttextParam = options[1]\n\t\tellipsis, err = cast.ToStringE(options[0])\n\t\tif err != nil {\n\t\t\treturn \"\", errors.New(\"ellipsis must be a string\")\n\t\t}\n\t\tif _, ok := options[0].(template.HTML); !ok {\n\t\t\tellipsis = html.EscapeString(ellipsis)\n\t\t}\n\tdefault:\n\t\treturn \"\", errors.New(\"too many arguments passed to truncate\")\n\t}\n\n\ttext, err := cast.ToStringE(textParam)\n\tif err != nil {\n\t\treturn \"\", errors.New(\"text must be a string\")\n\t}\n\n\t_, isHTML := textParam.(template.HTML)\n\n\tif utf8.RuneCountInString(text) <= length {\n\t\tif isHTML {\n\t\t\treturn template.HTML(text), nil","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/gohugoio/hugo/blob/8a468df065a75c1c7cf9f6850f32148746590ea5/tpl/strings/truncate.go#L44-L80","documentation":"In the three-argument form `truncate LENGTH ELLIPSIS TEXT`, the middle argument is cast to a string with `cast.ToStringE`. If that value isn't string-convertible (e.g. a map, slice, or page object), Hugo reports that the ellipsis must be a string. This often really means the arguments are in the wrong order.","triggerScenarios":"`{{ truncate 30 .Pages .Summary }}` or any call where the second of three arguments is a non-string type; commonly `{{ truncate 30 .Summary … }}` variants where a non-string got swapped into the ellipsis slot.","commonSituations":"Swapped ellipsis/text argument order (text belongs last); passing a dict or slice from a partial's context; assuming ellipsis is optional-last rather than optional-middle.","solutions":["Put the ellipsis (a plain string) second and the text last: `{{ truncate 30 \"...\" .Summary }}`","If you don't need a custom ellipsis, use the two-argument form: `{{ truncate 30 .Summary }}` (default is \" …\")","Pass `template.HTML` ellipsis via `safeHTML` if you need unescaped markup: `{{ truncate 30 (\"<b>…</b>\" | safeHTML) .Content }}`"],"exampleFix":"// before\n{{ truncate 30 .Summary \"...\" }}\n// after\n{{ truncate 30 \"...\" .Summary }}","handlingStrategy":"type-guard","validationCode":"{{ $ell := default \" …\" .Params.ellipsis }}\n{{ if ne (printf \"%T\" $ell) \"string\" }}{{ $ell = string $ell }}{{ end }}\n{{ truncate 70 $ell .Summary }}","typeGuard":"{{ $ell := .Params.ellipsis }}{{ if $ell }}{{ $ell = string $ell }}{{ else }}{{ $ell = \" …\" }}{{ end }}","tryCatchPattern":"{{ with try (truncate 70 .ell .text) }}{{ if .Err }}{{ truncate 70 .text }}{{ else }}{{ .Value }}{{ end }}{{ end }}","preventionTips":["When passing three args to truncate, the middle one must be a string ellipsis — a number there means you meant it as length and misordered the call.","Coerce configurable ellipses with `string` since data-file values may be typed otherwise.","Rely on the default ellipsis (omit the argument) unless you genuinely need a custom one."],"tags":["hugo","templates","truncate","type-conversion","arguments"],"analyzedSha":"8a468df065a75c1c7cf9f6850f32148746590ea5","analyzedAt":"2026-07-31T21:23:07.045Z","schemaVersion":2}