Build Steps Using NPM Scripts for My Hugo Blog
Quite recently I migrated this blog from WordPress to Hugo. Since I didn’t want to use a theme built by someone else, I had to add things like CSS and JavaScript myself. To be able to work with this locally in an efficient way and to be able to produce a complete build output in a reproducible manner, I had to automate the build steps. With WordPress I used Gulp for this, but I thought that might not be needed, so I made an attempt to do this using only npm scripts.
What I needed support for, was the following:
- Modern JavaScript to ES 5 compilation
- Sass to CSS compilation
- Live reload on all assets
- Search indexing
- Bundling and minification
These are the scripts I ended up with. The full package.json
can be found here.
Structure of scripts
To be able to control the order of execution and to avoid having one horribly long line, the tasks are broken down in smaller pieces and then composed accordingly. To run the blog on my local machine I run npm run start
(yes, the run
part can be skipped in this case). This simply runs five commands in order, among which four of them are other npm scripts: clean
, sass:build
, js:build
and serve
.
The other main script is build
, which is run by my build and hosting service Netlify. More on this later.
Cleaning
To have a clean start when doing sequential builds, I have two clean scripts. The reason for this is explained below, but the output folder is named public
in this setup, so that’s what needs to get cleaned.
Building JavaScript
To produce a JavaScript bundle that can run in web browsers, I use browserify and babelify to be able to use newer JavaScript syntax. This is done in script js:build
.
Building CSS
Compiling Sass to CSS is straight forward and done with node-sass. The script sass:build
specifies both output bundles, site.css
and print.css
.
The Hugo stuff
Hugo produces HTML from my content files in Markdown with the associated page templates. Since Hugo is the most performant part of this setup and includes a great web server, I use that to run the site locally. Looking at the script hugo:watch
you can see that I also have a different config file for Hugo when running locally. This is to disable Google Analytics and Disqus, which is pointless when running locally.
Search indexing
The technique I use to provide search might be worthy a separate post, but in essence it’s a separate Hugo template that outputs all blog posts into one file, data-to-index.json
. That file is then indexed using Lunr through a script I wrote myself, index-search.js
. To be able to have search locally, I have a separate clean
script that does not remove the entire public folder, since that would get me into a catch 22 situation. So to work with the “front-end” part of search, I run hugo && npm run index && npm run start
. That way the index is produced once and I can still have live reload on everything else. The timestamp
script simply adds a timestamp to the index file name to avoid caching issues.
Live reload
One of the best things in recent years for web development in my opinion, is live reload. To be able to save a file and very quickly see the changes reflected without any additional action, makes it so much easier to stay focused on what you’re doing. Here I use onchange
to listen to changes in the source files. The script js:watch
watches the js files and simply calls the js:build
script when a change is detected.
Producing a complete build to deploy
The only thing special to the scripts with a :prod
suffix, is that they add minification to the bundles. By running npm run build
I get a public
folder ready to be deployed.