Using Dart Sass with Hugo: taking it easy

Thanks to the speed and power of Hugo, I lose little or nothing in opting for the Node.js version of Sass.

2023-02-10

It’s been nearly a year since I first wrote about enabling the Hugo static site generator (SSG) to work comfortably with Dart Sass. A couple of months after that, I adopted what was arguably a technically superior, but somewhat more complicated, method.

Now, for several reasons, I’ve decided to fall back to the simpler approach I originally espoused — especially given the freedom I gain from how well Hugo’s built-in asset pipeline, Hugo Pipes, performs with either method.

At the bottom, I’ll link to each of my other articles in this series so you can get a fuller picture of the problems I was trying to solve in the first place, but here’s the short version:

  • Hugo supports two versions of Sass: LibSass and Dart Sass. However, LibSass was deprecated in 2020, so you should use Dart Sass if at all possible.
  • Although Hugo works with LibSass without additional help, things are more difficult where Dart Sass is concerned. Specifically, Hugo must be able to “see” an Embedded Dart Sass binary file in the PATH. Thus, you must (A.) obtain that extra file and (B.) make sure it’s in the PATH on whatever machine you’re using to run Hugo. Item (B) is the trickier of the two where remote hosts are concerned, as you can quickly imagine. I subsequently described two ways to accomplish this: scripting; and, depending on one’s preference for a CI/CD host, either GitHub Actions or GitLab CI/CD.
  • However, what I’d initially mentioned is a third, considerably simpler way to get Hugo to work with Dart Sass: using the same Node.js sass package that’s typically used by JavaScript-based SSGs such as Astro and Eleventy. Besides the fact that Hugo-with-sass doesn’t work quite as quickly during development as does Hugo-with-Embedded-Dart-Sass, the perceived drawback here is that sass is a Node package. This can offend anti-Node purists who delight in having a Hugo project free of that often-bloated node_modules folder that comes with using any such packages. (Trust me, there are such folks.)

I used the scripting method for two months, then the GitHub Actions method for nine months. I could still be using the latter to this day. But, the other day, I just decided I’d give the Node package another shot. Perhaps, this time, the whole development experience with sass wouldn’t be as slow as I seemed to recall from when I’d either tried it or briefly fallen back to it.

Lo and behold, it wasn’t.

Now, yes, it’s still a little slower at dev start-up than with Embedded Dart Sass, but truthfully not that much (two or three additional seconds, maybe) — and the automatic updating of my site whenever I make a change to any of my SCSS files seems just as snappy. For that, I tip my metaphorical hat to Hugo for what seems to be the umpteenth time.

Of course, in production, there’s zero difference in the final styling files that go on the site, so my visitors suffer not a bit for the change. Better still, I no longer have to bother with getting that Embedded Dart Sass binary in the right place on the build system, whether for the CI/CD host or the actual site host. That enables deploying a Hugo-with-sass site through the native UIs of most Jamstack-savvy hosts, especially the three I usually recommend1: Cloudflare Pages, Netlify, and Vercel.

For now, at least, I am continuing to deploy my Hugo site with a GitHub Action because, due to how hosts usually clone your Git repository for builds, that’s the only way to ensure the correct Git info appears at the top of each post.2

Not being one of those previously mentioned anti-Node purists, I was fine with letting not only sass but a few other helpful Node packages into the mix. The only thing that did bother me was that, unlike the last time I’d used sass with Hugo to output only one overarching index.css file, I now was using a multi-file, “sorta scoped” setup. Would it be a nightmare to maintain the Sass-related scripts in the resulting package.json file?

As it turned out, not at all.

I’d previously only skimmed the documentation for the Sass CLI, so I was pleasantly surprised to see that it has a “many-to-many mode” which can easily convert an entire directory of SCSS files to CSS in one fell swoop:

sass --no-source-map assets/scss:assets/css

. . . and that resulting placement (/assets/css/ in the Hugo project) meant Hugo Pipes would have no problem handling all those CSS files in its usual lightning-fast manner. For example, here’s how my head-criticalcss.html partial now looks:

{{- $css := "" -}}
{{- $css = resources.Get "css/critical.css" -}}
{{- with $css }}
	<style>{{ .Content | safeCSS }}</style>
{{- end }}

. . . while the relevant part of the head-css.html partial looks like this:

{{- range $cssTypes -}}
	{{- $condition = index . 0 -}}
	{{- $fileName = index . 1 -}}
	{{- if eq $condition true -}}
		{{- $css = resources.Get (print "css/" $fileName ".css") -}}
		{{- if hugo.IsProduction -}}
			{{- $css = $css | fingerprint "md5" -}}
		{{- end -}}
		<link rel="preload" href="{{ $css.RelPermalink }}" as="style">
		<link rel="stylesheet" href="{{ $css.RelPermalink }}" type="text/css">
	{{- end -}}
{{- end }}

(For more on these partials, see “Sorta scoped styling in Hugo, take two.”)

Finally, here are the scripts in the project’s package.json file3:

"clean": "rimraf public",
"pagefind": "pagefind --source public",
"devsass": "sass --no-source-map assets/scss:assets/css",
"prodsass": "sass --no-source-map assets/scss:assets/css --style=compressed",
"dev:hugo": "hugo server --port 3000 --bind=0.0.0.0 --baseURL=http://192.168.254.10:3000 --panicOnWarning --disableFastRender --forceSyncStatic --gc",
"dev:sass": "npm run devsass -- --watch",
"prod:hugo": "hugo --minify",
"prod:sass": "npm run prodsass",
"testbuild:hugo": "hugo server --port 3000 --bind=0.0.0.0 --baseURL=http://192.168.254.10:3000 --panicOnWarning --disableFastRender --forceSyncStatic --gc --environment=production",
"testbuild:sass": "npm run prodsass -- --watch",
"start": "NODE_ENV=development npm-run-all clean devsass --parallel dev:*",
"build": "NODE_ENV=production npm-run-all clean prodsass prod:hugo",
"testbuild": "NODE_ENV=production npm-run-all clean prodsass --parallel testbuild:*",
"testbuildpf": "NODE_ENV=production npm-run-all build pagefind --parallel testbuild:*"

(And, yes, I do have to run devsass within dev:sass. Perhaps it seems repetitive, but that ensures there are files waiting in /assets/css/ for Hugo Pipes to process when Hugo starts running. Otherwise, Hugo errors out when it hits the first of those resources.Get commands in the Sass-specific partials.)

In the end, is using sass with Hugo quite as nerdily interesting as has been obtaining and using that Embedded Dart Sass binary? Not quite.

But is it a lot less hassle? Absolutely.


Our saga so far

Here are all my previous posts on this subject.


  1. Notably, each allows you to specify the Hugo version through use of a HUGO_VERSION environment variable. That keeps you from being stuck with an older, less capable version on the host’s standard build image for site deployments. ↩︎

  2. Well, that is, except for a somewhat hacky approach that would do the job but kinda get back into the sort of thing I’m trying to avoid by “taking it easy.” Otherwise, one way I could get around this would be to provide only a link to the post’s GitHub history, which doesn’t require Hugo’s .GitInfo function. ↩︎

  3. Note that I have installed not only sass but also the cross-platform rimraf file-deletion tool, npm-run-all for running multiple scripts in one command, and pagefind for search. The latter is especially nice because that’s another thing with which I don’t have to futz in getting it to run on the remote build process: I just use npm run pagefind and all is good. Even if I decide to quit using CI/CD down the line, I could simply spec the host’s build command as npm run build && npm run pagefind and call it a day.) ↩︎

Reply via email
View comments