Sass in Hugo

06 Nov 2024

Introduction

Even relatively simple websites can benefit from Sass, to help make their CSS files more structured and less repetitive.

The extended releases of Hugo (those with _extended_ in their names!) come bundled with LibSass, a Sass-to-CSS transpiler wrapper.

So, before anything else, make sure you are running one of these “extended” versions of Hugo. Run the hugo version command, at the command line, and check for “extended” in the version string, shown in the output.

(You can also use a more modern Sass transpiler - Dart Sass) with Hugo, by following these instructions. But LibSass is fine for the purposes of this article.)


Sass-to-CSS Code

The following code transpiles a sass file to a css file. You would typically place this code wherever you need to access the css file - for example in your Hugo template containing <head>...</head> content.

This is probably in layouts/partials/head.html - but your set-up may be different.

Go Template
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{{ with resources.Get "scss/demo.scss" }}
  {{ $opts := dict "transpiler" "libsass"
                   "outputStyle" "expanded"
                   "targetPath" "css/demo.css" }}
  {{ with . | toCSS $opts }}
    {{ if hugo.IsDevelopment }}
      <link rel="stylesheet" href="{{ .RelPermalink }}">
    {{ else }}  
      {{ with . | minify | fingerprint }}
        <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

As with all things Hugo, there are various different ways you can arrange your code and related files. But here is what I did for the above example - a very basic setup:

  1. I created the file assets/scss/demo.scss (creating the scss/ directory along the way).

The file contains this sass:

SCSS
1
2
3
4
5
6
7
$font-stack: Helvetica, sans-serif;
$primary-color: #158aff;

body {
    font: 100% $font-stack;
    color: $primary-color;
}

This is the same sass example that you can find in the official documentation here.

The expected CSS (after transpiling) will eventually be this:

CSS
1
2
3
4
body {
    font: 100% Helvetica, sans-serif;
    color: #333;
}
  1. I added the above Hugo code to my head.html partial.

  2. I built my test site using the hugo server command. This defaults to using a Hugo environment of development.


Detailed Code Walkthrough

The code needs some explaining…

1
{{ with resources.Get "scss/demo.scss" }}

The with command takes the result of the rest of the expression and assigns it to the current context (Hugo’s .). This also has the property of silently skipping the nested commands if the rest of the expression fails (e.g. if the demo.scss file is not found). No error is thrown, in that case.

The resources.Get "scss/demo.scss" command only looks in the assets directory. Yes, there is a separate resources directory, but that is not where .Get looks, by default.

1
2
3
{{ $opts := dict "transpiler" "libsass"
                 "outputStyle" "expanded"
                 "targetPath" "css/demo.css" }}

The above command creates a Hugo variable $opts. It populates that variable with a dict - a dictionary strcture consisting of a collection of key-value items. These are options that we will be passing to the sass transpiler.

I have laid out this command over multiple lines (which is legal syntax for Hugo), to help make it easier to read the key-value pairs.

For example, we assign the value libsass to the key transpiler. This is actually the default value used by Hugo if you do not use any options at all… but I prefer to be explicit here. You would use dartsass instead if you had installed the Dart transpiler.

The expanded output style matches the sample CSS I show above. You can always look at the official sass documentation to explore this and other options.

The target path of css/demo.css defaults to the location where Hugo places all the built website files when you run the hugo server command. So, in my case this means the final CSS file is placed in public/css/.

tip
That may not have been what you were expecting. I expected the CSS file to be created in static/css/. But it is actually created in public/css/.

Now we use our options in the next line of code:

1
{{ with . | toCSS $opts }}

This takes the current context (.) - which as noted already is the SCSS resource (the file) we got using .get.

We pipe (|) this file through the toCSS function provided by Hugo. And we provide that toCSS function with our dictionary of parameters. This is what actually converts the scss file to css.

Note the difference between the | operator (which is a Go “pipe” connecting the output of one command into the input of the next command) and our topic of Hugo pipes - which are a different thing altogether. The above command uses both!

As before, the with in the above command means we take the resulting CSS output and make that the Hugo context for the next nested Hugo commands inside this with structure.

1
2
3
4
5
{{ if hugo.IsDevelopment }}
  do something
{{ else }}
  do a different thing
{{ end }}

Unless we change Hugo’s command line defaults, the above test will be true when we run our site using hugo server for development and testing; but it will be false when we build the production-ready site using the hugo command.

For the case where .IsDevelopment is true, we build this piece of HTML:

HTML
1
<link rel="stylesheet" href="{{ .RelPermalink }}">

And that is simply rendered as:

HTML
1
<link rel="stylesheet" href="/css/demo.css">

And there we have our stylesheet link in our <head>...</head> tag.


Creating the CSS File

The above command does one extra crucial thing for us, automatically. It triggers creation of the CSS file which gets placed where we wanted (as per the configuration options we provided) - and with the name we specified:

"targetPath" "css/demo.css"

This is covered (somewhat tersely) in the Hugo docs, here:

Hugo publishes assets to the publishDir (typically public) when you invoke .Permalink, .RelPermalink, or .Publish.

So our Hugo sass pipeline is triggered by our need to build that href="{{ .RelPermalink }}". The end result is:

(1) a new CSS file in public/css/demo.css
(2) the stylesheet link in our page’s <head>.


For the case where we are building the production version of our website, using the hugo command, we do this instead:

1
2
3
{{ with . | minify | fingerprint }}
  <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{ end }}

It’s basically the same overall process as for the development steps, but with an extra minify action and then a fingerprint action.

We use the results of these chained commands to generate a different CSS file - one which has been minified, and which has a fingerprint hash added to the file name - something like this (shortened for display purposes here):

demo.min.40b5dfdc306b472b6...9ce2bd15cdd080e.css

And the stylesheet link reflects the <link> attributes we specified - similar to this (again, simplified here for display purposes):

HTML
1
2
3
4
<link rel="stylesheet"
      href="/css/demo.min.40b5dfdc306b472b6...9ce2bd15cdd080e.css"   
      integrity="sha256-QLXf3DBrR...0VzdCA4="
      crossorigin="anonymous">

There’s a lot going on there - with some very important steps happening automatically.

There are many different (and many more sophisticated) ways to use Hugo pipes. This is just one very basic example, to explain the fundamentals.


Cacheing

And a final note: You may have noticed that there is also a Hugo resources directory in your project. This is used by Hugo pipes when processing resources (the ones you placed in that other assets directory…).

You can take a look at what’s in there, if you are curious. Hugo explains the directories like this:

assets:

The assets directory contains global resources typically passed through an asset pipeline. This includes resources such as images, CSS, Sass, JavaScript, and TypeScript.

resources:

The resources directory contains cached output from Hugo’s asset pipelines, generated when you run the hugo or hugo server commands. By default this cache directory includes CSS and images. Hugo recreates this directory and its content as needed.

This raises another very important point about caching:

The pipe chain is only invoked the first time it is encountered in a site build, and results are otherwise loaded from cache.

You can delete this cached output during development if you need to start afresh and clear out old, unwanted assets.