Minify CSS and JS with Hugo Pipes and Module Mounts

Sensible optimisation without the overheads

For the smaller Hugo projects I manage, any JS is deliberately light touch and for optimisation I will avoid a framework because of the overheads and long term maintenance, but some online experiences are enhanced by some small scripts.

It makes sense to keep as much code on one server in as few calls as possible, and with so many optimisation tools now available, this becomes a blessing and a curse.

I’m always keen to avoid overheads on so many frameworks and dependencies - and this is where Hugo sits well.

Almost every JS script has an npm install/update option, which is great, because it makes updates and maintenance a bit more manageable. However, the key reference I’m likely to need is a main .js file, which is usually easily available usually on a CDN to test. NPM, at least on my setup with Hugo, will put all the files into the node_modules folder so you can set something up of the back of this.

The /node_modules folder isn’t something I want to fiddle with, it’s referenced in .gitignore and generally contains about 178,685 (possibly an overegging here) files on average. So even if you know where your file you want to reference is, Hugo can’t see it, as it’s both in the /node_modules and not in the repository via .gitignore.

At this point, to build a central single compressed JS file, the usual way is to npm install another 1678 files and dependences, be it brilliant tools like Babel, Rollup, RequireJS or something else.

Enter Hugo Pipes and Module Mounts

To avoid this stack up of ‘more stuff’, I resolved there must be a Hugo method. Hugo Pipes is good here, this is where you can easily concact and minify CSS (PostCSS built in) and JS with fingerprints to avoid cache issues.

For example, if you would like to process a CSS file, it may look something like:

{{ $style := resources.Get "css/styles.css" | postCSS (dict "config" "./assets/css/postcss.config.js") | minify | fingerprint }}
  <link rel="stylesheet" href="{{ $style.Permalink }}" integrity="{{ $style.Data.Integrity }}">

This would also for JS files. But… I know where the styles.css is. Some JS files are hiding in the //node_modules folder. I don’t want to edit or be clever with the file, I just want to make one file with a couple of other small scripts. Hugo Pipes can do what I want, I just can’t get to the files easily.

My example here is deliberately heavy. I want to make one large JS file of all my scripts and compress it on build. Here, I’ve got 2 common JS solutions, Macy.js and Swiper.js that I’ve installed and have other small scripts of my own.

For my own scripts, I have one called menu.js and accordian.js - I know where these are and can bring these files together into one new one.

I start by getting these resources into their own variable:

{{ $accordianconfig := resources.Get "js/accordian.js" }}
{{ $menuconfig := resources.Get "js/menu.js" }}

Next, make the JS file for use (usually on the baseof.html file):

{{ $concatjs := slice $menuconfig $accordianconfig | resources.Concat "js/scripts.js" | resources.Minify }}

And this can be added into the template with:

<script src="{{ $concatjs.Permalink }}"></script>

I needed to get at those files hidden away in the /node_modules folder though, as there was no way Hugo was going to look at them. I really didn’t want to add to my npm jenga stack.

The answer is in the shape of a config setting: mounts. What this does is create a reference shortcut or symlink from outside into a project folder of your choice (as I understand it). I could now get access to the deep buried JS files in /node_modules though, but I did need to make them available in the repo, which meant editing the .gitignore file.

This gets messy, but I resolved to add the whole JS folder rather than just the file I wanted. I short, this works:


Now I could see the files I can reference them and give them a source in config.toml. In the target here, “node_modules” is just my name target of choice, this could be confusing, but it’s keeping it sensible for me.

    source = "themes/sitename/node_modules/swiper/js/swiper.js"
    target = "assets/node_modules/swiper.js"
    source = "themes/sitename/node_modules/macy/dist/macy.js"
    target = "assets/node_modules/macy.js"

Now this is available, I can return to the templates and call these files in another variable.

{{ $swiperconfigjs := resources.Get "node_modules/swiper.config.js" }}
{{ $macyjs := resources.Get "node_modules/macy.js" }}


{{ $concatfromnodemodulesjs := slice $swiperconfigjs $macyjs | resources.Concat "js/scripts.js" | resources.Minify }}

Joining it altogether

I can now take a number of scripts and feed them through… but even better, I can also create multiple files, so, where we have a need for example a gallery, we only need to have the swiper code on that page. This full example takes that into account.

One more thing to add, as above in the example but we can add another step by assing a secure fingerprint when joining these files as well:

There’s probably a smarter and cleverer way to achieve all this… but it works.

{{ $swiperconfigjs := resources.Get "node_modules/swiper.config.js" }}
{{ $macyjs := resources.Get "node_modules/macy.js" }}
{{ $macyjsconfig := resources.Get "js/macy.config.js" }}
{{ $menuconfig := resources.Get "js/menu.js" }}
{{ $accordianconfig := resources.Get "js/accordian.js" }}

{{ $concatalljs := slice $macyjs $macyjsconfig $menuconfig | resources.Concat "js/bundle-all.js" | resources.Minify }}
{{ $concatjs := slice $menuconfig | resources.Concat "js/bundle.js" | resources.Minify }}
{{ $secureJS := $concatjs | resources.Fingerprint "sha512" }}
{{ $secureallJS := $concatalljs | resources.Fingerprint "sha512" }}

{{ if eq .RelPermalink "/gallery/" }}
<script src="{{ $secureallJS.Permalink }}" integrity="{{ $secureallJS.Data.Integrity }}"></script>
{{ else }}
<script src="{{ $secureJS.Permalink }}" integrity="{{ $secureJS.Data.Integrity }}"></script>
{{ end }}

References and thanks to:

National Map Centre Logo