Responsive Images in Hugo Without a Plugin

Images are often the heaviest part of a web page.

A photo that looks fine at 1600 pixels wide on a desktop may be much larger than needed on a phone. If every visitor downloads the same large file, mobile users pay the cost even when the browser only displays the image at 400 pixels wide.

HTML already has srcset and sizes for responsive images.

Hugo can generate the image sizes for us during the build. No JavaScript or image plugin is required.

The main feature is documented in Hugo image processing.

The HTML we want

<img
  src="/images/photo-800.webp"
  srcset="
    /images/photo-400.webp 400w,
    /images/photo-800.webp 800w,
    /images/photo-1200.webp 1200w
  "
  sizes="(max-width: 700px) 100vw, 700px"
  alt="Street photograph in Riga"
>

The browser chooses the most suitable file.

The problem is creating all those files manually.

Use page resources

A common structure is a page bundle:

content/
└── blogs/
    └── my-article/
        ├── index.md
        └── photo.jpg

Inside the template:

{{ $image := .Resources.GetMatch "photo.jpg" }}

Now Hugo can process it.

Generate several sizes

{{ $small := $image.Resize "400x webp" }}
{{ $medium := $image.Resize "800x webp" }}
{{ $large := $image.Resize "1200x webp" }}

Then output them:

<img
  src="{{ $medium.RelPermalink }}"
  srcset="
    {{ $small.RelPermalink }} 400w,
    {{ $medium.RelPermalink }} 800w,
    {{ $large.RelPermalink }} 1200w
  "
  sizes="(max-width: 700px) 100vw, 700px"
  alt="Street photograph in Riga"
>

Add width and height

Hugo knows the generated dimensions.

<img
  src="{{ $medium.RelPermalink }}"
  width="{{ $medium.Width }}"
  height="{{ $medium.Height }}"
  alt="Street photograph in Riga"
>

Setting width and height helps the browser reserve space before the image loads and can reduce layout shift.

Make a reusable partial

Create:

layouts/
└── partials/
    └── responsive-image.html

Example:

{{ $image := .image }}
{{ $alt := .alt }}

{{ $small := $image.Resize "400x webp" }}
{{ $medium := $image.Resize "800x webp" }}
{{ $large := $image.Resize "1200x webp" }}

<img
  src="{{ $medium.RelPermalink }}"
  srcset="
    {{ $small.RelPermalink }} 400w,
    {{ $medium.RelPermalink }} 800w,
    {{ $large.RelPermalink }} 1200w
  "
  sizes="(max-width: 700px) 100vw, 700px"
  width="{{ $medium.Width }}"
  height="{{ $medium.Height }}"
  alt="{{ $alt }}"
  loading="lazy"
>

Then call it:

{{ partial "responsive-image.html" (dict
  "image" $image
  "alt" "Street photograph in Riga"
) }}

Do not lazy-load the main hero image

If the image is near the top of the page and likely to be the Largest Contentful Paint image, do not automatically add:

loading="lazy"

That can delay an image the browser should load early.

For images further down the page, lazy loading is useful.

fetchpriority for an important image

For the main image:

<img
  ...
  fetchpriority="high"
>

Use this carefully. Not every image should be high priority.

Usually one important LCP image is enough.

Use a Markdown render hook

If you want normal Markdown images to become responsive automatically, create:

layouts/
└── _default/
    └── _markup/
        └── render-image.html

A simplified version:

{{ $image := .Page.Resources.GetMatch .Destination }}

{{ if $image }}
  {{ $small := $image.Resize "400x webp" }}
  {{ $medium := $image.Resize "800x webp" }}
  {{ $large := $image.Resize "1200x webp" }}

  <img
    src="{{ $medium.RelPermalink }}"
    srcset="
      {{ $small.RelPermalink }} 400w,
      {{ $medium.RelPermalink }} 800w,
      {{ $large.RelPermalink }} 1200w
    "
    sizes="(max-width: 700px) 100vw, 700px"
    alt="{{ .Text }}"
    loading="lazy"
  >
{{ else }}
  <img
    src="{{ .Destination | safeURL }}"
    alt="{{ .Text }}"
    loading="lazy"
  >
{{ end }}

Now Markdown such as:

![Street photograph in Riga](photo.jpg)

can produce responsive HTML automatically.

Keep the number of sizes reasonable

You do not need 15 versions of every image.

For many sites:

400w
800w
1200w

is already useful.

A photography-heavy site may want more. A simple blog may need fewer.

Every extra size increases build work and storage.

Why I prefer doing it at build time

The browser should choose the image, but it should not have to generate the image.

Hugo can do that once when the site builds:

Original image
↓
Hugo build
↓
400 / 800 / 1200 versions
↓
Browser chooses one

That gives visitors smaller downloads without adding runtime JavaScript.

For an image-heavy static site, responsive images are one of the easiest performance improvements worth making.