Using Gulp and UnCSS in Combination with Sass based Hugo themes

  You block advertising 😢
Would you like to buy me a ☕️ instead?

In one of my previous articles, I wrote about setting up a basic blog with Hugo. My blog – you’re currently reading – runs on Hugo, and so far I’m very happy with its simplicity and speed. Today I’ll show you how we can use a Gulp build process to establish a convenient way to work on Hugo themes. Furthermore, we will utilize UnCSS to enable building a blog that loads almost instantly and scores highly in the Google PageSpeed Insights test.

Basic Gulp and npm setup

Depending on what kind of process you’re following, there are essentially two ways of how you can setup the build process for your theme. If you want to create a reusable theme, not only for your own personal usage but the world, and you’re planning to publish the theme on GitHub or some (Hugo) theme platform, you want to setup your theme as a separate project with a separate package.json file and a separate build process. On the other hand, if you want to create a build process for a theme only for your own personal usage and if you want to optimize the files created by Hugo in your build process, you might be better off creating a build process for the whole blog, directly in the root directory of your blog.

In this blog article we’re going to examine the second approach. Because thats the way I setup my blog and this approach makes it easier to implement a build step which utilizes UnCSS to optimize the HTML files generated by Hugo.

The directory structure

After I’ve created a new site with Hugo, and after initializing a new npm project with npm init, I also created a theme named fancy-theme with hugo new theme inside the themes directory. I won’t go into much detail how to setup a Hugo theme in general, you can read more about this topic in the official documentation. After the initial setup, you end up with a basic directory structure like the following.

.
├── archetypes
├── config.toml
├── content
├── data
├── layouts
├── package.json
├── static
└── themes
    └── fancy-theme
        ├── layouts
        ├── src
        │   └── scss
        │       └── index.scss
        ├── static
        └── theme.toml

The following steps assume a basic directory structure like you can see above. Depending on your setup you may have to change some paths to make the build process work for you.

npm dependencies

First of all we have to install all dependencies we’re going to need for our Gulp powered Hugo theme build process.

npm install --save-dev clean-css concurrently gulp gulp-autoprefixer gulp-htmlmin gulp-inline-source gulp-sass gulp-sourcemaps node-sass-magic-importer rimraf uncss

This long list of dependencies might look intimidating at first but they’re going to make our life a lot easier in the later steps.

Gulp tasks

Now that we’ve installed all the dependencies we’re going to need, we can setup the Gulp tasks we’ll trigger using npm scripts in our final setup.

The first step is to create a new gulpfile.js file inside the root directory of your Hugo project. At the beginning of the newly created gulpfile.js we have to require all the dependencies we’re going to need.

const autoprefixer = require('gulp-autoprefixer');
const CleanCSS = require('clean-css');
const gulp = require('gulp');
const htmlmin = require('gulp-htmlmin');
const inline = require('gulp-inline-source');
const uncss = require('uncss');
const nodeSassMagicImporter = require('node-sass-magic-importer');
const rimraf = require('rimraf');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');

const stylesDestDirectory = 'static/dist/css';

As you can see above I have also defined a stylesDestDirectory configuration variable which specifies were we’re going to put the CSS files generated by our Gulp build process.

Build Sass files

gulp.task('styles', ['clean:styles'], () =>
  gulp.src('themes/fancy-theme/src/scss/**/*.scss')
    .pipe(sourcemaps.init())
    .pipe(sass({
      importer: nodeSassMagicImporter(),
    }).on('error', sass.logError))
    .pipe(autoprefixer())
    .pipe(sourcemaps.write({ sourceRoot: '/scss' }))
    .pipe(gulp.dest(stylesDestDirectory))
);

The Gulp task called styles defined above takes all *.scss file it can find inside the scss directory of our fancy-theme and transforms them to CSS code. We’re using the gulp-sass package in combination with the node-sass-magic-importer to transform Sass files to regular CSS (using the node-sass-magic-importer is completely optional, you can remove it if you’re not planning to use some of its awesome features).

The gulp-sourcemaps package helps us by creating source maps for easier debugging in the browser. By using the gulp-autoprefixer plugin, we don’t have to worry about vendor prefixes anymore.

By specifying ['clean:styles'] as the second parameter of gulp.task we ensure the Gulp task named clean:styles is triggered before running the styles task itself. Here you can see the code for the clean:styles task.

gulp.task('clean:styles', () => rimraf.sync(stylesDestDirectory));

What it does is, that it deletes the directory we specified in the stylesDestDirectory configuration variable. The reason why we’re doing this is to delete files also from the destination directory, which were deleted from the source directory.

Watch mode

Hugo comes with a very convenient watch mode. Hugo will detect changes to certain files and automatically reloads the browser for you. To have the same convenience for bundling your Sass files, we have to create a Gulp watch task.

gulp.task('watch', () => {
  gulp.watch('themes/fancy-theme/src/scss/**/*.scss', ['styles']);
});

The code above creates a new Gulp task called watch which watches for changes to *.scss files inside our fancy-theme scss directory. Whenever a change to a file is detected, the Gulp task with the name styles will be triggered.

Minify markup and CSS and run UnCSS

If you’ve ever worked with a SEO guy you might know that they are crazy about reaching 100 / 100 in the Google PageSpeed Insights test. And with good reason – having a fast loading site is key to attract new customers or readers. Although following the recommendations of this test blindly is not always the best idea, the PageSpeed Insights test provides a good starting point for optimizing the performance of your website.

