25.3 CSS Processing: toCSS, PostCSS, and autoprefixer
Right, let’s talk about making your CSS less of a mess and more of a… well, a slightly more organized mess that actually works across browsers. Hugo gives us a fantastic toolkit for this, and if you’re not using it, you’re essentially writing your stylesheets with a rock and a chisel. We’re going to cover the three big hitters: toCSS, PostCSS, and the lifesaver known as autoprefixer.
First, the basics. You don’t just throw a regular .css file in your assets directory and call it a day. Instead, you process it. This usually means you’ll create a file with a special extension, like styles.css or, my personal favorite, something.scss (even if you’re not using Sass!). Why? Because this tells Hugo’s asset pipeline, “Hey, I need you to do something to this file before you serve it.”
The workhorse here is the resources.ToCSS pipe. You’ll use it in your templates to grab that raw asset and transform it.
{{/* Grab the CSS asset and process it */}}
{{ with resources.Get "sass/main.scss" | toCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{ end }}
See what we did there? We Get the file, pipe it toCSS, then we minify it (because we’re not monsters), and finally fingerprint it for cache busting. This chain is the golden path. The toCSS function is what triggers the whole processing party.
What the Heck is PostCSS, Anyway?
You might be thinking, “I used .scss but I’m not writing Sass! Is Hugo broken?” No, you’ve just stumbled into the mildly confusing but incredibly powerful world of PostCSS. resources.ToCSS uses PostCSS under the hood by default. Think of PostCSS not as a preprocessor itself, but as a post-processor. It’s a tool that takes finished CSS and transforms it using plugins.
Hugo runs it with a default set of plugins, which is why it accepts both .css and .scss files. The .scss extension is important because it allows for SCSS-like syntax (like nested rules) to be parsed correctly, even if you’re not using the full Sass compiler. It’s Hugo’s way of being helpful. So yes, you can write modern CSS with nesting right now:
/* assets/sass/main.scss */
.card {
background: white;
border-radius: 0.5rem;
/* This nested syntax will work! */
&__title {
font-size: 1.5rem;
color: #333;
}
&:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
}
When processed with toCSS, this will output perfectly valid, flat CSS that even IE11 (may it rest in peace) could theoretically understand: .card { }, .card__title { }, .card:hover { }.
Configuring Your PostCSS Plugins
Now, the default PostCSS setup in Hugo is… fine. But the real power comes from bringing your own plugins. This is where you create a postcss.config.js file in your project root. Hugo will automatically pick this up when you run toCSS.
Why would you do this? Maybe you want to use Tailwind CSS, or you’re a fan of the postcss-preset-env plugin which lets you use future CSS features today. Here’s a basic config that adds autoprefixer (which we’ll get to in a second) and cssnano for minification (though Hugo’s built-in minify is often good enough).
// postcss.config.js
module.exports = {
plugins: {
'autoprefixer': {},
'cssnano': { preset: 'default' }
}
};
The crucial thing to remember here: the moment you provide a postcss.config.js file, Hugo’s default plugins are disabled. You’re taking full control. This is a classic “power vs. responsibility” moment. If you want those handy nested rules from the default setup, you need to explicitly add a plugin for it, like postcss-nesting.
The Non-Negotiable: autoprefixer
This is the single most important plugin you will ever use. Writing vendor prefixes manually (-webkit-, -moz-, -ms-) is a form of self-punishment reserved for developers who also enjoy manually merging Git conflicts. autoprefixer does this for you automatically based on the browsers you want to support.
You configure your target browsers in a .browserslistrc file in your project root. This is a standard, not a Hugo-specific thing.
# .browserslistrc
last 2 versions
> 1%
not dead
This tells autoprefixer (and other tools like Babel) to support the last two versions of all browsers, browsers with more than 1% global usage, and browsers that have not been officially “dead” for more than 24 months.
With this setup, you can write clean, modern CSS:
.example {
display: flex;
backdrop-filter: blur(10px);
user-select: none;
}
And autoprefixer will magically transform it into:
.example {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
It’s like having a brilliant intern who’s obsessed with browser compatibility. The best part? As browser support changes, you update your .browserslistrc and regenerate your site—no need to touch your actual CSS.
Common Pitfalls and How to Avoid Them
- The Missing Config Trap: Remember, a custom
postcss.config.jsoverrides Hugo’s defaults. If you suddenly lose your CSS nesting and can’t figure out why, you probably created a config file and forgot to addpostcss-nestingto it. - The Pathing Puzzle: Your
postcss.config.jsand.browserslistrcfiles need to be in the root of your Hugo project, not in theassetsfolder. I’ve seen people do it. Don’t be that person. - The Chaining Order: The order of your Hugo pipe matters. Always run
toCSSbefore youminify. Minifying a raw.scssfile will give you a broken, minified SCSS file, which is useless to a browser. - The Live-Reload Gotcha: When you’re using a custom
postcss.config.js, you might need to restart Hugo’s server for the changes to take effect. It doesn’t always pick up config changes on the fly, which can lead to hours of confused screaming. Save yourself the time; just restart it.
The beauty of this whole system is that it happens at build time. The user’s browser gets a single, minified, prefixed, and fingerprinted CSS file. It’s the fastest, most robust way to handle your styles. It requires a bit of initial setup, but the payoff is not having to think about CSS plumbing ever again. And that, my friend, is worth its weight in gold.