Trouble with PurgeCSS

I’ve done a bunch of trial and error on this and haven’t figured it out. Hoping someone can point me in the right direction.

I’m trying to optimize my site. The pipeline I’m trying to create for my CSS is: Compile from scss to css → Autoprefix → Minify → Purge.

Having trouble with the Purge part. It either doesn’t do anything or it breaks the site. This is what’s in my <head>:

{{ $sass := resources.Get "scss/styles.scss" }}
{{ $toCssOpt := (dict "includePaths" "node_modules" "targetPath" "/assets/css/styles.css") }}
{{ $postCssOpt := (dict "use" "autoprefixer") }}
{{ $style := $sass | toCSS $toCssOpt | postCSS $postCssOpt }}
{{ if hugo.IsProduction }}
    {{ $style = $style | minify | fingerprint | resources.PostProcess }}
{{ end }}
<link href="{{ $style.RelPermalink }}" rel="stylesheet">

But this doesn’t change the size of my CSS file, it stays at 208kB whether | resources.PostProcess is there or not.
If I swap that out for | postCSS, my CSS does get reduced to 20kB, but it breaks the site, my styles aren’t applied correctly anymore. Some of them are still there though, so it’s not like the CSS is completely gone.
But this isn’t what the docs say to do, so I’m not surprised it doesn’t work. It’s just what I’ve tried that actually does do anything.

I’ve checked and Hugo is creating the hugo_stats.json file, so I think my config.toml is correct.

Here is my postcss.config.js file:

const purgecss = require("@fullhuman/postcss-purgecss")({
  content: ["./hugo_stats.json"],
  defaultExtractor: (content) => {
    const els = JSON.parse(content).htmlElements;
    return [...(els.tags || []), ...(els.classes || []), ...(els.ids || [])];
  safelist: [],
const autoprefixer = require('autoprefixer')

module.exports = {
  plugins: [
    ...(process.env.HUGO_ENVIRONMENT === "production" ? [purgecss] : []),

I am running the server with this command: npx hugo serve --environment production

I’m using Bootstrap to build the site, so there should be a fair bit of CSS that can be purged. Maybe there’s something I have to configure differently because of how Bootstrap is put together?

Here is the whole repo: GitHub - schaefsteven/rocky-hutchins at testing

Do not try to use resources.PostProcess with hugo server, even if you set the environment to production. If you view source in your browser, you probably see something with placeholders:

<link rel="stylesheet" href="__h_pp_l1_1_RelPermalink__e=" integrity="__h_pp_l1_1_Data.Integrity__e=" crossorigin="anonymous">

If you want to see what your site will look like after a production build, use a local server (not hugo server)…

npm install serve
npx serve public

Okay, got it. I do still seem to be having issues though.

It seems to me that resources.PostProcess is not able to find my postcss.config.js file. Do I need to put it somewhere else? The docs I can find on the subject say to put it in the project root, and that’s where it is.

The reason I suspect this is the issue is because if I put an error in postcss.config.js and try to use it with postCSS, it makes the build fail, but if I leave that error in the file and run resources.PostProcess, the build does not fail. Is it looking for a config file somewhere in the public folder?

Also, the file size of my css is not changing when calling resources.PostProcess with purgecss enabled.

This may help you to understand the setup and verify the result:

git clone --single-branch -b hugo-forum-topic-45501 hugo-forum-topic-45501
cd hugo-forum-topic-45501
npm ci
rm -rf resources/ public/ && hugo && more public/main.*.css

The resulting CSS file contains comments to indicate what should or should not be present in the file, verifying that both auto-prefixing and tree shaking are working as expected in a production environment.

Okay, I’m trying to understand this line:
{{ with . | postCSS | fingerprint | resources.PostProcess }}
As far as I can understand it, both postCSS and resources.PostProcess are running PostCSS.

Why are we calling PostCSS twice, and what is changing between those two calls if we are not passing different options to it each time?

Or am I completely misunderstanding this? (I really appreciate your help.)

Side question, with this setup, we are only running Autoprefixer in production, correct? Wouldn’t we want to have Autoprefixer run in our dev environment to check that the css is working correctly in all browsers?

Yes. See

That’s up to you. I trust autoprefixer to do its job when needed.

Got it. I’ve gotten things working now although I still don’t fully understand the syntax and the structure of how PostCSS is being called. For folx that find this thread, here is what is now in my head tag:

{{ $sass := resources.Get "scss/styles.scss" }}
{{ $toCssOpt := (dict "includePaths" "node_modules" "targetPath" "/assets/css/styles.css") }}
{{ $postCssOpt := (dict "use" "autoprefixer") }}
{{ $style := $sass | toCSS $toCssOpt | postCSS $postCssOpt }}
{{ if hugo.IsProduction }}
    {{ $style = $style | postCSS | minify | fingerprint | resources.PostProcess }}
{{ end }}
<link href="{{ $style.RelPermalink }}" rel="stylesheet">

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.