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 we’ll be working with. As shown in the image below, we’ll be storing our site assets in the _assets/
directory.
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 I’ve 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.89// Folder naming conventions.10paths.postFolder = '_posts';11paths.fontFolder = 'fonts';12paths.imageFolder = 'img';13paths.scriptFolder = 'js';14paths.stylesFolder = 'styles';1516// 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;2122// 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;2829// 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;3435// 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';4142// Asset files globs43paths.sassFilesGlob = paths.sassFiles + paths.sassPattern;44paths.jsFilesGlob = paths.jsFiles + paths.jsPattern;45paths.imageFilesGlob = paths.imageFiles + paths.imagePattern;4647// Jekyll files globs48paths.jekyllPostFilesGlob = paths.jekyllPostFiles + paths.markdownPattern;49paths.jekyllHtmlFilesGlob = paths.jekyllDir + paths.htmlPattern;50paths.jekyllImageFilesGlob = paths.jekyllImageFiles + paths.imagePattern;5152// Site files globs53paths.siteHtmlFilesGlob = paths.siteDir + paths.htmlPattern;5455module.exports = paths;
Dependencies
To get started with our Gulpfile, we first need to define and install the packages we’ll be using.
1// Gulpfile.js23const 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 support1920// Include paths from the created paths.js file21const paths = require('./_assets/gulp-config/paths');
Now, you could manually install all of these packages independantly, but in-case you’re looking for a shortcut, I’ve 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 site’s stylesheets, we’re 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, then2// output the file to the appropriate location3gulp.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 head2gulp.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});1112// Build all styles13gulp.task('build:styles', ['build:styles:main', 'build:styles:critical']);
Scripts
Within the js/
directory, you’ll 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 the2// result to the appropriate location3gulp.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});1415// Uglify local JS files and output the result to the16// appropriate location17gulp.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});2425// Build all scripts26gulp.task('build:scripts', ['build:scripts:global', 'build:scripts:local']);
Images
If you’ve read my previous installment on this topic, this task should look pretty familiar. Though, it’s 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 site’s images.
Additionally, this task currently optimizes all of your site’s 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 files2gulp.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 location2gulp.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 it’s 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});89// Special tasks for building and reloading BrowserSync10gulp.task('build:jekyll:watch', ['build:jekyll:local'], function(callback) {11 browserSync.reload();12 callback();13});1415gulp.task('build:scripts:watch', ['build:scripts'], function(callback) {16 browserSync.reload();17 callback();18});1920// Serve site and watch files21gulp.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 starting28 });29 gulp.watch(['_config.yml'], ['build:jekyll:watch']);30 // Watch .scss files and pipe changes to browserSync31 gulp.watch('_assets/styles/**/*.scss', ['build:styles']);32 // Watch .js files33 gulp.watch('_assets/js/**/*.js', ['build:scripts:watch']);34 // Watch image files and pipe changes to browserSync35 gulp.watch('_assets/img/**/*', ['build:images']);36 // Watch posts37 gulp.watch('_posts/**/*.+(md|markdown|MD)', ['build:jekyll:watch']);38 // Watch drafts if --drafts flag was passed39 if (module.exports.drafts) {40 gulp.watch('_drafts/*.+(md|markdown|MD)', ['build:jekyll:watch']);41 }42 // Watch html and markdown files43 gulp.watch(['**/*.+(html|md|markdown|MD)', '!_site/**/*.*'], ['build:jekyll:watch']);44 // Watch RSS feed45 gulp.watch('feed.xml', ['build:jekyll:watch']);46 // Watch data files47 gulp.watch('_data/**.*+(yml|yaml|csv|json)', ['build:jekyll:watch']);48});4950// Build site51gulp.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, we’re gonna need a few tasks to clean up after ourselves. These tasks can basically act as an “undo” button for the gulp tasks we’ve created.
1// Delete CSS2gulp.task('clean:styles', function(callback) {3 del([paths.jekyllCssFiles + 'main.css',4 paths.siteCssFiles + 'main.css',5 '_includes/critical.css'6 ]);7 callback();8});910// Delete processed JS11gulp.task('clean:scripts', function(callback) {12 del([paths.jekyllJsFiles + 'main.js', paths.siteJsFiles + 'main.js']);13 callback();14});1516// Delete processed images17gulp.task('clean:images', function(callback) {18 del([paths.jekyllImageFiles, paths.siteImageFiles]);19 callback();20});2122// Delete processed font files23gulp.task('clean:fonts', function(callback) {24 del([paths.jekyllFontFiles, paths.siteFontFiles]);25 callback();26});2728// Delete the entire _site directory29gulp.task('clean:jekyll', function(callback) {30 del(['_site']);31 callback();32});3334// Deletes _site directory and processed assets35gulp.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!