Static asset cache busting for Hugo

Update since writing this post, Hugo has implemented number of great features providing native way to implement cache busting etc without Webpack.

Checkout new post: Better performance with Hugo Pipes.

Hugo is a blazing fast static site generator with a great architecture for managing content. However asset pipeline à la Middleman or Jekyll is something I have been missing especially for generating images with cache busting hashes in the filenames.

I solved that by configuring Webpack for something similar.

For starters: “Predictable long term caching with Webpack” was great help in getting base webpack config right and content hashes consistent between the builds. I recommend that you too check that out to get around a few Webpack quirks.

Webpack generated files needs to be available for Hugo as static site data and their final filenames including content hashes for cache busting has to be known during Hugo build step. That would be possible by using webpack-manifest-plugin to generate manifest.json which would be used as Hugo site data. Manifest configuration and different content use cases are described below.

Webpack config with bundle manifest

  • I have chosen to place original scripts, styles and images are under assets/javascripts, assets/stylesheets and assets/images directories.

  • Webpack is configured to generate its output into /static

  • Webpack-manifest-plugin generates JSON file containing single object with original filenames as keys and generated names as values. For example:

    {
      "example.js": "example.c0ffee.js",
      "images/logo.svg": "media/logo.deadbeef.svg"
    }
    
  • Webpack plugin configuration outputs manifest.json under /data directory in Hugo project.

Full configuration is following:

I have configured following npm scripts for Webpack usage during development and build time.

{
  "scripts": {
    "watch": "webpack --watch -d --progress --color",
    "build": "webpack --bail -p"
  }
}

When developing I run both yarn run watch and hugo server -D. In build job yarn run build is executed before Hugo.

Handling scripts and styles in Hugo templates

Using manifest.json to include correct styles and scripts…

When including CSS and Javascripts into Hugo templates actual filenames are read from Site data in following manner:

<link rel="stylesheet"
      href="{{- (index .Site.Data.manifest "main.css") | relURL -}}" />

<script type="application/javascript"
        src="{{- (index .Site.Data.manifest "main.js") | relURL -}}">
</script>

Handling images

All images have to be loaded through Webpack first using file-loader and optional image optimizations to get them included into Webpack manifest.json and Hugo’s static files. It’s done by placing all original images into assets/images directory with special Javascript entrypoint which will recursively import each image file.

/* assets/images/index.js
 *
 * Webpack helper to import each image asset file.
 */

function requireAll(r) {
  r.keys().forEach(r);
}

requireAll(require.context("./", true, /\.(jpg|png|gif|svg)$/));

Images in templates and partials

After images are included in static site data using importer entry point hack they are referred in templates just like script and style files:

<img src="{{- (index .Site.Data.manifest "logo.png") | relURL -}}" alt="" />

Using Hugo’s internal templates

Hugo contains nice set of internal templates providing markup for usual social media integration and SEO metadata. These includes features which would create Twitter card and Facebook preview images if those are defined as .Params.images array in content front matter.

I haven’t figured any other way than copying those internal templates from Hugo sources to my site’s partials and modifying them to get image filenames from .Site.Data.manifest and converting them to absolute URLs with absURL pipe.

Here’s Twitter cards partial for example:

{{ if .IsPage }} {{ with .Params.images }}
<!-- Twitter summary card with large image must be at least 280x150px -->
<meta name="twitter:card" content="summary_large_image" />
<meta
  name="twitter:image:src"
  content="{{ (index $.Site.Data.manifest (index . 0)) | absURL }}"
/>
{{ else }}
<meta name="twitter:card" content="summary" />
{{ end }}

<!-- Twitter Card data -->
<meta name="twitter:text:title" content="{{ .Title }}" />
<meta name="twitter:title" content="{{ .Title }}" />
<meta
  name="twitter:description"
  content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}"
/>
{{ with .Site.Social.twitter }}<meta name="twitter:site" content="@{{ . }}" />{{
end }} {{ range .Site.Authors }} {{ with .twitter }}<meta
  name="twitter:creator"
  content="@{{ . }}"
/>{{ end }} {{ end }}{{ end }}

Images in posts and other site content

Custom shortcode had to be created for using images inside blog posts and other Markdown content. I created following layouts/shortcodes/image.html:

{{- with (index $.Site.Data.manifest (.Get "src")) -}} <img src="{{- . | relURL
-}}" {{- else -}} <img src="{{- .Get "src" | relURL -}}" {{- end -}} {{ with
.Get "alt" -}} alt="{{- . -}}" {{ end }} />

It is used in blog content like:

{{‌< image src="cat.jpg" alt="Cat picture" >‌}}

Other media

Other media like audio or videos would be included just like images.

  1. Install and configure media type specific webpack loader / file-loader
  2. Use manifest.json directly from templates or create custom shortcodes.