Sorta scoped styling in Hugo, take two

I had the right idea but not the right approach. Here’s a better one.


General note: This site’s appearance, configuration, hosting, and other basic considerations will change over time. As a result, certain content on this page could be at variance with what you’re currently seeing on the site, but the two were consistent when this post originally appeared.

I gave up on my earlier, Rube Goldberg-esque attempt to achieve scoped styling after finding it too convoluted to maintain. Fortunately, I’ve now stumbled on a much simpler way to get there.

Updates from the future:

Rather than leave in place my original post supplemented by its subsequent and confusing oh-never-mind-do-it-this-way revision, I’ve reworked this to keep the good parts but throw out the erroneous parts. My apologies to those who were confused in the interim.

Also, unless you’re using at least Hugo 0.114.0 — which included a fix for an issue that I discovered later — you might want to avoid this method.


As I explained in last year’s “Sorta scoped styling in Hugo,” I wanted to achieve some form of scoped styling within this Hugo-powered website. That goal was laudable, but the same couldn’t be said about the sadly anal approach I chose for achieving it:

  • First, I tagged each of my hundreds of posts according to which content types they contained — e.g., code blocks, video embeds, and so on.
  • Next, I broke down my one big Sass styling file, index.scss, into many separate files specific to the content types. And, when I say specific, I mean it: I had one file for a post that had both images and social media embeds, another for a post with all those and tables, and on it went. Going even further down the rabbit hole, I decided that each file would then @use one or more Sass partials to do its work. By the time I’d finished, I had over two dozen Sass files in play. Jeeeeeeez.
  • Finally, within the site repo’s head.html Hugo partial, I added conditionals that would present the tag(s)-appropriate Sass file(s) for each post.

While all of this worked, I soon abandoned it, as I described in a subsequent update:

Consider this now an abandoned experiment. I went with it for a couple of weeks, but, in the end, decided to revert to my previous definitely-not-scoped configuration after seeing that this method hampered attempts to make certain styling changes — that is, without invoking chaos which wasn’t worth my time to resolve. Perhaps you’ll have better luck with it.

Blind squirrel, meet acorn

Then, while experimenting with a Tailwind CSS theme for another post, I stumbled onto a method that also worked yet didn’t require the tags, or the ridiculously large spider’s web of styling files, or the associated hassle. While I initially did it just to limit the size of the Tailwind file, I soon realized it also was a much more idiot-proof way to get the scoped styling I’d previously sought.

It broke down like this.

  • Decide which styling is sufficiently site-wide as to be considered critical CSS, and let a head-criticalcss.html partial put it in the head as internal CSS, thus loading as quickly as possible.1
  • Create small, modular styling files — e.g., Sass partials, although this method can be used in vanilla CSS, too — with rules that are specific to various types of content.
  • In a head-css.html partial, use conditionals to identify the content in question within a given page, automatically determining which styling a page does (and doesn’t) need. Each conditional, if satisfied, then calls the appropriate modular styling file and runs it through Hugo Pipes to produce the final CSS.

Thus, I added this to my head.html partial:

{{- partialCached "head-criticalcss.html" . -}}
{{- partial "head-css.html" . -}}

The first of those, head-criticalcss.html, looks like this:


{{- $css := "" -}}
{{- $optionsCSSCritical := (dict "outputStyle" "compressed" "transpiler" "dartsass") -}}
{{- $css = resources.Get "scss/critical.scss" | resources.ToCSS $optionsCSSCritical -}}
{{- with $css }}
	<style>{{ .Content | safeCSS }}</style>
{{- end }}

And, as for head-css.html, it puts all those earlier conditionals in one file, selecting the correct styling files for Hugo to use on a page:


