Static asset cache busting for Hugo
posted in performance, webpack, 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
andassets/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.
- Install and configure media type specific webpack loader /
file-loader
- Use manifest.json directly from templates or create custom shortcodes.