Hugo Render Hooks for Links, Images, and Headings
Hugo render hooks change the HTML emitted for ordinary Markdown. A site can add link attributes, route images through a shared partial, or supply heading anchors while authors keep writing normal content.
The detail worth protecting is the original element’s meaning. A hook that adds an external-link class but drops titles or breaks fragment links has traded one repeated task for a new site-wide problem.
Give Hugo render hooks one job each
A normal Markdown link already carries a destination, text, and an optional title:
[Documentation](https://example.org/docs/ "Project documentation")
The link render hook context
exposes those values as .Destination, .Text, and .Title. Place a link template in layouts/_markup/render-link.html to control that output.
Current Hugo lookup paths are listed in the official render hook documentation. Older examples often use layouts/_default/_markup/; check the lookup rules when moving an existing site’s templates.
A complete link hook can preserve the original values while classifying external HTTP links:
{{ $destination := urls.Parse .Destination }}
{{ $site := urls.Parse .Page.Site.BaseURL }}
{{ $isHTTP := in (slice "http" "https") $destination.Scheme }}
{{ $external := and (ne $destination.Host "") (ne $destination.Host $site.Host) }}
{{ $external = and $external (or $isHTTP (eq $destination.Scheme "")) }}
<a href="{{ .Destination | safeURL }}"
{{ with .Title }}title="{{ . }}"{{ end }}
{{ if $external }}class="external-link"{{ end }}
>{{ .Text | safeHTML }}</a>
urls.Parse
separates the destination’s scheme and host. The hook recognises another host on an HTTP URL, including a protocol-relative URL, rather than treating every string starting with http as external.
An absolute URL on the site’s own host remains internal. Relative paths, fragments, mailto:, and tel: destinations keep their ordinary output without an external-link class.
This is a host comparison, not a rule that every subdomain belongs to one site. Change that policy deliberately if the project treats several origins as one publishing boundary.
Preserve titles and leave new tabs optional
The with .Title block outputs the optional Markdown title. Attribute escaping remains Hugo’s responsibility, and .Text retains inline formatting in the visible link description.
safeHTML
marks the already rendered link text as HTML. It is not a sanitiser for arbitrary untrusted input; do not extend that assumption to user-supplied HTML without a separate trust policy.
The hook does not force links into new tabs. I prefer leaving that choice to the reader unless the interaction has a specific reason to open another context.
If a link needs target="_blank", rel="noopener"
makes opener isolation explicit. Adding noreferrer also suppresses referrer information, which is a separate choice.
An external marker can come from the generated class:
.external-link::after {
content: " ↗";
font-size: 0.8em;
}
Generated content is not guaranteed to be silent to assistive technology. If a decorative marker is announced unhelpfully, use an explicitly hidden decorative element or omit the marker; the link text must make sense by itself.
Let the image hook delegate processing
An image hook should not grow a second copy of the image-generation algorithm. Keep resizing and candidate widths in a partial, then pass the Markdown values to it.
For an already configured responsive-image.html partial, layouts/_markup/render-image.html can contain:
{{ $image := .Page.Resources.GetMatch .Destination }}
{{ if reflect.IsImageResourceProcessable $image }}
{{ partial "responsive-image.html" (dict
"image" $image
"alt" .PlainText
"title" .Title
"sizes" "(max-width: 700px) 100vw, 700px"
) }}
{{ else }}
<img src="{{ .Destination | safeURL }}" alt="{{ .PlainText }}"
{{ with .Title }}title="{{ . }}"{{ end }}>
{{ end }}
The image hook context supplies plain description text for the image’s alternative and a title when present. The fallback preserves remote paths and resources the partial cannot process.
The complete partial is in responsive images in Hugo
. Keep its sizes assumption aligned with the actual article layout, rather than copying that value into unrelated components.
A caption is a separate presentation decision. If a Markdown title becomes a figcaption, account for standalone image wrapping and the surrounding paragraph markup. The hook documentation describes that parser configuration.
Give headings a stable destination
Create layouts/_markup/render-heading.html:
<h{{ .Level }} id="{{ .Anchor | safeURL }}">
{{ .Text | safeHTML }}
<a class="heading-anchor" href="#{{ .Anchor | safeURL }}"
aria-label="Link to section: {{ .PlainText }}">#</a>
</h{{ .Level }}>
The heading context provides the level, rendered text, and anchor. Naming the section in the accessible label distinguishes its anchor from the other section links.
Keep the anchor usable with keyboard focus, and avoid hiding it with styling that leaves no focus indication. A subtle mouse-hover treatment still needs an equivalent for a keyboard user.
Changing a hook’s heading markup should not casually change existing IDs. External references and internal fragment links may depend on those destinations even if the visible heading looks unchanged.
Exercise the ordinary cases before using a hook everywhere
A small fixture should include a relative link, a same-host absolute URL, another host, and a fragment. Add optional titles, inline formatting, mailto:, tel:, and an escaped ampersand.
For images, include a processable page resource, an SVG, a remote URL, and a missing path. A successful photograph is not enough to establish the fallback’s behaviour.
Hooks affect Markdown rendered through the matching template; they do not rewrite arbitrary raw HTML already present in an article. Shortcodes and render hooks also have different roles when the content itself needs custom syntax.
A gallery can justify a shortcode. A normal link whose site-wide attributes need consistency is a good fit for a hook. Keeping that distinction clear lets the Markdown stay readable while the site owns its rendering rules.
Filed under Hugo on
Last updated on
Enjoyed this article?
If you found it useful, share it with a friend or someone who might find it helpful too. Thanks for reading!
Related articles
- How I Organize My Hugo Site
A predictable Hugo structure for content, page resources, layouts, assets, static files, and configuration.
- When to Use Hugo Shortcodes or Render Hooks
Use render hooks to change normal Markdown output and shortcodes for custom content with meaning Markdown cannot express.
- Customizing Hugo’s RSS Feed
Adjust Hugo RSS titles, descriptions, content, and section filtering while keeping the generated feed valid and easy to discover.