In this step we’re going to solve the following problems most people will encounter when running the Google PageSpeed test.

  • Eliminate render-blocking JavaScript and CSS in above-the-fold content
  • Minify CSS
  • Minify HTML
gulp.task('minify:markup', () =>
  gulp.src('public/**/*.html')
    .pipe(htmlmin({ collapseWhitespace: true }))
    .pipe(inline({
      rootpath: 'public/',
      handlers: (source, context, next) => {
        if (source.type === 'css' && source.fileContent && !source.content) {
          uncss(context.html, { htmlroot: 'public' }, (error, css) => {
            if (error) throw error;
            source.content = '<style>${new CleanCSS({ level: 2 }).minify(css).styles}</style>';
            next();
          });
        } else {
          next();
        }
      },
    }))
    .pipe(gulp.dest('public'))
);

Let’s walk through what the above Gulp task named minify:markup is doing.

The minify:markup Gulp task will work on all *.html files it can find in the public directory which is the default output directory used by Hugo. First of all we’re using gulp-htmlmin to minify the contents of the HTML files.

To eliminate render blocking CSS, we can utilize the gulp-inline-source plugin. We’re specifying two options for this plugin: rootpath and handlers. The rootpath option is used to resolve the paths of CSS or JavaScript files (in our case only CSS files) which it finds in the HTML contents.

The handlers option takes a function as a value. The given function can be used to make certain optimizations on the contents of the CSS or JavaScript file which is currently inlined. The way this is implemented in the code above, we take all CSS files, run UnCSS on the current HTML content and replace the CSS code with the CSS code which is returned by UnCSS. UnCSS will remove all CSS rules which are not used in the current HTML file.

After reducing the CSS code to only the code which is actually needed to render the current HTML file, we’re further optimizing the code by passing it to CleanCSS.

Although running UnCSS on every HTML file which is generated by Hugo is pretty time consuming it may be very well worth it if you’re a performance junky like me.

The big picture

The following code shows all of the code snippets we saw above combined in one gulpfile.js file. I’ve also added a build task which essentially is a shortcut for running both the styles and minify:markup tasks. And I also added a default task which is a convenience feature to specify the task which Gulp will execute when running gulp without any arguments.

const autoprefixer = require('gulp-autoprefixer');
const CleanCSS = require('clean-css');
const gulp = require('gulp');
const htmlmin = require('gulp-htmlmin');
const inline = require('gulp-inline-source');
const uncss = require('uncss');
const nodeSassMagicImporter = require('node-sass-magic-importer');
const rimraf = require('rimraf');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');

const stylesDestDirectory = 'static/dist/css';

gulp.task('watch', () => {
  gulp.watch('themes/fancy-theme/src/scss/**/*.scss', ['styles']);
});

gulp.task('styles', ['clean:styles'], () =>
  gulp.src('themes/fancy-theme/src/scss/**/*.scss')
    .pipe(sourcemaps.init())
    .pipe(sass({
      importer: nodeSassMagicImporter(),
    }).on('error', sass.logError))
    .pipe(autoprefixer())
    .pipe(sourcemaps.write({ sourceRoot: '/scss' }))
    .pipe(gulp.dest(stylesDestDirectory))
);

gulp.task('minify:markup', () =>
  gulp.src('public/**/*.html')
    .pipe(htmlmin({ collapseWhitespace: true }))
    .pipe(inline({
      rootpath: 'public/',
      handlers: (source, context, next) => {
        if (source.type === 'css' && source.fileContent && !source.content) {
          uncss(context.html, { htmlroot: 'public' }, (error, css) => {
            if (error) throw error;
            source.content = '<style>${new CleanCSS({ level: 2 }).minify(css).styles}</style>';
            next();
          });
        } else {
          next();
        }
      },
    }))
    .pipe(gulp.dest('public'))
);

gulp.task('clean:styles', () => rimraf.sync(stylesDestDirectory));

gulp.task('build', ['styles', 'minify:markup']);

gulp.task('default', ['watch', 'build']);

npm scripts

For maximum convenience and the benefit of not having to install Gulp globally, we’re going to rely on npm scripts to start the Gulp tasks we’ve created previously.

"scripts": {
  "start": "concurrently 'gulp' 'hugo server --theme=fancy-theme --buildDrafts' 'open http://localhost:1313'",
  "build": "hugo --theme=fancy-theme && gulp build",
}

The start script uses concurrently to run both gulp and hugo in parallel. We’re using open to open the browser and pointing it to our Hugo development server. This will run the Gulp default script in watch mode and the Hugo development server is watching for changes and will reload the browser automatically if you’re making changes to blog articles or Sass files. Notice that we’re building drafts of articles when running the start script – do not forget to remove the draft status of articles you wanna publish before running the build script.

To create a new release of your blog ready for publishing, we can use the build npm script. The build script runs the Hugo default command and also runs the Gulp build task after Hugo has finished.

Recap

Although all the cool kids seem to use webpack nowadays, there is still a place for build tools like Gulp. By using Gulp and some plugins, we’re able to create a build process which accomplishes to fulfill the wishes of the Google PageSpeed Insights test. We’re eliminating render-blocking CSS and also minifying the CSS and HTML in the same process.

One drawback of using UnCSS to remove unnecessary CSS from your Hugo blog is, that build times can increase dramatically. Depending on your setup this might be a minor inconvenience or a deal breaker.


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi