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.