Optimizing Jekyll with Gulp

Optimizing Jekyll with Gulp

01June2018

While the previous entry on optimizing your Jekyll site provided some basics to optimizing your Jekyll site performance, in this post I want to focus on providing a more comprehensive explanation of how you can create a more enjoyable development experience with the magic of Gulp.
To get started, lets first discuss the basic workflow and file structure that well be working with. As shown in the image below, we’ll be storing our site assets in the _assets/ directory.

file tree

This will allow these files to be processed from within our Gulpfile, while remaining ignored by Jekyll. Though, a more in-depth explaination of the file-structure will be provided during each of the respective tasks.

Defining Paths

One of the first things that probably stands out is the gulp-config/paths.js file. In a practice Ive borrowed from savaslabs, the purpose of this file is to define the paths that will be used within our Gulpfile, allowing for cleaner code and increased modularity. Given a similar file-structure, the file should be as follows:

1var paths = {};
2// Directory locations.
3paths.assetsDir = '_assets/'; // The files Gulp will handle.
4paths.jekyllDir = ''; // The files Jekyll will handle.
5paths.jekyllAssetsDir = 'assets/'; // The asset files Jekyll will handle.
6paths.siteDir = '_site/'; // The resulting static site.
7paths.siteAssetsDir = '_site/assets/'; // The resulting static site's assets.
8
9// Folder naming conventions.
10paths.postFolder = '_posts';
11paths.fontFolder = 'fonts';
12paths.imageFolder = 'img';
13paths.scriptFolder = 'js';
14paths.stylesFolder = 'styles';
15
16// Asset files locations.
17paths.sassFiles = paths.assetsDir + paths.stylesFolder;
18paths.jsFiles = paths.assetsDir + paths.scriptFolder;
19paths.imageFiles = paths.assetsDir + paths.imageFolder;
20paths.fontFiles = paths.assetsDir + paths.fontFolder;
21
22// Jekyll files locations.
23paths.jekyllPostFiles = paths.jekyllDir + paths.postFolder;
24paths.jekyllCssFiles = paths.jekyllAssetsDir + paths.stylesFolder;
25paths.jekyllJsFiles = paths.jekyllAssetsDir + paths.scriptFolder;
26paths.jekyllImageFiles = paths.jekyllAssetsDir + paths.imageFolder;
27paths.jekyllFontFiles = paths.jekyllAssetsDir + paths.fontFolder;
28
29// Site files locations.
30paths.siteCssFiles = paths.siteAssetsDir + paths.stylesFolder;
31paths.siteJsFiles = paths.siteAssetsDir + paths.scriptFolder;
32paths.siteImageFiles = paths.siteAssetsDir + paths.imageFolder;
33paths.siteFontFiles = paths.siteAssetsDir + paths.fontFolder;
34
35// Glob patterns by file type.
36paths.sassPattern = '/**/*.scss';
37paths.jsPattern = '/**/*.js';
38paths.imagePattern = '/**/*.+(jpg|jpeg|png|svg|gif|webp|tif)';
39paths.markdownPattern = '/**/*.+(md|MD|markdown|MARKDOWN)';
40paths.htmlPattern = '/**/*.html';
41
42// Asset files globs
43paths.sassFilesGlob = paths.sassFiles + paths.sassPattern;
44paths.jsFilesGlob = paths.jsFiles + paths.jsPattern;
45paths.imageFilesGlob = paths.imageFiles + paths.imagePattern;
46
47// Jekyll files globs
48paths.jekyllPostFilesGlob = paths.jekyllPostFiles + paths.markdownPattern;
49paths.jekyllHtmlFilesGlob = paths.jekyllDir + paths.htmlPattern;
50paths.jekyllImageFilesGlob = paths.jekyllImageFiles + paths.imagePattern;
51
52// Site files globs
53paths.siteHtmlFilesGlob = paths.siteDir + paths.htmlPattern;
54
55module.exports = paths;

Dependencies

To get started with our Gulpfile, we first need to define and install the packages we’ll be using.

1// Gulpfile.js
2
3const autoprefixer = require('autoprefixer');
4const browserSync = require('browser-sync').create();
5const concat = require('gulp-concat');
6const cssnano = require('cssnano');
7const del = require('del');
8const gulp = require('gulp');
9const gutil = require('gulp-util');
10const newer = require('gulp-newer');
11const imagemin = require('gulp-imagemin');
12const pngquant = require('imagemin-pngquant');
13const postcss = require('gulp-postcss');
14const rename = require('gulp-rename');
15const run = require('gulp-run');
16const runSequence = require('run-sequence');
17const sass = require('gulp-ruby-sass');
18const uglify = require('gulp-uglify-es').default; // For es6 support
19
20// Include paths from the created paths.js file
21const paths = require('./_assets/gulp-config/paths');

