Hugo, Tailwind, and Netlify

Published April 27, 2023
Updated April 27, 2024

I recently hopped on the Tailwind CSS bandwagon, and I’ve been thoroughly enjoying how productive I feel building web pages. I also wanted to revamp this site, which was already built with Hugo and deployed on Netlify . Here’s the configuration I ended up with.

Assuming there’s an existing Hugo site, start by initializing an npm package.

npm init

Next we can install and initialize Tailwind:

npm install tailwindcss
npx tailwindcss init

For a Hugo site, we definitely want to utilize Tailwind classes in our layouts, so a barebones tailwind.config.js would be:

module.exports = {
  content: ["layouts/**/*.html"],
  theme: {
    extend: {},
  },
  plugins: [],
};

We’ll add the base CSS file assets/global.css that pulls in the Tailwind defaults:

@tailwind base;
@tailwind components;
@tailwind utilities;

PostCSS

PostCSS makes it easy to transform CSS with JavaScript plugins, and Tailwind makes itself available as such a plugin. To get started, install PostCSS and the related tooling:

npm install postcss postcss-cli autoprefixer

We can now configure PostCSS in postcss.config.js to utilize Tailwind and autoprefixer:

module.exports = {
  plugins: [require("tailwindcss"), require("autoprefixer")],
};

Development Server

Hugo provides a PostCSS pipe to preprocess your CSS. The problem is that this does not work well with Tailwind’s JIT compiler since Hugo only refreshes changed files, and it does not know that the CSS may have changed when a class is added to a layout file. The easiest way to get around this is to run the Hugo server and PostCSS process separately.

The two commands we’ll be running are:

hugo server
postcss ./assets/global.css -o ./assets/dist/global.css --watch

To make this as easy as possible to run, we’ll use concurrently to wrap these commands into a single NPM script. First, add concurrently to the package:

npm install --save-dev concurrently

Then add the following scripts to package.json:

{
  "scripts": {
    "dev": "concurrently \"npm:dev:*\"",
    "dev:css": "postcss assets/global.css -o assets/dist/global.css --watch",
    "dev:server": "hugo server"
  }
}

Don’t forget to reference the built CSS in your Hugo templates, and add the assets/dist directory to .gitignore.

Now we can run npm run dev and get a live-reloading server that recompiles our CSS on every change.

Production Build

Our production build is similar to our development build, just without the live reloading:

postcss assets/global.css -o assets/dist/global.css
hugo

Again, we’ll wrap these into a single NPM command for convenience:

{
  "scripts": {
    "build": "npm run build:css && npm run build:site",
    "build:css": "postcss assets/global.css -o assets/dist/global.css",
    "build:site": "hugo"
  }
}

We can now use npm run build to create our static site!

Netlify

Assuming you’ve already added your site in Netlify, there is a small amount of configuration we need to add to our repository in netlify.toml:

[build]
  publish = "public"
  command = "npm run build"

[build.environment]
  HUGO_VERSION = "0.111.3"

The publish option tells Netlify which directory to publish. Hugo outputs into the public/ directory by default.

We set command to our nicely packaged npm run build script. The presence of a package.json file causes Netlify to install the packages by default, so we don’t need to worry about that

Finally, we pin a HUGO_VERSION since the hugo provided in the base image is most likely older than what we want. Netlify automatically installs the pinned version .