Hugo Image Processing: Resizing, Optimizing, and Serving Images
Hugo has a built-in image processing pipeline that handles resizing, format conversion, and optimization at build time — no external service, no plugin, no JavaScript-based lazy loading required. For publishing sites where images are a significant part of content, understanding Hugo’s image processing is worth the investment. The result is faster pages with properly sized images served in modern formats, generated automatically from source files.
Page Resources vs Global Resources
Hugo works with images in two contexts:
Page resources are files stored in the same directory as a content file (a page bundle). A post at content/posts/my-article/index.md can have images stored in content/posts/my-article/ — these are page resources, accessible to that post’s templates through .Resources.
Global resources are files stored in the assets/ directory. These are accessible to any template through the resources.Get function.
Most publishing workflows use page bundles for article images (keeping images with the content they belong to) and global resources for site-wide assets.
Basic Image Processing
In a template, get an image resource and process it:
{{ $image := .Resources.GetMatch "hero.jpg" }}
{{ if $image }}
{{ $resized := $image.Resize "1200x" }}
<img src="{{ $resized.RelPermalink }}"
width="{{ $resized.Width }}"
height="{{ $resized.Height }}"
alt="Article hero image">
{{ end }}
The Resize "1200x" call resizes the image to 1200px wide, preserving aspect ratio (the x without a height value). Hugo generates the resized image in the build output and caches it — subsequent builds use the cached version if the source image has not changed.
Resize Methods
Hugo provides several image operations:
Resize — resizes to exact dimensions, or to width or height only with aspect ratio preserved:
{{ $img.Resize "800x600" }} // Exact dimensions (may distort)
{{ $img.Resize "800x" }} // Width 800, height auto
{{ $img.Resize "x600" }} // Height 600, width auto
Fit — resizes to fit within a box while preserving aspect ratio (never crops):
{{ $img.Fit "800x600" }}
Fill — resizes and crops to exactly fill the given dimensions (center crop by default):
{{ $img.Fill "800x600" }}
{{ $img.Fill "800x600 Top" }} // Crop from top
{{ $img.Fill "800x600 Smart" }} // Smart crop (finds focal point)
Crop — crops to exact dimensions without resizing:
{{ $img.Crop "800x600" }}
Format Conversion and WebP
Hugo can convert images to modern formats at build time. Convert to WebP (typically 25-35% smaller than JPEG at equivalent quality):
{{ $webp := $image.Resize "1200x webp" }}
Or use the Process method:
{{ $webp := $image.Process "webp" }}
For browsers that do not support WebP, serve a fallback using the HTML <picture> element:
{{ $image := .Resources.GetMatch "photo.jpg" }}
{{ $webp := $image.Resize "1200x webp" }}
{{ $jpg := $image.Resize "1200x" }}
<picture>
<source srcset="{{ $webp.RelPermalink }}" type="image/webp">
<img src="{{ $jpg.RelPermalink }}"
width="{{ $jpg.Width }}"
height="{{ $jpg.Height }}"
alt="{{ .Params.image_alt | default "Article image" }}">
</picture>
Responsive Images with srcset
Generate multiple sizes for responsive delivery:
{{ $image := .Resources.GetMatch "hero.jpg" }}
{{ $small := $image.Resize "600x webp" }}
{{ $medium := $image.Resize "1200x webp" }}
{{ $large := $image.Resize "2000x webp" }}
{{ $fallback := $image.Resize "1200x" }}
<picture>
<source type="image/webp"
srcset="{{ $small.RelPermalink }} 600w,
{{ $medium.RelPermalink }} 1200w,
{{ $large.RelPermalink }} 2000w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 80vw, 1200px">
<img src="{{ $fallback.RelPermalink }}"
width="{{ $fallback.Width }}"
height="{{ $fallback.Height }}"
alt="Hero image"
loading="lazy">
</picture>
Setting explicit width and height attributes is important for Core Web Vitals — it allows the browser to reserve the correct space before the image loads, preventing Cumulative Layout Shift.
Quality Settings
Control compression quality for JPEG and WebP:
{{ $img.Resize "1200x q80" }} // 80% quality
{{ $img.Resize "1200x webp q75" }} // WebP at 75% quality
A quality setting of 75–85 for WebP and 80–85 for JPEG is a reasonable default for editorial photography — high quality with meaningful file size reduction.
Image Shortcode
Wrap the image processing logic in a Hugo shortcode to make it reusable in Markdown content:
layouts/shortcodes/img.html:
{{- $src := .Get "src" -}}
{{- $alt := .Get "alt" | default "" -}}
{{- $caption := .Get "caption" -}}
{{- $image := .Page.Resources.GetMatch $src -}}
{{- if $image -}}
{{- $webp := $image.Resize "1200x webp q80" -}}
{{- $fallback := $image.Resize "1200x q85" -}}
<figure>
<picture>
<source srcset="{{ $webp.RelPermalink }}" type="image/webp">
<img src="{{ $fallback.RelPermalink }}"
width="{{ $fallback.Width }}"
height="{{ $fallback.Height }}"
alt="{{ $alt }}"
loading="lazy">
</picture>
{{- with $caption -}}
<figcaption>{{ . | markdownify }}</figcaption>
{{- end -}}
</figure>
{{- end -}}
Usage in content:
{{< img src="interview-photo.jpg" alt="Jane Smith at her desk" caption="Photo: John Doe" >}}
Handling Featured Images
For featured images set in frontmatter, process them as global resources:
---
title: "My Post"
featured_image: "/images/posts/my-post-hero.jpg"
---
In the template:
{{ with .Params.featured_image }}
{{ $image := resources.Get . }}
{{ if $image }}
{{ $webp := $image.Resize "1200x webp q80" }}
<img src="{{ $webp.RelPermalink }}"
width="{{ $webp.Width }}"
height="{{ $webp.Height }}"
alt="{{ $.Title }}">
{{ end }}
{{ end }}
Note: images must be in the assets/ directory to be processed as global resources. Images in static/ are copied verbatim and cannot be processed.
Build-Time Caching
Hugo caches processed images in a resources/ directory at the project root. This directory should be committed to version control — without it, every fresh build regenerates all image variants, which can significantly slow CI/CD pipelines for image-heavy publishing sites. With the cache committed, only new or changed source images require processing.