26.1 SCSS Compilation with Hugo Extended: toCSS
Right, let’s talk about getting Hugo to turn your elegant SCSS into the blunt, browser-ready CSS it so desperately needs. This isn’t magic, it’s Hugo’s toCSS pipeline, and it’s the single most important reason you need the “Extended” version of Hugo. The regular version is a brilliant static site generator; the Extended version is that plus a full-fledged asset pipeline. Don’t even try this with the regular version. You’ll just get errors and a profound sense of disappointment, and I won’t feel bad for you.
The core concept is simple: you point Hugo at an .scss (or .sass) file, and it compiles it for you, on the fly, during the build process. No separate Node.js sass script, no Grunt, Gulp, or Webpack configs to wrestle with (unless you want to, you masochist). Hugo handles it internally using the LibSASS library. It’s fast, it’s integrated, and it just works.
The Absolute Basics: A Simple SCSS Workflow
You don’t need a complex setup to get started. The simplest way is to create an SCSS file in your assets/ directory. Hugo automatically processes anything in assets/ through its pipelines. Let’s say you create assets/scss/main.scss.
// assets/scss/main.scss
$primary: #3b82f6; // Let's use a nice Tailwind-ish blue
body {
font-family: sans-serif;
color: #333;
background-color: lighten($primary, 45%);
}
.header {
background-color: $primary;
color: white;
padding: 2rem;
}
To get this into your site, you reference this file in a template—likely your baseof.html—using Hugo’s resources function. You fetch the SCSS file and pipe it through toCSS.
{{/* layouts/_default/baseof.html */}}
<head>
...
{{ with resources.Get "scss/main.scss" | toCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{ end }}
</head>
Let’s break down that chained pipeline because it’s beautiful:
resources.Get "scss/main.scss": Grabs the raw SCSS file.| toCSS: The star of the show. Compiles SCSS to CSS.| minify: Strips all the whitespace and comments for production. You want this.| fingerprint: Appends a unique hash to the filename (e.g.,main.abcd1234.css) for cache-busting. When you update your CSS, the filename changes, forcing browsers to download the new version. Always do this in production.
The Nitty-Gritty: Imports, Partials, and That Damn Underscore
You’re not writing all your styles in one file. That’s madness. You’ll want to break things up into partials. SCSS uses the @import directive for this, and Hugo’s LibSASS handles it perfectly, but there’s a major gotcha you need to understand.
Hugo’s toCSS is not a pre-processor; it’s a compiler. This is a crucial distinction. It expects the file you directly resources.Get to be a complete SCSS file. You cannot resources.Get a partial—a file whose name begins with an underscore (_components.scss). Those files are meant to be imported into a main file, not compiled on their own.
The correct, and frankly excellent, way to structure this is:
- Create a directory like
assets/scss/. - Inside it, create your main file:
main.scss. - Also inside it, create your partials with leading underscores:
_variables.scss,_components.scss,_layout.scss.
Your main.scss file then acts as a manifest, importing everything else:
// assets/scss/main.scss
// These lines are literally just importing the other files.
// Hugo/LibSASS will find them and pull them in.
@import 'variables';
@import 'layout';
@import 'components';
And your partials live right next to it:
// assets/scss/_variables.scss
$primary: #3b82f6;
$secondary: #10b981;
$accent: #f59e0b;
// assets/scss/_components.scss
.btn {
padding: .5rem 1rem;
border-radius: .25rem;
background-color: $primary;
color: white;
&:hover {
background-color: darken($primary, 10%);
}
}
The magic is that you only ever call resources.Get on scss/main.scss. Hugo sees the @import statements and handles the rest, compiling it all into one beautiful, minified, fingerprinted CSS file. This structure is clean, logical, and works flawlessly.
Configuration: When You Need to Go Deeper
Sometimes LibSASS’s default behavior isn’t enough. For example, maybe you need to add an extra import path so Hugo can find SCSS libraries you’ve installed via npm. This is where Hugo’s top-level config file (hugo.toml or config.toml) comes in.
[module]
[[module.mounts]]
source = "node_modules"
target = "assets/pkg" # Makes your node_modules available inside assets/pkg/
[params]
[params.sass]
# This is the equivalent of Sass's --load-path flag.
loadPaths = ["assets/pkg/bootstrap/scss"]
With this config, you could now @import 'bootstrap'; in your main.scss and it would successfully find Bootstrap’s SCSS in node_modules/bootstrap/scss because Hugo has mounted it to assets/pkg/bootstrap/scss. It’s a bit of configuration upfront that saves you from the hell of relative path spaghetti later.
The toCSS function also accepts options directly for things like the output style (nested, expanded, compact, compressed). In practice, you’ll just use minify after it, which achieves the same result as compressed, so you can mostly ignore this.
The bottom line is this: Hugo’s integrated SCSS compilation is a killer feature. It removes an entire class of build tooling complexity. Respect its opinions—especially about partials and the assets/ directory—and it will reward you with a stupidly simple and incredibly fast styling workflow. Now, let’s make it look good with Tailwind.