Better performance with Hugo Pipes

I recently used Hugo Pipes to optimize asset handling in this site and replaced old Webpack based hack I had used to achieve same.

That hack was done in the times of Hugo version 0.3x. Currently all necessary things like JS bundling, CSS pre- and post-processing, fingerprinting, minification, etc is possible with Hugo native features:

  • Markdown render hooks for customizing how certain blocks in Markdown content is rendered
  • Hugo Pipes for CSS / JS processing, bundling, fingerprinting, minification, image processing etc
  • Page bundles for better organization of post related resources with Markdown content

Like you see in following snippets, Hugo Pipes version is really simple to use and maintain compared to configuring and handling Webpack builds. Essentially they work like you would expect with zero configuration.

Styles

Handling CSS is really simple:

{{ $style := resources.Get "stylesheets/all.css.scss" | toCSS | minify | fingerprint }}
<link rel="stylesheet"
  href="{{ $style.Permalink }}"
  integrity="{{ $style.Data.Integrity }}" />

Referenced CSS file could be stored in global /assets directory, or in the themes/{{ theme name}}/assets.

In the above template function toCSS and minify does exactly what it says in the tin, but fingerprint is a bit more special enabling multiple things with the content based hash it creates:

  • It determines actual filename generated for the asset stored in the public directory after the build. .Permalink and .RelPermalink provides urls with that hash fingerprint enabling cache busting when content changes.
  • It also calculates hash value which could be used as subresources integrity hash

Javascript

Bundling Javascript is just as easy as CSS above and template functions are used in similar fashion.

{{ $jsMain := resources.Get "javascripts/index.js" | js.Build "main.js" | minify | fingerprint }}
<script type="application/javascript"
  src="{{ $jsMain.Permalink }}"
  integrity="{{ $jsMain.Data.Integrity }}">
</script>

It uses esbuild under the hood and NPM dependencies installed from package.json with NPM or Yarn works just like one would expect.

Images

Image handling needed a bit more work, but it’s also great example how powerful content rendering possibilities Hugo has.

Stuff I had to do:

  1. Store images in /assets/images directory, similar directory inside theme, or with the page bundle directory
  2. Create image rendering hook to customize how regular Markdown image references eg. ![alt text](file path / name) are rendered.
  3. Customize templates to use global resources or .Page.Resources modules

Here’s the layouts/_default/_markup/render-image.html implementation:

{{ $image := .Page.Resources.GetMatch (printf "%s" (.Destination | safeURL)) | fingerprint }}
{{ $alt := .PlainText | safeHTML }}
{{ $caption := "" }}
{{ with .Title }}
  {{ $caption = . | safeHTML }}
{{ end }}
<figure>
  <a href="{{ $image.RelPermalink }}">
    <img
      src="{{ $image.RelPermalink }}"
      width="{{ $image.Width }}"
      height="{{ $image.Height }}"
      alt="{{ if $alt }}{{ $alt }}{{ else if $caption }}{{ $caption | markdownify | plainify }}{{ else }}&nbsp;{{ end }}"
      />
  </a>
  {{ with $caption }}
    <figcaption>{{ . | markdownify }}</figcaption>
  {{ end }}
</figure>

And another example how global images could be used in the layouts:

<div class="logo">
  <a href="/" title="Home">
    {{ with resources.Get "images/logo.jpg" }}
      {{ $image := . | fingerprint }}
      <img
        src="{{ $image.RelPermalink }}"
        width="{{ $image.Width }}"
        height="{{ $image.Height }}"
        alt="Logo"
        />
    {{ end }}
  </a>
</div>

I’m especially happy to be able to use regular Markdown for all images instead of custom shortcodes I earlies had to implement for that Webpack + manifest json version.