NNicheStackTutorials
← All tutorials
Hugo

Remove Unused CSS from a Hugo Theme (PurgeCSS Pipeline)

Cut 140 KB of dead CSS from PaperMod without ejecting the theme. Hugo Pipes + PurgeCSS in CI.

·12 min read·Hugo
Remove Unused CSS from a Hugo Theme (PurgeCSS Pipeline) cover

Tested on: Hugo 0.139.4, PaperMod v8, PurgeCSS 6.0.0, Node 20 Before: 168 KB CSS transferred. After: 31 KB.

Problem

Lighthouse flagged "Reduce unused CSS" — 82% of stylesheet.css was dead rules for features I disabled (search modal, archives layout, code-copy buttons).

Strategy

Don't edit theme CSS by hand. Pipe bundled CSS through PurgeCSS at build time scanning public/**/*.html.

Step 1: npm in site root

npm init -y
npm install -D @fullhuman/postcss-purgecss postcss postcss-cli

Step 2: Hugo asset bundle

layouts/partials/head.html override — replace theme CSS link with:

{{ $opts := dict "targetPath" "css/main.css" }}
{{ $base := resources.Get "css/extended/empty.css" | default (resources.FromString "empty.css" "/* */") }}
{{ range site.Params.customCSS }}
  {{ $base = slice $base (resources.Get .) | resources.Concat "css/pre.css" }}
{{ end }}
{{ $css := resources.Get "css/extended" | resources.Match "**/*.css" | resources.Concat "css/all.css" }}
{{ $out := $css | postCSS $opts | minify | fingerprint }}
<link rel="stylesheet" href="{{ $out.RelPermalink }}" integrity="{{ $out.Data.Integrity }}">

Simpler approach that worked for me — post-process theme output:

Step 3: post-build purge script

package.json:

{
  "scripts": {
    "build": "hugo --minify && node scripts/purge-css.mjs"
  }
}

scripts/purge-css.mjs:

import { PurgeCSS } from 'purgecss';
import fs from 'node:fs';
import path from 'node:path';

const out = path.join('public', 'assets', 'css');
const cssFile = fs.readdirSync(out).find(f => f.endsWith('.css'));
if (!cssFile) process.exit(0);

const filePath = path.join(out, cssFile);
const css = fs.readFileSync(filePath, 'utf8');

const purgeCSSResult = await new PurgeCSS().purge({
  content: ['public/**/*.html'],
  css: [{ raw: css }],
  safelist: {
    standard: [/^hljs/, /^chroma/, 'dark', 'light', 'active', 'open'],
    greedy: [/^post-/, /^hc-/]
  }
});

fs.writeFileSync(filePath, purgeCSSResult[0].css);
console.log('Purged', cssFile, css.length, '→', purgeCSSResult[0].css.length);

Safelist is critical

First build stripped Chroma syntax classes — code blocks went unstyled.

Add greedy patterns for any class toggled by JS or shortcodes.

Errors

Error: ENOENT public/assets/css

PaperMod fingerprints to public/assets/css/stylesheet.*.css only after hugo. Run purge after Hugo.

Dark mode broken in production

prefers-color-scheme: dark rules purged because no HTML contained string dark. Safelist dark.

hugo.toml enable hooks

[module.hugoVersion]
  min = "0.112.0"

Verify

npm run build
ls -la public/assets/css/
# compare size before/after

npx lighthouse http://localhost:1313 --only-audits=unused-css-rules

Visual regression: spot-check homepage, single post, tag page, 404.

Results

FileBeforeAfter
stylesheet.css168 KB31 KB
Lighthouse unused CSS82%11%
FCP1.9s1.4s

CI: GitHub Action runs npm run build — adds ~4s to pipeline.

Tags: HugoCSSPurgeCSSPerformance