smhk

Automatic image thumbnails in Hugo from static directory

Hugo v0.32 introduced image processing, which can be used to resize, fit or fill images. This can be useful to, for example, automatically create a low resolution thumbnail from a high resolution image. Hugo v0.34 (latest at time of writing) modified the API for image processing.

Using the image processing in Hugo v0.34, my goal is this:

  1. Store my full resolution “original” images in the static directory (/static/img/)
  2. Use Hugo to automatically generate low resolution “thumbnail” images
  3. Use a shortcode to include the thumbnail image in markdown as a link to the original image

Shortcodes §

Hugo image processing can be used directly in Hugo templates, but there is no syntax for using it within markdown. The solution is to use Hugo shortcodes.

Shortcodes let us include a Hugo template with a markdown file. For example, using the following post and shortcode…

/content/post/my-post.md
{{< my-shortcode "hello, world!" 3 />}}
/layouts/shortcodes/my-shortcode.html
{{ $text := .Get 0 }}
{{ $num := .Get 1 }}
<ul>
{{ range (seq $num) }}
  <li>{{ $text }}</li>
{{ end }}
</ul>

…will cause Hugo to render a web page as follows:

Rendered page
<ul>
  <li>hello, world!</li>
  <li>hello, world!</li>
  <li>hello, world!</li>
</ul>

Page Resources §

The Hugo v0.32 release notes include an example shortcode which addresses goals 2 and 3 I outlined above, but does not address goal 1. Further, I wanted to write the shortcode myself to help me better understand how everything fits together.

Goal 1 is not addressed by the aforementioned example because it relies upon including your images within your /content/ directory rather than within your /static/ directory. Using the static directory is desirable for me because I want the option to be able to reference the same image from different posts, rather than assuming a one-to-one relationship between each image and a post.

We can use .Page.Resources within a template to find resources (e.g. images) within that post. For example, given the following directory structure:

my-hugo-site/
├── config.toml
├── content/
│   └── post/
│      └── example-post/
│         ├── index.md
│         └── images/
│             ├── image_1.jpg
│             └── image_2.jpg
├── layouts/
│  └── shortcodes/
│      └── list-images.html
└ static/
   └── img/
       ├── image_3.jpg
       └── image_4.jpg

If we create a shortcode to list all image resources:

/layouts/shortcodes/list-images.html
<ul>
{{ range .Page.Resources }}
  <li><a href="{{ .RelPermalink }}">{{ .ResourceType | title }}</a></li>
{{ end }}
</ul>

And then use this shortcode within our example post:

/post/example-post/index.md
{{< list-images />}}

We get the following page rendered:

Rendered page
<ul>
  <li><a href="/post/example-post/images/image_1.jpg">Image</a></li>
  <li><a href="/post/example-post/images/image_2.jpg">Image</a></li>
</ul>

However it appears there is no way to get .Page.Resources to search in our /static/img/ directory. There is a proposal to add a .Site.Resources but as of Hugo v0.34 it is just not possible.

So there you have it. Not currently possible.

Or is it?

Hacky solution §

Alright…

Let’s alter our structure from earlier to create a new “fakestatic” section with a “fakepost” which contains all our static content:

my-hugo-site/
├── config.toml
├── content/
│   ├── post/
│   │  └── example-post/
│   │     └── index.md
│   └── fakestatic/
│      └── fakepost/
│         ├── index.md
│         └── images/
│             ├── image_1.jpg
│             ├── image_2.jpg
│             ├── image_3.jpg
│             └── image_4.jpg
└── layouts/
    └── shortcodes/
        └── list-fakestatic-images.html

Then in fakestatic/fakepost/index.md we have:

+++
type = "post"
layout = "single"
title = "Fake post"
url = "fake-post"
date = "1970-01-01T00:00:00+00:00"
+++

If we take our example shortcode from earlier and take advantage of the GetPage function, we can list the images from “fakepost” in any other post.

/layouts/shortcodes/list-fakestatic-images.html
{{ $fakestatic := .Site.GetPage "page" "fakestatic/fakepost/index.md" }}
<ul>
{{ range $fakestatic.Resources }}
  <li><a href="{{ .RelPermalink }}">{{ .ResourceType | title }}</a></li>
{{ end }}
</ul>

