Ronalds Vilciņš

Compile SCSS With Hugo Pipes and Fingerprint the CSS

Hugo Pipes can compile SCSS, keep source maps in development, and publish compressed CSS with a content hash in production. The important part is keeping the whole pipeline in one template so the filename and integrity value describe the same generated file.

Start with an asset rather than a file in static:

SCSS
// assets/scss/main.scss
$accent: #0666c2;

body {
  color: #222222;
  font-family: system-ui, sans-serif;
}

a {
  color: $accent;
}

This example uses Dart Sass. Install a maintained Dart Sass executable through the official installation instructions and ensure that Hugo can find it in the build environment as well as locally.

Compile SCSS with Hugo Pipes in one partial

Create layouts/_partials/styles.html:

Go HTML template
{{- with resources.Get "scss/main.scss" -}}
  {{- $outputStyle := cond hugo.IsDevelopment "expanded" "compressed" -}}
  {{- $options := dict
    "transpiler" "dartsass"
    "targetPath" "css/style.css"
    "enableSourceMap" hugo.IsDevelopment
    "outputStyle" $outputStyle
  -}}
  {{- $style := . | css.Sass $options -}}
  {{- if not hugo.IsDevelopment -}}
    {{- $style = $style | fingerprint "sha256" -}}
  {{- end -}}
  <link rel="stylesheet" href="{{ $style.RelPermalink }}"{{ if not hugo.IsDevelopment }} integrity="{{ $style.Data.Integrity }}" crossorigin="anonymous"{{ end }}>
{{- else -}}
  {{- errorf "Missing stylesheet: assets/scss/main.scss" -}}
{{- end -}}

Include it inside the layout’s <head>:

Go HTML template
{{ partial "styles.html" . }}

Hugo’s css.Sass documentation describes the compiler and options. toCSS remains an alias, but naming the operation explicitly makes the example easier to trace.

The missing-file branch fails the build with a useful message. Silently skipping the stylesheet would create a successful deployment with a broken page.

Choose the build environment deliberately

The hugo.IsDevelopment function checks the environment name. It is a clearer switch here than treating every server invocation as the only possible development build.

Build each variant explicitly when checking the pipeline:

Shell
hugo --environment development
hugo --environment production

The development variant keeps expanded output and source maps. The production variant compresses the CSS and fingerprints the result.

A continuous deployment build must have the same Sass prerequisite as the local project. A template working on one laptop does not establish that the hosting build image has the compiler installed.

Let the generated resource supply both URL and integrity

The fingerprint function hashes the compiled resource and changes its published name. The same resource supplies .Data.Integrity for the <link>.

Do not copy a sample hash into the template. It would describe the sample bytes, not the CSS generated by your project.

After a production build, inspect the page source and its referenced file. A change that alters the compiled CSS should change the fingerprinted URL; a source-only edit that compiles to identical output need not.

Fingerprinting also does not configure the server’s cache lifetime. The hosting policy still decides how long a client may reuse that URL, and old assets may need to remain available for pages cached from an earlier deployment.

Split SCSS without adding another build system

Dart Sass supports modules. A small component module can live beside the entry point:

SCSS
// assets/scss/_buttons.scss
.button-link {
  display: inline-block;
  padding: 0.5rem 0.75rem;
  border: 1px solid currentColor;
}

Load it from assets/scss/main.scss, before the other rules:

SCSS
@use "buttons";

The Sass @use documentation explains module loading. Keep one entry point responsible for the published stylesheet while modules organize its source.

The older example used Sass @import and Hugo’s embedded LibSass. Hugo now marks that compiler as deprecated, so it is a poor foundation for a fresh example. Check the current compiler documentation before migrating an existing project with older Sass dependencies.

Use the same resource approach for plain JavaScript

A script that needs no bundling can be minified and fingerprinted too:

Go HTML template
{{- with resources.Get "js/main.js" -}}
  {{- $script := . | minify | fingerprint "sha256" -}}
  <script src="{{ $script.RelPermalink }}" integrity="{{ $script.Data.Integrity }}" crossorigin="anonymous" defer></script>
{{- end -}}

That is an independent example; it does not make a framework build or package dependency graph disappear. Choose a separate toolchain when its transforms or dependency handling are actually required.

For a small site, I would keep the SCSS pipeline this explicit. One asset, one compiler choice, and one generated resource make a failed build much easier to investigate.

Filed under on

Last updated on

← Back to all articles