Now, you could manually install all of these packages independantly, but in-case youre looking for a shortcut, Ive compiled the following command to allow you to conveniently install them all at once.

1npm i -D autoprefixer browser-sync del gulp gulp-cssnano gulp-concat gulp-util gulp-newer gulp-imagemin imagemin-pngquant gulp-notify gulp-postcss gulp-ruby-sass gulp-run gulp-rename gulp-uglify-es run-sequence

Styles

To optimize our sites stylesheets, were first going to compile our main Sass file and utilize autoprefixer and cssnano to add the necessary vendor-prefixes and optimize the resulting CSS. Additionally, the use of PostCSS allows us to perform both of these processes while only parsing the CSS once. All of which, produces a single stylesheet that gets placed in our assets and _site/assets directories.

If you’re really looking to squeeze out all the performance possible, you can check out the CSS minification benchmark and see which one works best for you.

1// Process styles, add vendor-prefixes, minify, then
2// output the file to the appropriate location
3gulp.task('build:styles:main', () => {
4 return sass(paths.sassFiles + '/main.scss', {
5 style: 'compressed',
6 trace: true,
7 loadPath: [paths.sassFiles]
8 }).pipe(postcss([autoprefixer({ browsers: ['last 2 versions']}), cssnano()]))
9 .pipe(gulp.dest(paths.jekyllCssFiles))
10 .pipe(gulp.dest(paths.siteCssFiles))
11 .pipe(browserSync.stream())
12 .on('error', gutil.log);
13});

To further optimize our stylesheet delivery we are going to want to ensure that we prioritize the most important and immediately utilized styles. This can be accomplished by creating a critical.scss file that includes only the critical and above-the-fold styling of your site to be loaded before the rest of your stylesheets, resulting in improved page-load speeds.
To accomplish this, we will perform the same optimizations as the previous stylesheets, though we will pipe the output to a CSS file to the _includes/ directory, rather than the assets/ directory, so that it can be included in the sites head.html.

1// Create and process critical CSS file to be included in head
2gulp.task('build:styles:critical', function() {
3 return sass(paths.sassFiles + '/critical.scss', {
4 style: 'compressed',
5 trace: true,
6 loadPath: [paths.sassFiles]
7 }).pipe(postcss([ autoprefixer({ browsers: ['last 2 versions'] }), cssnano()]))
8 .pipe(gulp.dest('_includes'))
9 .on('error', gutil.log);
10});
11
12// Build all styles
13gulp.task('build:styles', ['build:styles:main', 'build:styles:critical']);

Scripts

Within the js/ directory, youll notice there are two subdirectories. The purpose of these directories is to differentiate global javascript files from those that are only needed on certain pages. Consequently, the global third-party javsacript files should be placed within the libs/ subdirectory, while the local/ subdirectory is reserved for scripts that are only included on specific pages. This allows for the concatenation of the global javascript files, while preserving the distinction of the local javscript files so they may be individually included as necessary.
For this process we will need to create three tasks, two for processing the global and the local scripts, and a third task that calls them both.

1// Concatenate and uglify global JS files and output the
2// result to the appropriate location
3gulp.task('build:scripts:global', function() {
4 return gulp.src([
5 paths.jsFiles + '/lib' + paths.jsPattern,
6 paths.jsFiles + '/*.js'
7 ])
8 .pipe(concat('main.js'))
9 .pipe(uglify())
10 .pipe(gulp.dest(paths.jekyllJsFiles))
11 .pipe(gulp.dest(paths.siteJsFiles))
12 .on('error', gutil.log);
13});
14
15// Uglify local JS files and output the result to the
16// appropriate location
17gulp.task('build:scripts:local', function() {
18 return gulp.src(paths.jsFiles + '/local' + paths.jsPattern)
19 .pipe(uglify())
20 .pipe(gulp.dest(paths.jekyllJsFiles))
21 .pipe(gulp.dest(paths.siteJsFiles))
22 .on('error', gutil.log);
23});
24
25// Build all scripts
26gulp.task('build:scripts', ['build:scripts:global', 'build:scripts:local']);

Images

If youve read my previous installment on this topic, this task should look pretty familiar. Though, its important to note that this task is not intended to lessen the importance of choosing the correct image size and filetype, which are integral to optimizing your sites images.

Additionally, this task currently optimizes all of your sites images, rather than only processing new images, in most cases, making this the most time-consuming build processes. As a result, if your site has a lot of images it may be beneficial to remove this task from the default build task, and instead run this task manually when new images are added, rather than with each build.

1// Optimize and copy image files
2gulp.task('build:images', function() {
3 return gulp.src(paths.imageFilesGlob)
4 .pipe(imagemin({
5 optimizationLevel: 3,
6 progressive: true,
7 interlaced: true,
8 use: [pngquant()]
9 }))
10 .pipe(gulp.dest(paths.jekyllImageFiles))
11 .pipe(gulp.dest(paths.siteImageFiles))
12 .pipe(browserSync.stream());
13});

