Forem

Luke Harris
Luke Harris

Posted on • Originally published at lkhrs.com on

PurgeCSS with Hugo

Over the past several months I slowly rebuilt the theme for my site with Pico.css, with the goal of eliminating the div soup I had. This past week, my site stopped building altogether because of a dependency issue1, and I polished up the theme last night for deployment.

One win from this rebuild is that I finally got PurgeCSS and cssnano 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. Here’s the file in full:

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']
        })
    ]
};

Enter fullscreen mode Exit fullscreen mode

And the Hugo partial for processing the SCSS:

{{ $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 }}

Enter fullscreen mode Exit fullscreen mode

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 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. ↩︎

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (0)

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay