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.
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
| File | Before | After |
|---|---|---|
| stylesheet.css | 168 KB | 31 KB |
| Lighthouse unused CSS | 82% | 11% |
| FCP | 1.9s | 1.4s |
CI: GitHub Action runs npm run build — adds ~4s to pipeline.