Hugo CSS concat, minify and purgecss using postcss

Tutorial and example on now to configure Hugo to automatically performance tune CSS using PurgeCSS

Posted on 30 December 2019

Using bundled libraries and frameworks means including a ton of CSS being added in the site. This CSS often adds bloat and unnecessary bandwidth drain to the page request. Sure the CSS and JS may be minified - but still, you still have a lot of classes that are doing nothing.

Hugo already has an excellent support for minification, fingerprinting and concatenation. This blog post will demonstrate how I have set it up for automatically purging the unnecessary CSS.

Configure NPM with PostCSS packages

If you dont already have a package.json file, create one as normal and add the following dependencies.

  "dependencies": {
      "@fullhuman/postcss-purgecss": "^1.3.0",
      "autoprefixer": "^9.6.1",
      "cssnano": "^4.1.10",
      "postcss-cli": "^6.1.3",
      "purgecss": "^1.4.0"
  }

Configure PurgeCSS as a processor for PostCSS

Add PurgeCSS config

PostCSS will scan your build directories for any potential CSS classes that are being referenced in order to build a purge list.

Create a new file named postcss.config.js at the root of your Hugo project and edit as follows:

module.exports = {
  plugins: {
      '@fullhuman/postcss-purgecss': {
          content: [
              './themes/{insert_theme_name_here}/layouts/**/*.html', 
              './themes/{insert_theme_name_here}/assets/js/*.js',
              './themes/{insert_theme_name_here}/static/js/*.js',
              './layouts/**/*.html',
              './static/js/*.js'
            ],
          whitelist: [
              'highlight',
              'language-bash',
              'pre',
              'video',
              'code',
              'content',
              'h3',
              'h4',
              'ul',
              'li'
          ]
      },
      autoprefixer: {},
      cssnano: {preset: 'default'}
  }
};

Remember to include any additional folder paths. If you have any classes being added dynamically (via JS or as a loop in Hugo), you need to whitelist them here.

Configure Hugo to Concat, minify and fingerprint automatically.

Here is what my partial for CSS processing looks like:

{{ $css_options := dict "targetPath" "css/main.css" }}

{{- if (in (slice (getenv "HUGO_ENV") hugo.Environment) "production") -}}
  {{- $css_options = merge $css_options (dict "outputStyle" "compressed") -}}
{{- end -}}

{{ $sass_template := resources.Get "scss/main.scss" }}
{{ $style := $sass_template | resources.ExecuteAsTemplate "main_parsed.scss" . | toCSS $css_options }}

{{- if (eq (getenv "HUGO_ENV") "production") -}}
  {{- $style = $style | postCSS | minify | fingerprint "md5" -}}
{{- end -}}

<link rel="stylesheet" href="{{ $style.RelPermalink }}">

Lets go line by line and explain what each bit is doing:

1. Defines the target path where the file will get saved

{{ $css_options := dict "targetPath" "css/main.css" }}

2. Production options for compression

If we are in production mode, which is defined as an ENV var, hugo will add additional option to compress the parsed file.

{{- if (in (slice (getenv "HUGO_ENV") hugo.Environment) "production") -}}
  {{- $css_options = merge $css_options (dict "outputStyle" "compressed") -}}
{{- end -}}

3. Hugo parsing SCSS to CSS

Support for SCSS is already built in, so no need to npm install anything.

{{ $sass_template := resources.Get "scss/main.scss" }}
{{ $style := $sass_template | resources.ExecuteAsTemplate "main_parsed.scss" . | toCSS $css_options }}

4. Production options

To make life a little easier when developing, I do not want to postCSS, minify or fingerprint. We only need to do this in production.

{{- if (eq (getenv "HUGO_ENV") "production") -}}
  {{- $style = $style | postCSS | minify | fingerprint "md5" -}}
{{- end -}}

Its as simple as adding another pipeline postCSS to enable purging.

5. Output

If all is setup correctly, in production you should see a minified, purged and fingerprinted file in the path you have specified in step 1. In my case it will be in public/css/main.min.somenumbers.css. Hugo will only parse the additional options if you are setting the correct ENV flag, if you have no such distinction and want to always process the file the same way, simply remove the if / end lines.

6. Test

Test your site to make sure all the CSS tweaks and purging is working as expected.