{"id":"cf3e609e957a5b5e","repo":"gohugoio/hugo","slug":"text-is-already-rendered-repeating-it-may-cause-i","errorCode":null,"errorMessage":"text is already rendered, repeating it may cause infinite recursion","messagePattern":"text is already rendered, repeating it may cause infinite recursion","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hugolib/page__content.go","lineNumber":912,"sourceCode":"\t\tm, ok := args[0].(map[string]any)\n\t\tif !ok {\n\t\t\treturn \"\", errors.New(\"first argument must be a map\")\n\t\t}\n\n\t\tif err := mapstructure.WeakDecode(m, &opts); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to decode options: %w\", err)\n\t\t}\n\t\tif opts.Markup != \"\" {\n\t\t\topts.Markup = markup.ResolveMarkup(opts.Markup)\n\t\t}\n\t}\n\n\tcontentToRenderv := args[sidx]\n\n\tif _, ok := contentToRenderv.(hstring.HTML); ok {\n\t\t// This content is already rendered, this is potentially\n\t\t// a infinite recursion.\n\t\treturn \"\", errors.New(\"text is already rendered, repeating it may cause infinite recursion\")\n\t}\n\n\tvar err error\n\tcontentToRender, err = cast.ToStringE(contentToRenderv)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = pco.initRenderHooks(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconv := pco.po.p.getContentConverter()\n\n\tif opts.Markup != \"\" && opts.Markup != pco.po.p.m.pageConfigSource.ContentMediaType.SubType {\n\t\tvar err error\n\t\tconv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)\n\t\tif err != nil {","sourceCodeStart":894,"sourceCodeEnd":930,"githubUrl":"https://github.com/gohugoio/hugo/blob/8a468df065a75c1c7cf9f6850f32148746590ea5/hugolib/page__content.go#L894-L930","documentation":"Raised in RenderString (page__content.go) when the value passed in is already of type hstring.HTML — i.e. output that Hugo's own render pipeline produced (such as the result of a render hook's .Text or a previous RenderString). Re-rendering already-rendered HTML is at best a no-op and at worst causes infinite recursion (a render hook calling RenderString on its own output), so Hugo refuses.","triggerScenarios":"A markdown render hook (e.g. layouts/_markup/render-link.html) passing `.Text` (already-rendered HTML) into `.Page.RenderString`; templates piping the result of one RenderString/markdownify call into another RenderString.","commonSituations":"Render-hook authors trying to post-process link/heading text with RenderString; recursive shortcode/hook chains introduced when porting themes to newer Hugo versions where .Text became typed HTML; double-processing front-matter fields once through markdownify and again through RenderString.","solutions":["Don't re-render: use the already-rendered value directly, e.g. `{{ .Text }}` in the render hook.","If you genuinely need to re-process, work from the raw source instead — e.g. `.PlainText` in a render hook, or the original front-matter string — and pass that to RenderString.","Convert explicitly only when you understand the recursion risk: `{{ .Page.RenderString (string $v) }}` forces plain-string handling."],"exampleFix":"{{/* layouts/_markup/render-link.html before */}}\n<a href=\"{{ .Destination }}\">{{ .Page.RenderString .Text }}</a>\n{{/* after */}}\n<a href=\"{{ .Destination }}\">{{ .Text }}</a>","handlingStrategy":"validation","validationCode":"{{/* never feed already-rendered content back into RenderString */}}\n{{ $raw := .RawContent }}\n{{ $out := .RenderString $raw }} {{/* OK: raw, not .Content */}}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pass raw/plain strings to RenderString, never .Content or another RenderString result","In shortcodes, use .Inner or .InnerDeindent, not the page's rendered content","Treat this error as a recursion guard: find where rendered output re-enters the pipeline"],"tags":["hugo","renderstring","render-hooks","recursion","templates"],"analyzedSha":"8a468df065a75c1c7cf9f6850f32148746590ea5","analyzedAt":"2026-07-31T21:23:07.045Z","schemaVersion":2}