One of the things I love about Hugo is its flexibility when it comes to Markdown rendering. While the default behavior
for images is usually enough, I found myself wanting more control. Specifically the ability to add CSS classes
or wrap images in links without resorting to raw HTML in my Markdown files (as hugo raises warnings about this).
To solve this, I started using a custom render-image.html template. It allows me to stay in “Markdown mode” while
still getting the benefits of optimized, flexible image output.
The Problem
Standard Markdown image syntax is simple: . However, it doesn’t provide a native
way to:
- Add a CSS class for styling (e.g., centering an image or adding a border).
- Link the image to another page or a larger version of itself.
- Automatically handle Hugo’s Image Processing for local resources.
The Solution: A Custom Render Hook
Hugo allows you to override how Markdown elements are rendered by creating a “render hook”. By placing a file at
layouts/_default/_markup/render-image.html, we can intercept every image in our Markdown and process it through our
own logic.
Advanced Syntax
My template extends the “Title” part of the image syntax to support extra attributes. I use a # separator to define
classes and a | to define a link.
Basic Image:

Image with a CSS Class:

Image with a Class and a Link:

How It Works
The template does a few smart things:
- Resource Resolution: It first checks if the image is a Page Resource. If not, it looks in the global
assetsviaresources.Get. If it’s still not found (like an external URL), it falls back to the provided destination. - Title Parsing: it splits the
.Titlestring by#and|to extract the actual title, the CSS class, and the link URL. - Performance: It automatically adds
loading="lazy"anddecoding="async"to every image, which is great for Core Web Vitals. - Dimensions: If the image is found as a Hugo resource, it automatically populates the
widthandheightattributes to prevent layout shifts.
The Template
Here is the full render-image.html template. You can drop this into your Hugo project at
layouts/_default/_markup/render-image.html.
1{{- $dest := .Destination -}}
2{{- $class := .Attributes.class -}}
3{{- $link := .Attributes.link -}}
4
5{{- if (strings.Contains .Title "#") -}}
6{{- $parts := split .Title "#" -}}
7{{- .Page.Scratch.Set "title" (index $parts 0) -}}
8{{- $fragment := (index $parts 1) -}}
9{{- if (strings.Contains $fragment "|") -}}
10{{- $fragParts := split $fragment "|" -}}
11{{- $class = (index $fragParts 0) -}}
12{{- $link = (index $fragParts 1) -}}
13{{- else -}}
14{{- $class = $fragment -}}
15{{- end -}}
16{{- else -}}
17{{- .Page.Scratch.Set "title" .Title -}}
18{{- end -}}
19{{- $title := .Page.Scratch.Get "title" -}}
20
21{{- $img := .Page.Resources.GetMatch $dest -}}
22{{- if not $img -}}
23{{- $img = resources.Get $dest -}}
24{{- end -}}
25
26{{- if $img -}}
27{{- $processed := $img -}}
28{{- if $link -}}
29<a href="{{ $link | safeURL }}">
30 {{- end -}}
31 <img src="{{ $processed.RelPermalink }}"
32 width="{{ $processed.Width }}"
33 height="{{ $processed.Height }}"
34 {{ with .Text }}alt="{{ . }}" {{ else }}alt="" {{ end }}
35 {{ with $title }}title="{{ . }}" {{ end }}
36 {{ with $class }}class="{{ . }}" {{ end }}
37 loading="lazy"
38 decoding="async"
39 >
40 {{- if $link -}}
41</a>
42{{- end -}}
43{{- else -}}
44{{- if $link -}}
45<a href="{{ $link | safeURL }}">
46 {{- end -}}
47 <img src="{{ $dest | safeURL }}"
48 {{ with .Text }}alt="{{ . }}" {{ else }}alt="" {{ end }}
49 {{ with $title }}title="{{ . }}" {{ end }}
50 {{ with $class }}class="{{ . }}" {{ end }}
51 loading="lazy"
52 decoding="async"
53 >
54 {{- if $link -}}
55</a>
56{{- end -}}
57{{- end -}}
Hope you find it useful!