The above shows that we can access the resources of one post from another. Next, we create a shortcode that accepts the image name as an argument and displays the said image:

{{ $fakestatic := .Site.GetPage "page" "fakestatic/fakepost/index.md" }}
{{ $imagename := printf "images/%s*" (.Get 0) }}
{{ $image := $fakestatic.Resources.GetMatch $imagename }}
<img src="{{ $image.RelPermalink }}">

We can then use the above to include any arbitrary image from /content/fakestatic/fakepost/images/ in any markdown post:

{{< img-fakestatic "image_4.jpg" >}}

Adding the thumbnail §

/layouts/shortcodes/img-thumb-cap.html
{{ $fakestatic := .Site.GetPage "page" "fakestatic/fakepost/index.md" }}
{{ $imagename := printf "images/%s*" (.Get 0) }}
{{ $size := .Get 1 }}
{{ $imageoriginal := $fakestatic.Resources.GetMatch $imagename }}
{{ $imagethumbnail := $imageoriginal.Resize $size }}
<a href="{{ $imageoriginal.RelPermalink}}">
<img src="{{ $imagethumbnail.RelPermalink }}">
</a>
/content/post/example-post/index.md
{{< img-thumb-cap "image_4.jpg" "200x" />}}

Adding a caption and some style §

We can make things a bit more interesting by adding an optional caption and styling.

/layouts/shortcodes/img-thumb-cap.html
{{ $fakestatic := .Site.GetPage "page" "fakestatic/fakepost/index.md" }}
{{ $imagename := printf "images/%s*" (.Get 0) }}
{{ $size := .Get 1 }}
{{ $imageoriginal := $fakestatic.Resources.GetMatch $imagename }}
{{ $imagethumbnail := $imageoriginal.Resize $size }}
<figure>
  <a href="{{ $imageoriginal.RelPermalink}}">
    <img src="{{ $imagethumbnail.RelPermalink }}">
  </a>
  <figcaption>
    {{ with .Inner }}
    {{ . }}
    {{ end }}
  </figcaption>
</figure>
/content/post/example-post/index.md
{{< img-thumb-cap "egg_04.jpg" "200x" >}}A bowl of molten chocolate{{</ img-thumb-cap >}}

To show the above in action:

alt-text
A bowl of molten chocolate

Bonus: getting nicer URLs §

To store the resources with nicer URLs, we can move things around at build time. For example, to store full size images within public/img/full/ rather than public/fake-post/images/, and to store thumbnails within public/img/thumb/, we can create the following script:

fakestatic.js
'use strict'

var fse = require('fs-extra')
var path = require('path')

var relocateStatic = function() {

  try {
    console.log('Copying full images')
    fse.copySync(
      path.resolve(__dirname, '../public/fake-post/images/'),
      path.resolve(__dirname, '../public/img/full'))
  } catch (err) {
    console.error(err)
  }

  try {
    console.log('Copying thumbnail images')
    fse.copySync(
      path.resolve(__dirname, '../resources/_gen/images/fakestatic/fakepost/images/'),
      path.resolve(__dirname, '../public/img/thumb'))
  } catch (err) {
    console.error(err)
  }

  try {
    console.log('Deleting pseudo static')
    fse.remove(
      path.resolve(__dirname, '../public/fakestatic/'))
  } catch (err) {
    console.error(err)
  }
}

relocateStatic()

This requires fs-extra to be added to the devDependencies, e.g.:

package.json
  "devDependencies": {
    "concurrently": "^7.3.0",
    "fs-extra": "^10.1.0",
    "onchange": "^7.1.0",
    "sass": "^1.54.5"
  }

Then add a call to fakestatic.js within the build command, e.g.:

package.json
{
  ...
  "scripts": {
    ...
    "build": "npm run assets:build:prod && hugo --gc --config=hugo.toml && node scripts/fakestatic.js"
  }
}

Then when you run the build command, it will move the “global” resources into the public/img/ directory, and they will be accessible at /img/ on the deployed website.