PurgeCSS with Hugo
Published: 2022-11-07 01:08:42

---
title: PurgeCSS with Hugo
published: true
date: 2022-10-29 17:59:15 UTC
tags: 
canonical_url: https://www.lkhrs.com/blog/2022/10/postcss-hugo/
---

Over the past several months I slowly rebuilt the theme for my site with [Pico.css](https://picocss.com), with the goal of eliminating the `div` soup I had. This past week, my site stopped building altogether because of a dependency issue<sup id="fnref:1"><a href="#fn:1" role="doc-noteref">1</a></sup>, and I polished up the theme last night for deployment.

One win from this rebuild is that I finally got [PurgeCSS](https://purgecss.com) and [cssnano](https://cssnano.co) working with Hugo as part of my build pipeline. Although the savings were small, in the range of 1-2kb after Brotli compression, it was bugging me that it wasn’t working.

**The first insight** was finding out I had Hugo’s `.Resources.PostProcess` and `.Resources.PostCSS` functions flipped in my head — the correct usage is to run `.Resources.PostCSS` on your input from Hugo Pipes, and then tag the output with `.Resources.PostProcess`.

Tagging the output with PostProcess tells Hugo not to run that file through PostCSS until after it’s built the pages, which is important because PurgeCSS runs through the HTML (or `hugo_stats.json`) to figure out which CSS selectors are used on the pages, and which ones it can get rid of.

**The second insight** was that Hugo’s built-in selector detector (new band name) wasn’t catching the conditional selectors used in Pico, like `:where` and `:is`, resulting in significant savings in file size while leaving my site looking like it drove the wrong lane down a dirt road.

To fix that, I stopped using `hugo_stats.json` for PurgeCSS and used the output folder of HTML files instead. Then I added a whitelist to my `postcss.config.js` based on [Tom’s post](https://ttntm.me/blog/hugo-tailwind-boilerplate/). Here’s the file in full:

```javascript
const purgecss = require('@fullhuman/postcss-purgecss');

module.exports = {
	plugins: [
		require("cssnano"),
		purgecss({
			content: ['./public/**/*.html'],
			defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
			safelist: [':hover', ':focus', 'button', 'button:focus', ':where', ':is']
		})
	]
};

```

And the Hugo partial for processing the SCSS:

```go
{{ $tocss := dict "enableSourceMap" true }}
{{ if hugo.IsProduction }}
	{{ $tocss := dict "enableSourceMap" false "output" "compressed" }}
{{ end }}
{{ $styles := resources.Get "scss/styles.scss" | resources.ToCSS $tocss }}
{{ if not hugo.IsProduction }}
	<link href="{{ $styles.RelPermalink }}" rel="stylesheet" />
{{ end }}
{{ if hugo.IsProduction }}
	{{ $css := $styles | minify | fingerprint | postCSS | resources.PostProcess }}
	<link href="{{ $css.RelPermalink }}" rel="stylesheet" />
{{ end }}

```

Now the only issue left to solve is figuring out why PostCSS doesn’t work in GitHub Actions.

There’s a few nitpicks I have with Pico.css, I might swap it out for [Simple.css](https://simplecss.org) now that my site uses semantic HTML. And I’m planning yet another redesign with vanilla CSS — I’ve built two sites that way recently and it’s been wonderful writing plain CSS again, with new features like `calc()` and variables.

* * *

1. Bootstrap 5 now uses SASS features that are incompatible with Hugo’s SASS processor, and I had no luck pinning the working version. [↩︎](#fnref:1)