Skip to content

tpl/tplimpl: Add error handling to embedded link/image render hooks #14108

@gregjotau

Description

@gregjotau

I want to run in a --strict mode that fails if I have:

  1. Unused params
  2. Unused frontmatter keys
  3. broken links (either to images or other articles), at least internal links
  4. unused i18n keys (https://stackoverflow.com/questions/78436641/unused-i18n-keys-and-duplicate-keys-in-hugo)

And possibly other issues with the code that are most likely unintentional.

I also find the existing solutions to failing on internal links unsatisfactory. I'm not sure if GPT-5 is providing the best solution, but I don't want to make a file manually in all hugo repos to make invalid links fail the build, that should ideally be provided by the framework.

GPT-5:

Short answer:

  • You can’t reliably use ref inside Markdown link syntax like [text]({{< ref "..." >}}) anymore. Hugo calls the ref/relref shortcodes “obsolete for Markdown” and recommends the embedded link render hook instead. ([gohugo.io]1)
  • You can fail the build for Markdown links (and images) that point to missing internal targets by adding link/image render hooks that errorf when resolution fails. This runs at render time, before the site is written. ([gohugo.io]2)
  • For external URLs (http/https), Hugo won’t fetch them during render. Use a post-build checker (e.g., Lychee/Muffet) in CI to fail on 404s. ([Well-Shaped Words]3)

Make Markdown links fail the build (internal only)

1) Enable/use the embedded hooks

# hugo.toml
[markup.goldmark.renderHooks.link]
useEmbedded = "always"   # auto|fallback|never|always

[markup.goldmark.renderHooks.image]
useEmbedded = "always"

This lets Hugo resolve Markdown destinations the same way it resolves ref/relref. ([gohugo.io]2)

2) Add render-link.html that validates + errors

layouts/_default/_markup/render-link.html:

{{- $u := urls.Parse .Destination -}}
{{- $isAbs := or (ne $u.Scheme "") (strings.HasPrefix .Destination "//") -}}

{{- if not $isAbs -}}
  {{/* Try to resolve internal target: page → page resource → global resource */}}
  {{- $page := site.GetPage $u.Path | default (.Page.GetPage $u.Path) -}}
  {{- $res  := cond (not $page) (resources.Get $u.Path) nil -}}

  {{- if not (or $page $res) -}}
    {{- errorf "Broken internal link %q on %s" .Destination .Page.File.Path -}}
  {{- end -}}

  {{- if and $page $u.Fragment -}}
    {{- if not ($page.Fragments.Identifiers.Contains $u.Fragment) -}}
      {{- errorf "Broken fragment #%s in link %q on %s" $u.Fragment .Destination .Page.File.Path -}}
    {{- end -}}
  {{- end -}}
{{- end -}}

<a href="{{ .Destination | safeURL }}">{{ .Text | safeHTML }}</a>

Notes:

  • .Fragments.Identifiers.Contains gives you reliable, GitHub-style heading id checks. ([gohugo.io]4)
  • Use warnf instead of errorf if you want warnings, then combine with --panicOnWarning to fail the build. ([HUGO]5)

3) Do the same for images

layouts/_default/_markup/render-image.html:

{{- $u := urls.Parse .Destination -}}
{{- $isAbs := or (ne $u.Scheme "") (strings.HasPrefix .Destination "//") -}}

{{- if not $isAbs -}}
  {{- $img := .Page.Resources.GetMatch $u.Path | default (resources.Get $u.Path) -}}
  {{- if not $img -}}
    {{- errorf "Broken internal image %q on %s" .Destination .Page.File.Path -}}
  {{- end -}}
{{- end -}}

<img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />

This catches missing page/global resources referenced via Markdown images. ([gohugo.io]6)


What about external links/images?

Hugo’s render hooks don’t fetch remote URLs, so they can’t know if https://… returns 404. Run a link checker after hugo in CI and fail if any external links are dead (e.g., Lychee or Muffet). ([Well-Shaped Words]3)


TL;DR

  • Don’t embed ref/relref inside [](); use Markdown links plus render hooks. ([gohugo.io]1)
  • With the above hooks, any unresolved internal link or fragment will error out and fail the build. ([gohugo.io]2)
  • Use a post-build checker for external URLs. ([Well-Shaped Words]3)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions