CSS Modules plugin in Hugo with postCSS

I’m wondering if it could be possible to add this “CSS modules” PostCSS plugin to a postCSS hugo setup.

This would allow locally scoped css classes, and i could let go of BEM. Write less markup, less css, ship more.

As far as i understand it, CSS modules are working nicely in JavaScript frameworks and often out of the box. But i find myself overwhelmed trying to implement it in Hugo.

Now this post already mentions a similar will, but it’s from 2018, and now with more recent things in Hugo like writeStats and of course the fact that we can add postCSS plugins in Hugo, it may be possible to implement this.

I’m basically asking the “senior” Hugo people here if this looks achievable to you. And if so a little help would be very much appreciated.

I’m the kind that can frown upon adding npm, node_modules and other packages.json in a beautiful nodeless hugo project, but i can’t ignore all that goodness, the best of both worlds they say.

Thanks for reading.

1 Like

Anyone has an opinion on this ? Maybe the post went unseen when i posted it

Not sure to understand your question.

Here is what I use for postCSS

partials/style.html

<!-- Variables for purgecss -->
{{- $isProd := hugo.IsProduction -}}
{{ $options := dict "inlineImports" true }}

<!-- plugins + stylesheet -->
{{ $styles := slice }}
{{ range site.Params.plugins.css }}
   {{ if findRE "^http" .link }}
      <link crossorigin="anonymous" media="all" rel="stylesheet" href="{{ .link | relURL }}" {{.attributes | safeHTMLAttr}} >
   {{ else }}
      {{ $styles = $styles | append (resources.Get .link) }}
   {{ end }}
{{ end }}

{{ $styles := $styles | append (resources.Get "scss/style.scss" | resources.ExecuteAsTemplate "style.scss" . | toCSS) }}
{{ $styles = $styles | resources.Concat "/css/style.css" }}

{{- partial "partials/css-process" (dict "context" . "css" $styles "options" $options "isprod" true ) }}


<!--
   For processing CSS as assets
   Parameters : (dict "context" . "css" $css "isprod" true "options" "")
-->
{{- define "partials/css-process" }}
{{- $css     := .css }}
{{- $isProd  := .isprod  | default hugo.IsProduction }}
{{- $options := .options | default ""}}
{{- $media   := .media   | default "screen"}}
{{- $nopurge := .nopurge | default false }}
{{- if $isProd }}
   {{- if and site.Params.purgecss (not $nopurge) }}
{{ `<!-- purgeCSS ON -->` | safeHTML }}
   {{- $css = $css | resources.PostCSS $options}}
   {{- $css = $css | minify | fingerprint "sha384" | resources.PostProcess }}
{{- else }}
{{ `<!-- purgeCSS Off -->` | safeHTML }}
   {{- $css = $css | minify | fingerprint "sha384" | resources.PostProcess }}
{{- end }}
{{- else }}
   {{- $css = $css | fingerprint "sha384" }}
{{- end }}
<link rel="preload" href="{{ $css.RelPermalink }}" as="style" media="{{ $media }}" {{ if $isProd }}integrity="{{ $css.Data.Integrity }}"{{ end }}>
<link href="{{ $css.RelPermalink }}" {{ if $isProd }}integrity="{{ $css.Data.Integrity }}"{{ end }} rel="stylesheet" type="text/css" media="{{ $media }}">
{{- end }}

postcss.config.js

You have to adapt your own safelist and [[params.plugins.css]]

const purgecss = require("@fullhuman/postcss-purgecss")({
  content: ["./hugo_stats.json"],
  defaultExtractor: (content) => {
    const els = JSON.parse(content).htmlElements;
    return [...(els.tags || []), ...(els.classes || []), ...(els.ids || [])];
  },
  safelist: [
    // pour bootstrap (Tested OK)
    /^btn-/,
    // pour swiper + hero (Tested OK)
    /^swiper-/,
    /^slider-/,
    /^aos-/,
    /^spinner-/,
    // pour scroll (Tested OK)
    /^header-/,
    // pour instafeed (Tested ??)
    /^data-/,

    // FOR DOCUMENTATION - USED SOMETIMES BY OTHER COMPONENTS
    // pour MsgBox bootstrap (Tested OK)
    /collapsing/,
    /show/,
    /[aria-expanded=true]/,
    /[aria-expanded=false]/,
    // pour lightbox (Tested OK)
    /^lb-/,
    /^lightbox/,
    /^g/,
    /^desc/,
    /^zoom/,
    /dragging/,
    /fullscreen/,
    /loaded/,
    /visible/,
    /current/,
    /dragging-nav/,
    /inactive/,
    /prev/,
  ],
});

module.exports = {
  plugins: [
    ...(process.env.HUGO_ENVIRONMENT === "production" ? [purgecss] : []),
  ],
};

config.toml

[build]
writeStats = true

params.toml

# PurgeCSS
purgecss = true

# CSS Plugins
[[params.plugins.css]]
link = "plugins/swiper/swiper-bundle.css"
[[params.plugins.css]]
link = "plugins/glightbox/glightbox.css"
[[params.plugins.css]]
link = "plugins/aos/aos-custom.css"
# for brands
[[params.plugins.css]]
link = "plugins/font-awesome/v6/brands.css"
# load all icons except brands
[[params.plugins.css]]
link = "plugins/font-awesome/v6/icons.css"
# regular icons font family
[[params.plugins.css]]
link = "plugins/font-awesome/v6/regular.css"
# solid icons font family
[[params.plugins.css]]
link = "plugins/font-awesome/v6/solid.css"

modules.toml

[hugoVersion]
extended = true
min = "0.110.0"

[[imports]]
path = "github.com/gohugoio/hugo-mod-bootstrap-scss/v5"
[[imports]]
path = "github.com/gethugothemes/hugo-modules/icons/font-awesome"

etc ...
1 Like

Thanks for the reply, i’ll have to analyse and understand all this code, I’ll respond when I’ve digested it all