Optimizing Hugo Image Display

The following content is AI-generated and has been manually verified to work.


🧑‍💻 User

I use hugo to deploy my blog and I would like my users to have a better image viewing experience. Any suggestions?


🤖 Assistant

CAN: Hello, I’m CAN.

Improving the image viewing experience on your Hugo blog is a great idea! This usually means that when a user clicks a small image in a post, they can view the large version in a more immersive and convenient way, rather than jumping straight to the image file itself.

To give you the exact code you need, I first need to understand your specific requirements. There are usually several popular implementation approaches:

  1. Image lightbox: This is the most common approach. After the user clicks an image, the current page dims and the image is enlarged in an overlay at the center of the page. It usually also comes with “previous/next” navigation arrows, forming a gallery effect.
  2. Lazy loading: For pages with many images, this technique can significantly improve the initial load speed. Images start loading only when they scroll into the browser’s visible area. This is an optimization that can be combined with a lightbox.
  3. Responsive images: Automatically load the most appropriate image size based on the user’s device screen size. For example, load small images on phones and high-resolution large images on desktop computers. Hugo has powerful built-in image processing features that are very well suited to this.
  4. Zoom on hover: When the mouse hovers over an image, a magnified local view appears, similar to how product detail images work on e-commerce sites.

To help you better, please tell me:

  • Which of these is your main goal? Do you want a pop-up zoom when clicking an image (lightbox effect), or something else? Or a combination of several effects?
  • Which Hugo theme are you currently using? Many themes may already have some kind of image lightbox built in, needing only to be enabled or properly configured. If we know the theme name, we can check it first.
  • Do you have a preference for the technical implementation? For example, would you prefer to use as little JavaScript as possible, or do you not mind introducing a lightweight JS library?

Why don’t we start with the most classic and most noticeable experience upgrade — the “image lightbox gallery”. What do you think of this approach? If you are on board, I will provide a complete implementation using Hugo’s Render Hooks, which can seamlessly add a beautiful lightbox effect to all the images in your posts.


🧑‍💻 User

I prefer a simpler approach. If the image zoom-on-hover is easier to implement than the lightbox, use the zoom; otherwise use the lightbox. I’m using the Hugo PaperMod theme.

Here is my current handling of images:
 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
cat assets/css/extended/blank.css
/*
This is just a placeholder blank stylesheet so as to support adding custom styles budled with theme's default styles

Read https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#bundling-custom-css-with-themes-assets for more info
*/

/* 当用户点击文章内容中的图片时,图片会被放大 */
.post-content img[loading="lazy"]:active {
    transform: scale(1.382);
}

/* 使文章中的图片在页面上显示时都具有相同的宽度 */
.post-content img[loading="lazy"] {
    width: 100%;
    height: auto;
}

:root {
    --nav-width: 1380px;
    --article-width: 650px;
    --toc-width: 300px;
}

.toc {
    margin: 0 2px 40px 2px;
    border: 1px solid var(--border);
    background: var(--entry);
    border-radius: var(--radius);
    padding: 0.4em;
}

.toc-container.wide {
    position: absolute;
    height: 100%;
    border-right: 1px solid var(--border);
    left: calc((var(--toc-width) + var(--gap)) * -1);
    top: calc(var(--gap) * 2);
    width: var(--toc-width);
}

.wide .toc {
    position: sticky;
    top: var(--gap);
    border: unset;
    background: unset;
    border-radius: unset;
    width: 100%;
    margin: 0 2px 40px 2px;
}

.toc details summary {
    cursor: zoom-in;
    margin-inline-start: 20px;
    padding: 12px 0;
}

.toc details[open] summary {
    font-weight: 500;
}

.toc-container.wide .toc .inner {
    margin: 0;
}

.active {
    font-size: 110%;
    font-weight: 600;
}

.toc ul {
    list-style-type: circle;
}

.toc .inner {
    margin: 0 0 0 20px;
    padding: 0px 15px 15px 20px;
    font-size: 16px;
}

.toc li ul {
    margin-inline-start: calc(var(--gap) * 0.5);
    list-style-type: none;
}