{{- $css := "" -}}
{{- $cssOptions := dict "outputStyle" "compressed" "transpiler" "dartsass" -}}
{{- $condition := "" -}}
{{- $fileName := "" -}}
{{- $conditionSocial := false -}}
{{- $conditionCode := false -}}
{{- $conditionArtCode := false -}}
{{- $conditionTables := false -}}
{{- $conditionLiteYT := false -}}
{{- $conditionBillboard := false -}}
{{- $conditionArticle := false -}}
{{- $conditionPostsSingle := false -}}
{{- $conditionPostsList := false -}}
{{- $conditionFootnotes := false -}}
{{- $conditionHome := false -}}
{{- $conditionSitemap := false -}}
{{- $conditionSearchBtn := false -}}
{{- $conditionSearchForm := false -}}
{{- $conditionDetails := false -}}
{{- $condition404 := false -}}
{{- if (findRE `<blockquote class="toot-blockquote"` .Content 1) -}}{{- $conditionSocial = true -}}{{- end -}}
{{- if (findRE `<div class="highlight"` .Content 1) -}}{{- $conditionCode = true -}}{{- end -}}
{{- if and (findRE `(<code)` .Content 1) (not (findRE `<div class="highlight"` .Content 1)) -}}{{- $conditionArtCode = true -}}{{- end -}}
{{- if (findRE `<table` .Content 1) -}}{{- $conditionTables = true -}}{{- end -}}
{{- if (findRE `<lite-youtube` .Content 1) -}}{{- $conditionLiteYT = true -}}{{- end -}}
{{- if (and (ne .Title "Home page") (ne .Title "Sitemap (HTML form)") (ne .Title "Posts")) -}}{{- $conditionBillboard = true -}}{{- end -}}
{{- if (and (and (ne .Title "Search the site") (ne .Title "Posts")) (or (eq .Section "posts") (eq .Title "About me") (eq .Title "Privacy policy") (eq .Title "Want to reach me?"))) -}}{{- $conditionArticle = true -}}{{- end -}}
{{- if (and (eq .Section "posts") (ne .Title "Posts")) -}}{{- $conditionPostsSingle = true -}}{{- end -}}
{{- if (eq .Title "Posts") -}}{{- $conditionPostsList = true -}}{{- end -}}
{{- if (findRE `class="footnote-ref"` .Content 1) -}}{{- $conditionFootnotes = true -}}{{- end -}}
{{- if (eq .Title "Home page") -}}{{- $conditionHome = true -}}{{- end -}}
{{- if (eq .Title "Sitemap (HTML form)") -}}{{- $conditionSitemap = true -}}{{- end -}}
{{- if (ne .Title site.Params.SearchTitle) -}}{{- $conditionSearchBtn = true -}}{{- end -}}
{{- if (eq .Title site.Params.SearchTitle) -}}{{- $conditionSearchForm = true -}}{{- end -}}
{{- if (findRE `<details>` .Content 1) -}}{{- $conditionDetails = true -}}{{- end -}}
{{- if (eq .Title "404 Page not found") -}}{{- $condition404 = true -}}{{- end -}}

{{- $cssTypes := slice -}}{{/* init big slice */}}
{{- $cssTypes = append slice (slice $conditionSocial "social") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionCode "code") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionArtCode "artcode") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionTables "tables") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionLiteYT "lite-yt-embed") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionBillboard "billboard") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionArticle "article") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionPostsSingle "posts-single") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionPostsList "posts-list") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionFootnotes "footnotes") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionHome "home") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionSitemap "sitemaphtml") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionSearchBtn "search-btn") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionSearchForm "search-form") $cssTypes -}}
{{- $cssTypes = append slice (slice $conditionDetails "details") $cssTypes -}}
{{- $cssTypes = append slice (slice $condition404 "fourohfour") $cssTypes -}}

{{- range $cssTypes -}}
	{{- $condition = index . 0 -}}
	{{- $fileName = index . 1 -}}
	{{- if eq $condition true -}}
		{{- $cssOptions := merge $cssOptions (dict "targetPath" (print "css/" $fileName ".css" )) -}}
		{{- $css = resources.Get (print "scss/" $fileName ".scss") | resources.ToCSS $cssOptions -}}
		{{- 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 -}}

  1. Although Dart Sass’s @use rule makes it easy to access multiple Sass partials from within a critical.scss file, Hugo allows you to accomplish roughly the same thing with vanilla CSS. For example, you could put your modular CSS files in assets/css/partials/, name them so that alphanumerical sorting will tell Hugo Pipes in which order to process them (such as 001_reset.css, 010_vars.css, 020_global.css, etc.), and use the following to concatenate them:
    {{ $css := (resources.Match "css/partials/0*.css") | resources.Concat "critical.css" }}
    (Thanks as always to Joe Mooring of the Hugo team for helping me with this.) ↩︎

Reply via email
View comments