A Powerful Blog Setup with Hugo and NPM

Hugo is a very popular static site generator used for blogs and other static websites. With Hugo, we can create layouts and content and package it all into a nice bundle that a web server can then serve to our readers’ browsers.

While Hugo (or any static site generator, for that matter) does a good job with templating and transforming our content into nice-looking HTML, it lacks functionality to support custom processing tasks, for example to automatically resize images or minify Javascript files.

Tasks like these are where the Javascript ecosystem shines. So in this article, we’re going to wrap our Hugo project into an NPM (Node Package Manager) project to have all the power of the Javascript ecosystem at our fingertips when building our blog.

Example Project

You can find the complete example discussed in this article on GitHub.

Why Use NPM?

Before we dive into how to combine Hugo and NPM, let’s discuss the perks we get when we do it.

First of all, when we use NPM, we don’t have to install Hugo any more. That may not be a big deal for when we’re working on our local machine to write and preview our articles, but as soon as we want to create an automated pipeline that builds and publishes our blog from a remote computer, this is a big deal. We no longer have to install Hugo on that machine. Instead, NPM will take care of it. And pretty much all of the automation / continuous deployment systems out there support NPM.

Also, with NPM, we can add custom steps to the build process of our blog. Hugo takes care of templating and creating HTML, but has little support for processing images, for example. With NPM, we can use any of a million of Javascript packages to build our blog (I don’t know the exact number, but a million is probably still a conservative estimate). This includes tasks like:

  • resizing images,
  • converting images into web-optimized formats like WEBP,
  • minifying Javascript and CSS files,
  • using CSS preprocessors like SASS to better manage our CSS styles,

We can basically add any step we can think of to the build pipeline of our Hugo project. There’s probably an NPM package for it!

So let’s dive right in!

Setting Up an NPM Project

Before we start, we need to have Node.js installed, which comes bundled with NPM nowadays. This is the only thing we’ll have to install manually, because, after this, NPM will take care of installing everything we need, including Hugo.

If you haven’t installed Node yet, you can get it from the download page.

Next, we create a new NPM project by typing this command into the console:

npm init

NPM will ask some questions about the project name and similar things. We can just answer them with the default answers by hitting “enter”.

The result will be a file package.json that looks like this:

{
  "name": "hugo-npm",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

The package.json file is the heart of an NPM project. It defines scripts that we can call to interact with the project and also dependencies on any NPM package we might need (though there are no dependencies in a fresh project).

Let’s tell the NPM project to work with Hugo, next.

Installing Hugo

Installing Hugo into our NPM project is as easy as calling this command:

npm install --save-dev hugo-bin 

This will download the hugo-bin package into the node_modules folder and add the dependency to our package.json file, which now contains a dependencies section:

{
  ...
  "dependencies": {
    "hugo-bin": "^0.60.0"
  } 
}

We now have access to the Hugo command from within our project. Let’s set up a Hugo project, next.

Setting Up a Hugo Project

Setting up a Hugo project within an NPM project is very similar to the standard Hugo quickstart. Remember that we haven’t installed Hugo on our machine, though, so we have to call Hugo through the hugo-bin package we have installed above:

npx hugo new site . --force

The npx command executes a certain npm package. In this case, it’s executing the hugo-bin package and uses the new site command to have Hugo create an empty site for us. We need to add the --force parameter because we already have some content in our folder.

Next, we proceed with the steps from the official Hugo quickstart:

git init
git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke

This initializes a Git repository in the current folder and adds the “Ananke” theme to our blog project. We can also choose any other theme, of course.

We’re ready to run Hugo now!

Running Hugo

To work with Hugo, we add the following scripts to our package.json:

{
  ...
  "scripts": {
    "hugo:build": "hugo -d build",
    "hugo:serve": "hugo server",
    "hugo:clean": "rm -rf build resources public"
  },
  ... 
}

Note that within the scripts section we’re adding commands that look just like usual hugo commands. The script hugo:build will run Hugo to package the site for us, the script hugo:serve starts the Hugo server, and the script hugo:clean removes some folders which contain files that have been created by a previous build.

Each of these scripts we can now run with the command npm run <script>.

When we run npm run hugo:serve now, we should be able to access the (empty) blog at http://localhost:1313.

Polishing the Project

To give some final polish to the project, let’s add some more scripts:

{
  ...
  "scripts": {
    "build": "npm run hugo:build",
    "clean": "npm run hugo:clean",
    "serve": "npm run hugo:serve",
    "hugo:build": "hugo -d build",
    "hugo:serve": "hugo server",
    "hugo:clean": "rm -rf build resources public"
  },
  ... 
}

We created another script for each of our previous scripts without the hugo: prefix that simply forwards the call to the script with the hugo: prefix. Why would we do that?

The reason is that we might want to add certain processing steps in the future (image resizing, Javascript minification, …), which have nothing to do with Hugo, so they shouldn’t be added to the hugo scripts. We can add steps like these into the more generic build, clean, and serve scripts, for example like this:

{
  ...
  "scripts": {
    ...
    "build": "npm run images:resize && npm run minify && npm run hugo:build"
  },
  ... 
}

Also, npm run serve is less to type than npm run hugo:serve.

Conclusion

That’s it! We have a fully functional Hugo project that lives within an NPM project. With NPM we have access to all NPM packages out there that can do cool things like image processing, Javascript and CSS minification, or CSS processing.

If you want to play around with the setup described in this article, you can find it on GitHub.