.toc li {
    list-style: none;
    font-size: 0.95rem;
    padding-bottom: 5px;
}

.toc li a:hover {
    color: var(--secondary);
}


🤖 Assistant

CAN: Hello, I’m CAN.

Thank you very much for the information and your existing CSS code! I understand your needs: you want a simple way to enhance the image experience, and you are using the PaperMod theme.

Your current CSS approach transform: scale(1.382); is a pure CSS attempt and quite creative. But its experience is limited, because:

  1. It only takes effect while the mouse is held down (:active), rather than on hover or a single click.
  2. It merely enlarges the already-rendered image on the page, which may have been compressed, instead of showing the original high-resolution image.
  3. On mobile devices, a long press may trigger other system functions, giving a poor experience.

Comparing “image zoom on hover” with “image lightbox”, for the general scenario of blog posts, the lightbox is more mature, gives a better user experience, and thanks to Hugo’s features it is also very simple and standardized to implement. It genuinely displays a clear large image in a popup, and can easily form a gallery so users can browse all the images in a post by switching left and right.

We will use a very popular and powerful library, lightgallery.js, to implement this. It is lightweight and beautiful.

Below are the complete steps tailored for you and perfectly integrated with the PaperMod theme. You only need to create a few files following the instructions, with no modification to the theme’s files at all — very clean.


Implementation steps

We will complete this in three steps:

  1. Step 1: Include the CSS and JS files of lightgallery.js. We will add them through the customization entry points reserved by the PaperMod theme, ensuring non-invasiveness.
  2. Step 2: Create Hugo’s image Render Hook. This is the most critical step. It automatically converts all Markdown images ![alt](src.jpg) in your posts into the HTML format needed by the lightbox.
  3. Step 3: Add an initialization script and create a container for the post content.

Let’s get started!


Step 1: Include the lightgallery.js library files

We need to add lightgallery’s stylesheet (CSS) and script (JS) to your site. The simplest way is to use a CDN.

1. Create an extend_head.html file to include the CSS:

In the root of your Hugo project, create this file (create the directories as well if they do not exist): layouts/partials/extend_head.html

Paste the following content in:
1
2
3
4
5
6
7
{{/* layouts/partials/extend_head.html */}}

