Hugo Pipes Revolution
A Hugo built-in asset pipeline
{{ $style := resources.Get "main.scss" | toCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ $style.Permalink }}" emotion="🤩">
Hugo’s roadmap got itself a new milestone when .43 delivered Hugo Pipes, a built-in set of asset processing methods.
From now on, Hugo will take care of bundling, minifying, fingerprinting our assets and even compiling our sass files! All of this without any external build tools.
In this post we’ll go through Hugo Pipes methods to cover how easy it is to implement a basic Hugo Pipes asset pipeline before turning to more advanced use cases, involving relinquishing some Sass and JS variables to our editors.
What does it change?
Let’s pause for a minute to process the changes brought by Hugo Pipes and how they’ll impact me, and hopefully you too.
I’m using version control and I want to keep my processed sources (distributed) out of my repository!
This means I have to fill my README.md
with instructions on how to npm install
, npm run dev
, grunt watch
etc… I have to educate collaborators and content editors alike on command line and nodejs install, risking much frustration on the other side.
This may also lead to that dreaded security alerts rabbit hole, the worst place for my theme users or collaborators to spend their afternoon… 🐇🕳️
Also, I’m a CI newbie, which means I don’t know how to easily set up a deployment process which would ensure my host doesn’t spend too much time installing Ruby (for SASS), node, and whatever my packages.json
points to for every build. I know there are solutions out there, but not an « out of the box » one that I know of.
As a result, I often end up committing my dist
directory pending someone would come and help… 😔
Turns out Hugo just did, and I’m psyched! 😇
Enters Hugo Pipes🚰
Enough about me, let’s talk about Hugo’s newly introduced Asset Processing set of methods! It’s been the on the #staticgen news cycle for more than a week now so it’s time to get acquainted!
Assets is the new static (wait… no!)
First thing of note, these methods will only be available on files living in the assets
directory, think of it as a static
directory except the files will not be published by default.
Much like its static
counterpart:
- Its location is configurable with the
assetDir
key of yourconfig.yaml
. (will default toassets
) - It follows the Hugo’s file unison logic. Meaning, anything in your
project/assets
will override homonymous files ofyour-theme/assets
.
The big difference with static
is that the files contained in assets
will not be published unless the .Permalink
or .RelPermalink
of their resource object is used.
As of yet you can only define one assets
directory.
Hugo Pipes vs Go Pipes
We’ll be using Go Template Pipes a lot in this article. They are not to be confused with the topic at hands but in a few words, they allow to chain several template functions together using the output of the former as the input of the latter.
Turning this:
{{ $teaser := markdownify (index .Params "teaser") }}
{{ safeHTML $teaser }}
Into that:
{{ index .Params "subtitle" | mardkownify | safeHTML }}
Let’s dive in! 🏊
We could simply list the methods and explain what they do, but that is perfectly done in the official doc 😊, so instead, we’ll dive right in and introduce the methods as we go along.
Here’s our imaginary Hugo project craving for a built-in asset pipeline:
- We have a sass directory containing many sass files all imported by
sass/main.scss
. We want to apply auto-prefixing on our outputtedstyle.css
then minify and lastly fingerprint it for cache busting! - On the script side, we have our
main.js
which needsplugins.js
on every page andcarousel.js
on the portfolio section. Also, that carousel script requires jQuery… 😒. - Last but not least, we’d love to let our editors customize some sass and javascript variables via their
config.yaml
or Front Matter.
Here we .Get.
This is how you grab that asset file and turn it into a processable resource. Once you do that, every Hugo Pipes’ method will be applicable to it.
.Get
looks in the assets
directory of your project, so its second parameter is our filepath relative to that directory.
Let’s start with our Sass file and go .Get
it.
{{ $styleSass := resources.Get "style/main.scss" }}
Sass to CSS with .toCSS
The name is pretty intuitive as .toCSS
will compile our sass
or scss
file into a css
file!
Here we go:
{{ $styleCSS := $styleSass | resources.ToCSS }}
What we just did is use the resource we created from our asset file above and used resources.ToCSS
on its piped in input.
Just like most of the following methods, you can pass a dict
of options as parameter.
Here we want to specify the output path and add a source map, so we’ll use the following bit instead:
{{ $styleCSS := $styleSass | resources.toCSS (dict "targetPath" "custom/style.css" "enableSourceMap" true) }}
.ToCSS
may one day support other preprocessors, but until then it’s only Sass or Scss.Autoprefixing with .PostCSS
resources.PostCSS
does require nodeJS to run. But shall you be ok with a touch of npm
in your environment, you should definitely give it a spin. I’ll let go of my good-riddance-npm smirk and use it in this project so we can « autoprefix » our style.css
.
Hugo will look for a PostCSS config file at the root of our theme or project under the name postcss.config.js
. Ours is pretty straight forward and look like this:
module.exports = {
plugins: {
autoprefixer: {
browsers: [
"last 2 versions",
"Explorer >= 8",
]
}
},
}
Hugo needs postcss-cli
to process PostCSS
so we should install it along our unique PostCSS plugin: autoprefixer
.
Once we have happily run npm install
⌛, we can safely use PostCSS on our style file:
{{ $styleAutoprefixed := $styleCSS | resources.PostCSS }}
Shall we need our PostCSS config file to live elsewhere, we could have set its path in the .PostCSS
method’s options’ dict:
{{ $styleAutoprefixed := $styleCSS | resources.PostCSS (dict "config" "config/postcss.js") }}
Minifying with .Minify
We’re way past 2010 these days so we obviously can’t serve our CSS file as is! Let’s turn hundreds of lines of readable code into a wall of glyphs and save some precious bandwidth in the process…
{{ $styleMinified := $styleAutoprefixed | resources.Minify }}
Fingerprinting with .Fingerprint
Now, we’d usually add some sort of hash after our stylesheet url for some good old cache busting. Previously it involved some readFile
and sha
or now.Unix
if you were lazy like me, but we don’t need that complexity anymore.
.Fingerprint
will update your resource’s .Permalink
with a sha256
hash (or md5
or sha512
if passed as argument).
{{ $styleFingerpinted := $styleMinified | resources.Fingerprint }}
What a style!
We’re currently done with our style file and ready to drop that <link>
.
But before we do, let’s get rid of those many lines of variable declarations. They really helped pacing our walkthrough here but they’re an eye sore. Using Go Pipes we’ll squash them into one happy line! And because each Hugo Pipes transformation method uses a camel-cased alias, we can even go furter and write this beauty:
{{ $style := resources.Get "sass/main.scss" | toCSS | postCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ $style.Permalink }}" emotion="🤩">
Bundling our resources with .Concat
Let’s move our attention to our scripts now. As we mentioned previously, we have several of those and we would like to concatenate them into one big file. We cannot use the same combination of files on every page though.
Let’s start by storing all our script files as independent resources.
{{ $main := resources.Get "js/main.js" }}
{{ $plugins := resources.Get "js/plugins.js" }}
{{ $carousel := resources.Get "js/carousel.js" }}
{{ $jQuery := resources.Get "js/jquery.js" }}
For most of our pages, we’ll use resources.Concat
to bundle $plugins
and $main
, in that order!
{{ $defaultJS := slice $plugins $main | resources.Concat "js/global.js" }}
For most of the transformation methods we used with our style, the resulting filepath was guessed by Hugo Pipes. It usually does so by taking the original asset filepath and modifying its extension when needed. But here, we’ve got several files and filepaths and Hugo won’t take any guess so we need to set our desired filepath as argument.
Great, we have a bundled js/global.js
for most of our pages.
Now for our portfolio section, it is a bit more complex as we need both jQuery
and the carousel thingy.
{{ $portfolioJS := slice $plugins $main $jQuery $carousel |resources.Concat "js/global-carousel.js" }}
Now we have two bundles to chose from: js/global.js
and js/global-carousel.js
.
Assuming we’re in the near future where Go Template allows variable overwrite, this would be our code:
{{ $script := $defaultJS }}
{{ if eq .Section "portfolio" }}
{{ $script = $portfolioJS }}
{{ end }}
{{ $globalJS := $script | resources.Minify | resources.Fingerprint }}
<script src="{{ $globalJS.Permalink }}"></script>
Securing our script!
Subresource Integrity is not broadly adopted yet, but we’re already familiar with it…
<script src="https://cdn.fancyscript.com/this.js"
integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
crossorigin="anonymous"></script>
To use it though, all we need to do is apply resources.Fingerprint
on our resource. Once done, Hugo will add a new property, .Data.Integrity
to the resource object.
Because we already .Fingerprint
ed our script above, we can just drop this:
<script src="{{ $globalJS.Permalink }}" integrity="{{ $globalJS.Data.Integrity }}"></script>
Which would output in your HTML something like:
<script src="/js/global.8a9b235048fd76f45b330ac0064465533974e0a56b16c8adbfe9ee05e7ec16a0.js" integrity="sha256-viNbi1/jdSKJ2RjaMNoxkJur/LU4duVn6G92KVhULfM="></script>
If we wanted to use integrity without a Fingerprinted .Permalink
we’d have to isolate our fingerprinted resource from the linked one like so:
{{ $fpJS := $script | resources.Fingerprint }}
<script src="{{ $script.Permalink }}" integrity="{{ $fpJS.Data.Integrity" }}></script>
hugo server
you may test .Site.IsServer before adding fingerprint, SRI and minify to your assets.Customizing our sass variables with .ExecuteAsTemplate
Let’s level up and talk advanced Hugo Piping!
Wouldn’t it be great if our users could define the colour of our theme or project’s general text, or the background colour of their header?
All of this without writing some ugly <style>
tags in our template, but by naturally updating our Sass variables? I know I’d love that!
This is how our sass/main.scss
currently looks like.
$backgroundColor: #e6e4e4;
$textColor: #000000;
@import "header";
@import "main";
// Etc...
What we want is to replace those variables’ value with some Go Template code using .Param
.
Let’s set our variables in our config.yaml
params:
style:
backgroundColor: maroon
textColor: red
Now, let’s edit our sass/main.scss
file with some Go Template magic:
$backgroundColor: {{ .Param "style.backgroundColor" }};
$textColor: {{ .Param "style.textColor" }};
@import "header";
@import "main";
// Etc...
From now on, we cannot use this asset file as is, it needs to be rendered with .resources.ExecuteAsTemplate
so we’ll need to do something like this:
{{ $style := resources.Get "sass/main.scss" | resources.ExecuteAsTemplate "style.scss" . }}
Above, we retrieved the sass file and turned it into a resource. Then using Go Pipes we applied resources.ExecuteAsTemplate
on it.
As first argument we set a filepath for the file.
As a second argument, much like a partial
, we passed a context to be used from within our file. Here, the dot is our page context from which we can use .Param
.
Now $style
will be written with its customizable variables into a processable Sass file.
We’ll be able compile it to CSS, minify it, fingerprint it and drop its .Permalink
like any other resource!
Customizing our JS variables with .FromString
Back to our javascript. Some .Site.Params
and Front Matter needs to be exploitable from our javascript file because:
- Our carousel lazyloads cloudinary images, we need the project’s cloudinary root url.
- A component in our script uses a weather API to display a travel post’s city temperature. So we either need the page’s
weather_location
Front Matter, or our project’s default.Site.Params.weather_location
.
Our configuration looks like this:
#config.yaml
params:
cloudinary: https://res.cloudinary.pipeit
weather_location: "Montreal, CA"
One of our page’s Front Matter looks like this:
# content/post/touring-the-apple.md
title: Touring the Apple!
weather_location: "New York City, NY, USA"
We want to inject those variables in a separate script tag to make sure it is available very early and for all of our scripts. This is what our code will look like:
{{ $string := (printf "var cloudinary_url = '%v'; var weather_location = '%v';" (.Param "cloudinary") (.Param "weather_location") ) }}
{{ $filePath := printf "vars.%x.js" (.Param "weather_location") }}
{{ $vars := $string | resources.FromString $filePath }}
<script type="text/javascript" src="{{ $vars.Permalink }}"></script>
What now? 🧐
What we do above is hard to read but easy to explain.
{{ $string := (printf "var cloudinary_url = '%v'; var weather_location = '%v';" (.Param "cloudinary") (.Param "weather_location") ) }}
First we use printf
to build a string which will replace every %v
verb with our properties’ respective value, producing something like the follwing.
var cloudinary_url = 'https://res.cloudinary.pipeit'; var weather_location = 'Montreal, CA';
Next:
{{ $filePath := printf "vars.%x.js" (.Param "weather_location") }}
Every resource sharing the same filepath will inevitably overwrites each other. Here we will have several variations of our vars.js
throughout our site, we need to specify a unique file path for every version of it.
We choose to use its only changing factor, weather_location
, to ensure Hugo only builds one variation of vars.js
per existing location.
To make this unique string safely useable as filename, we use printf
again but this time with the verb %x
with will be replaced by a base 64 representation of our weather_location
.
From now on if two pages use our default beautiful Montréal, CA
, they’ll use the same resource, while this other page written from New York will use it’s own Manhattan style vars.n3wy0rkc1ty.js
!
{{ $vars := $string | resources.FromString $filePath }}
<script type="text/javascript" src="{{ $vars.Permalink }}"></script>
Those two last lines are pretty self explanatory. We create the resource from our string using resources.FromString
while passing its unique $filePath
as parameter. Lastly we drop its .Permalink
as our script’s src
.
We could even improve this by directly outputting the content of our resource in a <script>
tag in order to save us an unnecessary request.
<script type="text/javascript">{{ $vars.Content | safeJS }}</script>
.Permalink
, we still need to define a unique filepath for Hugo to correctly tell the two resource variations appart.Conclusion 🏁
By using using Hugo’s built-in asset pipeline Hugo Pipes, we were able with very few lines of code:
- To build a customizable CSS asset using SASS.
- To apply « autoprefix » on the resulting
.css
file. - To mix our different script files into two distinct bundles ready to be called in our pages.
- To output some Javascript user defined variables unique to several pages.
- To minify, fingerprint and SRI secure all of the above.
Even if many will still need nodeJS and npm
because of PostCSS or any other asset pipeline not yet covered by Hugo Pipes, this asset revolution will undoubtedly change the way we built our projects.
I know it has already heavily changed mine and hope it will soon change yours.
If it has already, feel free to share your Hugo Pipes own experiments and implementations in the comments!