Fonts

The task for processing the fonts is fairly simple, we simply want to collect all of the files from the fonts/ subdirectories and place them all within the resulting assets/fonts/ directory.

1// Place fonts in proper location
2gulp.task('build:fonts', function() {
3 return gulp.src(paths.fontFiles + '/**/**.*')
4 .pipe(rename(function(path) {path.dirname = '';}))
5 .pipe(gulp.dest(paths.jekyllFontFiles))
6 .pipe(gulp.dest(paths.siteFontFiles))
7 .pipe(browserSync.stream())
8 .on('error', gutil.log);
9});

Jekyll

Now its time to setup the basic Jekyll build process and browser-sync for the convenient auto-reload functionality.

1// Run jekyll build command.
2gulp.task('build:jekyll', function() {
3 var shellCommand = 'bundle exec jekyll build --config _config.yml';
4 return gulp.src('')
5 .pipe(run(shellCommand))
6 .on('error', gutil.log);
7});
8
9// Special tasks for building and reloading BrowserSync
10gulp.task('build:jekyll:watch', ['build:jekyll:local'], function(callback) {
11 browserSync.reload();
12 callback();
13});
14
15gulp.task('build:scripts:watch', ['build:scripts'], function(callback) {
16 browserSync.reload();
17 callback();
18});
19
20// Serve site and watch files
21gulp.task('serve', ['build'], function() {
22 browserSync.init({
23 server: paths.siteDir,
24 ghostMode: false, // Toggle to mirror clicks, reloads etc (performance)
25 logFileChanges: true,
26 logLevel: 'debug',
27 open: true // Toggle to auto-open page when starting
28 });
29 gulp.watch(['_config.yml'], ['build:jekyll:watch']);
30 // Watch .scss files and pipe changes to browserSync
31 gulp.watch('_assets/styles/**/*.scss', ['build:styles']);
32 // Watch .js files
33 gulp.watch('_assets/js/**/*.js', ['build:scripts:watch']);
34 // Watch image files and pipe changes to browserSync
35 gulp.watch('_assets/img/**/*', ['build:images']);
36 // Watch posts
37 gulp.watch('_posts/**/*.+(md|markdown|MD)', ['build:jekyll:watch']);
38 // Watch drafts if --drafts flag was passed
39 if (module.exports.drafts) {
40 gulp.watch('_drafts/*.+(md|markdown|MD)', ['build:jekyll:watch']);
41 }
42 // Watch html and markdown files
43 gulp.watch(['**/*.+(html|md|markdown|MD)', '!_site/**/*.*'], ['build:jekyll:watch']);
44 // Watch RSS feed
45 gulp.watch('feed.xml', ['build:jekyll:watch']);
46 // Watch data files
47 gulp.watch('_data/**.*+(yml|yaml|csv|json)', ['build:jekyll:watch']);
48});
49
50// Build site
51gulp.task('build', function(callback) {
52 runSequence(['build:scripts', 'build:styles', 'build:images', 'build:fonts', 'build:downloads'], 'build:jekyll', callback);
53});

Cleaning up

In accordance with the Boy Scout Rule, were gonna need a few tasks to clean up after ourselves. These tasks can basically act as an undo button for the gulp tasks weve created.

1// Delete CSS
2gulp.task('clean:styles', function(callback) {
3 del([paths.jekyllCssFiles + 'main.css',
4 paths.siteCssFiles + 'main.css',
5 '_includes/critical.css'
6 ]);
7 callback();
8});
9
10// Delete processed JS
11gulp.task('clean:scripts', function(callback) {
12 del([paths.jekyllJsFiles + 'main.js', paths.siteJsFiles + 'main.js']);
13 callback();
14});
15
16// Delete processed images
17gulp.task('clean:images', function(callback) {
18 del([paths.jekyllImageFiles, paths.siteImageFiles]);
19 callback();
20});
21
22// Delete processed font files
23gulp.task('clean:fonts', function(callback) {
24 del([paths.jekyllFontFiles, paths.siteFontFiles]);
25 callback();
26});
27
28// Delete the entire _site directory
29gulp.task('clean:jekyll', function(callback) {
30 del(['_site']);
31 callback();
32});
33
34// Deletes _site directory and processed assets
35gulp.task('clean', ['clean:jekyll', 'clean:styles', 'clean:scripts', 'clean:images', 'clean:fonts', 'clean:downloads']
36);

Here’s a gist with the resulting Gulpfile.js

Conclusion

While this Gulpfile provides a lot of optimization and added convenience to just about any site, there is definitely still room for improvement. Feel free to contact me or leave a comment with any improvements or suggestions!


Brett Stevenson