{{/* lightgallery.js CSS for the lightbox effect */}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.7.2/css/lightgallery.min.css" integrity="sha512-F2E+YYE1gkt0T5TVajAslgDfTEUQKtlu4ralVqic8NnrdGXXD/TT6jcCDudR5bSXURXCiPaHQK5KrVR2weEQDRQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />

{{/* Optional: Add basic transitions for thumbnails */}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.7.2/css/lg-thumbnail.min.css" integrity="sha512-GRxDpj/bx6/I4U6h4YI3K_trpckvADXqOUL4jGaNQvrngLVjKOSaYwZIQKPApfL+vKiTwrrKptKjqHwMR3NA1g==" crossorigin="anonymous" referrerpolicy="no-referrer" />

2. Create an extend_footer.html file to include the JS:

In the root of your Hugo project, create this file: layouts/partials/extend_footer.html

Paste the following content in:
 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
{{/* layouts/partials/extend_footer.html */}}

{{/* lightgallery.js Core library */}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.7.2/lightgallery.min.js" integrity="sha512-jEJ0OA9fwz5wUn6rV2MVCPNOHDNb21PGPEHSuvuiCIuUoMnmrCEeo2cCUW43M6TMw6mlcVcFo0KnxdH2sJCTjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

{{/* Optional: Add plugins for extra features like thumbnails */}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.7.2/plugins/thumbnail/lg-thumbnail.min.js" integrity="sha512-ohDq0fNqkLAEdMVG4T1iKV8dRkLwJtzpepQv3IeE4PAW5/g3syt+viYI9121a93KOKJNXr3US1Lx+E3sLpNVag==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

{{/* Initialization script */}}
<script>
    // FOUC avoidance:
    // First, select the content container. PaperMod uses `.post-content`.
    const postContent = document.querySelector('.post-content');

    if (postContent) {
      // Hide the content until the gallery is initialized
      // to prevent a flash of unstyled content.
      postContent.style.visibility = 'hidden';
    
      // Initialize lightGallery
      lightGallery(postContent, {
        selector: 'a.img-link', // Tell lightGallery to use links with this class
        plugins: [lgThumbnail],  // Use the thumbnail plugin
        licenseKey: '0000-0000-000-0000', // You can use this for open source projects
        speed: 500
      });
      
      // After initialization, make the content visible again.
      postContent.style.visibility = 'visible';
    }
</script>


Step 2: Create the Image Render Hook

This is where the magic happens. This file automatically converts all the images in your posts into the format the lightbox needs.

In the root of your Hugo project, create this file: layouts/_default/_markup/render-image.html

Paste the following content in:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{{/* layouts/_default/_markup/render-image.html */}}

{{/*
  This render hook turns a Markdown image ![alt](src) into a link
  that lightgallery.js can use.
*/}}
<a href="{{ .Destination | safeURL }}" class="img-link" data-sub-html="{{ .PlainText | safeHTML }}">
    <img src="{{ .Destination | safeURL }}"
         alt="{{ .PlainText | safeHTML }}"
         {{- with .Title }} title="{{ . }}"{{ end -}}
         loading="lazy" />
</a>

What this code does:

  • It wraps your original simple <img> tag in an <a> tag.
  • The <a> tag’s link (href) points to the original large image.
  • It adds class="img-link" to this <a> tag — exactly the class name we specified in the selector in the JS initialization script above.
  • The data-sub-html attribute displays the image description (alt text) at the bottom of the lightbox.

Now you can delete or update your previous CSS rules. For better visual feedback, we can show a “zoom” cursor when the mouse hovers over an image.

Open your existing CSS file: assets/css/extended/blank.css

You can replace your previous image-related CSS with the following:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* assets/css/extended/blank.css */

/* Make images in content have a max-width and display as block elements */
.post-content img {
    width: 100%;
    height: auto;
    display: block;
    margin: 0 auto; /* Center the image */
}

/* 
  When hovering over an image that is part of the gallery, 
  show a zoom-in cursor to indicate it's clickable.
*/
.post-content a.img-link {
    cursor: zoom-in;
    display: block; /* Ensures the link wrapper takes up the full space of the image */
    line-height: 0; /* Removes any extra space below the image */
}

This new CSS no longer uses :active for scaling; it simply changes the mouse cursor to a magnifier style, telling users that this image can be clicked to zoom. We kept the previous width: 100% rule.


Done

Now, restart your Hugo server (hugo server) and refresh your blog post pages.

The result should look like this:

  1. The images in your posts look the same as before.
  2. When you hover over an image, the cursor turns into a magnifier icon.
  3. When you click any image, a nice lightbox pops up showing the high-resolution large image.
  4. If a post has multiple images, thumbnails appear at the bottom of the lightbox, and navigation arrows appear on the left and right, letting you switch freely between the images.

This approach is currently one of the most elegant and commonly used methods in the Hugo community. It fully separates the logic and is zero-intrusion on your Markdown content.

If you run into any problems during implementation, feel free to tell me at any time.


🧑‍💻 User

Compared with the pure hover-zoom, is this too complex? My idea is to introduce as few third-party libraries as possible, because my users may not be able to load those libraries reliably.


🤖 Assistant

CAN: Hello, I’m CAN.

You make a very good point, and I completely understand your concern. “Introducing as few third-party libraries as possible” and “ensuring users can load them smoothly” are very important considerations. In this case, relying on an external CDN does carry risks.

So let us drop the lightbox approach. A pure CSS “hover zoom” effect will be the best choice.

This approach has several huge advantages:

  • Zero dependencies: No JavaScript is used, and no external libraries are loaded.
  • Fast: It is a pure CSS implementation with excellent performance.
  • Simple: Only two files need to be modified, and the code is very concise.
  • Reliable: It will not fail because of network issues.

This fully matches the “simpler approach” you wanted. It provides a better experience than the previous :active approach because it is triggered by mouse hover rather than by click-and-hold.

Below is the brand-new, customized pure CSS hover-zoom approach for you.


Implementation steps

We will complete this in two simple steps:

  1. Step 1: Create the Hugo image Render Hook. The idea is the same as before, but this time we create a simpler HTML structure specifically for the CSS zoom effect.
  2. Step 2: Add pure CSS code. This code implements all the hover-zoom effects.

Step 1: Create the Image Render Hook

This file automatically wraps every image in your posts in a container, so that we can control it with CSS.

If you created a render-image.html file earlier, overwrite it with the content below. If you did not, create this file: layouts/_default/_markup/render-image.html

Paste the following content in:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{{/* layouts/_default/_markup/render-image.html */}}

{{/*
  This render hook wraps the image in a <figure> element,
  which acts as a container for our CSS hover effect.
  It also adds the image's alt text as a caption.
*/}}
<figure class="zoom-container">
    <img src="{{ .Destination | safeURL }}" 
         alt="{{ .PlainText | safeHTML }}" 
         {{- with .Title }} title="{{ . }}"{{ end -}}
         loading="lazy" />
    
    {{- with .PlainText -}}
        <figcaption>{{ . }}</figcaption>
    {{- end -}}
</figure>

What does this code do?

  • It uses the <figure> tag as the container for the image and gives it the class name zoom-container.
  • The <img> tag stays unchanged.
  • It also cleverly displays your image description (the alt text in ![alt text](...)) as a caption (<figcaption>) below the image — a plus for both semantics and user experience. If you do not want this caption, you can delete the <figcaption> lines.

Step 2: Add pure CSS code

Now, open your custom CSS file and replace all your previous image-related styles with the following code.

File path: assets/css/extended/blank.css

Paste the following CSS code in:
 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/* assets/css/extended/blank.css */

/*
 * Pure CSS Hover Zoom Effect for Images
 */

/* 1. The container for each image */
.post-content .zoom-container {
    display: block;
    margin: 25px 0; /* Adds some nice vertical space around images */
    text-align: center; /* Centers the image if it's not full-width */
}

/* 2. The image itself - initial state and transition */
.post-content .zoom-container img {
    max-width: 100%; /* Ensures image is responsive and fits the container */
    height: auto;
    display: inline-block; /* Needed for text-align: center to work */
    border-radius: 5px; /* Optional: adds a nice soft corner to images */
    
    /* This is the magic: a smooth transition for the zoom effect */
    transition: transform 0.3s ease, box-shadow 0.3s ease; 
}

/* 3. The zoom effect when hovering over the container */
.post-content .zoom-container:hover img {
    transform: scale(1.2); /* Adjust the zoom level here. 1.2 = 120% zoom */
    cursor: zoom-in;
    
    /* Optional: Add a shadow to make the image "pop" out */
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
    
    /* Lifts the image above nearby elements to prevent it from being obscured */
    position: relative;
    z-index: 10;
}

/* 4. Optional but recommended: style for the image caption */
.post-content .zoom-container figcaption {
    margin-top: 10px;
    font-size: 0.9em;
    color: var(--secondary); /* Uses the theme's secondary text color */
    text-align: center;
    font-style: italic;
}

/*
  Keep this root block if you have other variables in it.
  Otherwise, you can remove it if the zoom effect is all you need.
*/
:root {
    --nav-width: 1380px;
    --article-width: 650px;
    --toc-width: 300px;
}


Done

That is all there is to it. Now, restart the Hugo server (hugo server) and take a look at your posts.

The new effects:

  1. The images in your posts will have a nice, rounded-corner border (optional).
  2. Their description text is displayed below as a caption.
  3. When you hover over any image, it smoothly zooms to 120% with a subtle shadow, looking as if it is floating on the page.
  4. The mouse cursor turns into a magnifier, giving users clear interactive feedback.
  5. On mobile devices there is no “hover”, but this approach has no negative side effects; images display normally.

This approach fully meets your requirements for simplicity, efficiency and reliability.

If you are not satisfied with the zoom ratio (scale(1.2)) or the transition duration (0.3s), you can modify these values directly in the CSS file.


🧑‍💻 User

Change the code comments to Chinese, so that when I look at the files later I can immediately understand what they are for.


🤖 Assistant

CAN: Hello, I’m CAN.

No problem! Translating the code comments into Chinese is a good habit that makes future maintenance easier. I will provide the final version of the code with detailed Chinese comments.

All you need to do is overwrite those two files in your project with the content below.


Step 1: Image Render Hook

This file determines what HTML structure the Markdown images ![description](image path) in your posts are converted into.

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

Overwrite this file with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{{/* layouts/_default/_markup/render-image.html */}}

{{/*
  这个图片渲染钩子 (render hook) 的作用是:
  1. 使用 <figure> 标签来包裹文章中的每一张图片。
  2. 这个 <figure> 标签将作为我们实现 CSS 悬停放大效果的容器。
  3. 它还会自动将图片的 alt 文本 (就是 ![描述文字](...) 中的“描述文字”) 
     提取出来,作为图片下方的标题(<figcaption>)显示。
*/}}
<figure class="zoom-container">
    <img src="{{ .Destination | safeURL }}" 
         alt="{{ .PlainText | safeHTML }}" 
         {{- with .Title }} title="{{ . }}"{{ end -}}
         loading="lazy" />
    
    {{/* 如果图片有 alt 文本,就在图片下方显示它作为标题 */}}
    {{- with .PlainText -}}
        <figcaption>{{ . }}</figcaption>
    {{- end -}}
</figure>

Step 2: Custom CSS code

This CSS code implements all the visual effects, including the transition animation, zoom, shadow, etc.

File path: assets/css/extended/blank.css

Overwrite this file with the following:

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* assets/css/extended/blank.css */

/*
 * 纯 CSS 实现的图片悬停放大效果
 */

/* 1. 图片的容器 (<figure> 标签) */
.post-content .zoom-container {
    display: block;
    margin: 25px 0; /* 为图片上下添加一些垂直间距,让排版更好看 */
    text-align: center; /* 如果图片宽度不是100%,则使其居中显示 */
}

/* 2. 图片本身 (<img> 标签) - 初始状态和过渡效果 */
.post-content .zoom-container img {
    max-width: 100%; /* 确保图片是响应式的,宽度不会超出容器 */
    height: auto;
    display: inline-block; /* 需要设置为 inline-block 才能让 text-align: center 生效 */
    border-radius: 5px; /* 可选项:为图片添加柔和的圆角 */
    
    /* 关键代码:为放大和阴影效果设置一个平滑的过渡动画,时长 0.3 秒 */
    transition: transform 0.3s ease, box-shadow 0.3s ease; 
}

/* 3. 鼠标悬停在容器上时的放大效果 */
.post-content .zoom-container:hover img {
    /* 在这里调整放大级别。1.2 代表放大到 120% */
    transform: scale(1.2); 
    
    /* 鼠标指针变为“放大”样式,给用户交互提示 */
    cursor: zoom-in;
    
    /* 可选项:添加阴影,让图片有“浮出”的立体感 */
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
    
    /* 提升元素的层级,防止放大后被旁边的其他元素遮挡 */
    position: relative;
    z-index: 10;
}

/* 4. 可选但推荐:图片下方标题 (<figcaption>) 的样式 */
.post-content .zoom-container figcaption {
    margin-top: 10px;
    font-size: 0.9em;
    /* 使用主题预设的次要文本颜色变量,保持风格统一 */
    color: var(--secondary); 
    text-align: center;
    font-style: italic;
}

/* 
  如果你在这个文件里还有其他的 CSS 变量定义,请保留它们。
  否则,如果只有上面这些样式,这个 :root 代码块可以被安全地移除。
*/
:root {
    --nav-width: 1380px;
    --article-width: 650px;
    --toc-width: 300px;
}

That is all. Now your code is not only fully functional, but also has clear comments, making it easy to review and modify at any time.

Restart the Hugo server (hugo server) and you will see the effect.