Optimizing Hugo Image Display

Hugo renders Markdown images directly by default. For large images, you can wrap them in a container through an image render hook, then use CSS to improve how they are displayed.

This article uses the PaperMod theme as an example and implements hover zoom with pure CSS, without JavaScript or any third-party library. That way, a failed CDN request cannot break the feature.

Problems with the previous approach

If you only use :active in CSS, an image is enlarged only while the mouse button is held down, and it is hard to trigger on mobile. It also only scales the current image; it does not show a higher-resolution original.

The approach in this article only improves how images are displayed on the page: images keep a responsive width, scale up smoothly on hover, and indicate the current state with a shadow. It does not modify the image files themselves and adds no extra interaction on mobile.

Implementation

1. Create the image render hook

Create layouts/_default/_markup/render-image.html in the site root:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{{/*
  Wrap Markdown images in a figure element to provide a container for the CSS hover effect.
  Also display the image's alt text as its caption.
*/}}
<figure class="zoom-container">
    <img src="{{ .Destination | safeURL }}"
         alt="{{ .PlainText | safeHTML }}"
         {{- with .Title }} title="{{ . }}"{{ end -}}
         loading="lazy" />

    {{- with .PlainText -}}
        <figcaption>{{ . }}</figcaption>
    {{- end -}}
</figure>

The hook automatically handles Markdown images in posts, such as ![caption](image.jpg), so no changes to the post content are needed.

2. Add the CSS

Add the following styles to assets/css/extended/blank.css. If image-related rules already exist in the file, replace them with the rules below so that the old :active styles do not interfere.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* Image container */
.post-content .zoom-container {
    display: block;
    margin: 25px 0;
    text-align: center;
}

/* Initial state and transition of the image */
.post-content .zoom-container img {
    max-width: 100%;
    height: auto;
    display: inline-block;
    border-radius: 5px;
    transition: transform 0.3s ease, box-shadow 0.3s ease;
}

/* Scale the image up on hover */
.post-content .zoom-container:hover img {
    transform: scale(1.2);
    cursor: zoom-in;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
    position: relative;
    z-index: 10;
}

/* Image caption */
.post-content .zoom-container figcaption {
    margin-top: 10px;
    font-size: 0.9em;
    color: var(--secondary);
    text-align: center;
    font-style: italic;
}

If you previously defined custom PaperMod layout widths, you can keep the variable definitions from the original file, for example:

1
2
3
4
5
:root {
    --nav-width: 1380px;
    --article-width: 650px;
    --toc-width: 300px;
}

Result and notes

After restarting hugo server and refreshing the page:

  1. Images never exceed the post content area and adapt to the screen width.
  2. When an image has alt text, that text is shown below the image.
  3. On hover, the image scales up smoothly and shows a zoom cursor and a shadow.
  4. Mobile devices usually have no hover state, so images stay at their normal size and reading is unaffected.

The zoom ratio and animation duration are controlled by scale(1.2) and 0.3s respectively, and can be adjusted to fit the post layout. Since a scaled-up image may overlap adjacent content, it is best not to set the ratio too high.