Andrew Welch · Insights · #frontend #gulp #workflow

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

A Gulp Workflow for Frontend Development Automation

Gulp is a work­flow automa­tion tool that helps you build cool stuff faster; here’s how to use it for fron­tend development.

Gulp Logo

As web­sites become more com­pli­cat­ed to build, a fron­tend work­flow automa­tion tool of some sort becomes a neces­si­ty. Gulp is one such tool:

gulp is a toolkit for automating painful or time-consuming tasks in your development workflow, so you can stop messing around and build something.

The above quote from the GulpJS​.com web­site says it well. But before we get into using Gulp specif­i­cal­ly, let’s talk about why we even need a thing that auto­mates our fron­tend workflow.

Back in the day, we cre­at­ed some CSS, we edit­ed some HTML, and maybe includ­ed a JavaScript or two on the web­site, and called it a day. Web­sites have grown up sig­nif­i­cant­ly since their days as online brochures”, and now we are cre­at­ing some­what com­pli­cat­ed soft­ware appli­ca­tions that hap­pen to be deployed via the web.

To help man­age this com­plex­i­ty, we can use a fron­tend work­flow automa­tion tool that works like an assem­bly line, putting all of the pieces togeth­er for us.

Automated Assembly Line

Com­put­ers are real­ly, real­ly good at being giv­en a list of things to do, and exe­cut­ing them in a deter­min­is­tic man­ner every sin­gle time. This hap­pens to be some­thing that humans are not that great at; we are bet­ter at high­er lev­el archi­tec­ture and cre­ative thinking.

So let’s have computer do what they’re good at, and allow us to focus on making cool stuff

While each web­site we work on is dif­fer­ent in terms of style and con­tent, at an abstract lev­el they all con­tain the same things” that end up get­ting built and com­bined to make the final website.

From a con­cep­tu­al lev­el, this type of dig­i­tal assem­bly line” used to build stuff has been around since the Make tool was cre­at­ed in 1976 by Stu­art Feld­man. When­ev­er a project reach­es a cer­tain lev­el of com­plex­i­ty, the time spent build­ing an automa­tion work­flow ends up sav­ing you time.

Mod­ern web­sites reached that lev­el of com­plex­i­ty some time ago. As stat­ed in the Fron­tend Dev Best Prac­tices for 2017 arti­cle, you real­ly need a fron­tend work­flow automa­tion tool if you’re going to keep your sanity.”

Link What does our Gulpfile do?

So what exact­ly does our Gulp fron­tend work­flow automa­tion do for us? Here’s a rundown:

  • Han­dles our CSS 
    • Com­piles all of our SCSS down to CSS, using a cache to make it faster
    • Auto-pre­fix­es our CSS for the browsers we support
    • Cre­ates SourceMaps for our CSS to make debug­ging easy
    • Pulls in CSS from any third-par­ty modules/​packages we use
    • Com­bines and mini­fies all of our CSS
  • Han­dles our JavaScript
    • Tran­spiles all of the JavaScript we write from ES6 to some­thing web browsers support
    • Pulls in JavaScripts from any third-par­ty modules/​packages we use
    • Ugli­fies our JavaScripts
    • Pulls in any JavaScripts we need to inline in our HTML separately
  • Han­dles live reloading
    • CSS/SCSS changes cause an imme­di­ate brows­er repaint with­out a page load
    • JavaScript changes cause the brows­er to reload the page
    • Changes to our Twig/​HTML tem­plates cause the brows­er to reload the page
  • Gen­er­ates Crit­i­calC­SS for our website
  • Runs an acces­si­bil­i­ty audit on our website
  • Gen­er­ates a cus­tom icon font using only the glyphs we use via Fontel­lo
  • Gen­er­ates all of the var­i­ous fav­i­cons for our web­site (and the HTML code for them) from a sin­gle source image
  • Loss­less­ly min­i­mizes all of the the images our web­site uses via imagemin

…and a bit more too! But that’s a quick overview of what a work­flow automa­tion can do for you. And it’s portable from project to project, because of sep­a­ra­tion of con­cerns that puts the data into our package.json.

Link So why Gulp?

There’s no short­age of tools that you can use to auto­mate your fron­tend work­flow, most of which rely on the Node.js & NPM ecosys­tem. The key is using the right tool for the job.

The Right Tool For The Job

For JavaScript-cen­tric projects that use React or Vue for JAM­stack-style web­sites, you’ll inevitably want to use web­pack, because of all of the scaf­fold­ing that exists around it, and advanced fea­tures like code split­ting, hot mod­ule reload­ing, etc. But for web­pack to be used effec­tive­ly, it real­ly needs to be embraced whole-hog and used not just as a mod­ule bundler, but as a mod­ule loader as well.

There’s also an old­er fron­tend work­flow automa­tion tool called Grunt, and it works fine, but it can be some­what more ver­bose to con­fig­ure. It’s also in gen­er­al slow­er than Gulp for build­ing things, due to its file-ori­ent­ed approach.

There’s even Lar­avel Mix, which adds a lay­er on top of web­pack. I’ve found that it’s fan­tas­tic for boot­strap­ping projects, but when­ev­er the project grows to any scale, its been nec­es­sary to have more con­trol over the build process.

Viget has built a tool called Blendid! that uses a hybrid approach of uti­liz­ing both Gulp and web­pack, and it looks pret­ty well done. But sim­i­lar to Lar­avel Mix, I pre­fer a bit more con­trol over the build process when necessary.

At some point, the lay­ers upon lay­ers ends up get­ting a bit sil­ly as well.

In the end, all of these tools (Gulp includ­ed) sim­ply exe­cute Node.js JavaScript pack­ages on the com­mand line. Gulp just adds an API and stream­ing lay­er on top that makes doing typ­i­cal fron­tend builds easier.

You could also just use npm scripts to exe­cute the var­i­ous Node.js mod­ules direct­ly, but I find the con­ve­nience lay­er that Gulp pro­vides to be worth the trade­off of anoth­er lev­el of dependency.

There is even a GUI tool called CodeK­it that offers some nice func­tion­al­i­ty, but I think ulti­mate­ly it will end up being the DreamWeaver of the fron­tend automa­tion tool world. Some things sim­ply can’t be expressed as effi­cient­ly via a GUI, and there’s lit­tle chance it will be able to keep pace with the Node.js ecosystem.

There is no one true “best” build system tool, so don’t be dogmatic about it, and pick whatever the best tool for the job is.

For most fron­tend devel­op­ment projects, I find that Gulp strikes a nice mix between flex­i­bil­i­ty and automa­tion. I can re-use my gulpfile.js and package.json for just about every project I do, and for the times when cus­tomiza­tion is need­ed, I can eas­i­ly do so.

Tan­gent: Why are all of these fron­tend automa­tion tools using Node.js? They cer­tain­ly don’t have to be, they could be writ­ten in PHP, Perl, Ruby, Go, shell scripts (if you’re a masochist), or any lan­guage real­ly. The rea­son is sim­ply that fron­tend devel­op­ers were already famil­iar with JavaScript, and it was nat­ur­al to write the fron­tend automa­tion tools they need­ed in JavaScript run­ning via Node.

Link General Philosophy of Building Websites

Before we get into the nit­ty grit­ty of the actu­al gulpfile.js, it’s impor­tant to under­stand the gen­er­al phi­los­o­phy that I’m using to build web­sites. The gulpfile.js is some­thing that helps me to build these web­sites, so under­stand­ing my over­all approach is instructive.

General Philosophy

In gen­er­al, the web­sites that I work on fol­low the PRPL pat­tern, in that we want to load only what is need­ed to to ren­der the ini­tial above the fold” web con­tent, prefetch oth­er like­ly need­ed resources, and then lazy load every­thing else asynchronously.

You can read more in detail about what this means in the Imple­ment­ing Crit­i­cal CSS on your web­site & Ser­vice­Work­ers and Offline Brows­ing arti­cles, and more broad­ly the top­ic of web­site per­for­mance in the A Pret­ty Web­site Isn’t Enough & Cre­at­ing Opti­mized Images in Craft CMS articles.

On a prac­ti­cal lev­el, this means that we com­bine all of our site-wide CSS into a sin­gle site.combined.min.css file that gets loaded asyn­chro­nous­ly, and we pro­vide the ini­tial page styling via Crit­i­cal CSS.

The site-wide CSS con­sists of SCSS I author that gets built down to CSS, com­bined with CSS from any oth­er third-par­ty pack­ages that I might use, and that all gets auto-pre­fixed and minimized.

We also inline a core set of JavaScript into each page that we use to load oth­er things (CSS, JavaScript, etc.) asynchronously.

The JavaScript that I write is done using ES6 syn­tax, and then tran­spiled down to some­thing that all web browsers can under­stand via Babel.

All third-par­ty pack­ages I use (whether CSS or JavaScript) are declared as dependencies in my package.json, and npm or yarn is used to install and/​or update them via semver. Once again, you’ll find detail on all of this in the A Bet­ter package.json for the Fron­tend article.

Final­ly, the JavaScript we use gets ugli­fied indi­vid­u­al­ly, and loaded asyn­chro­nous­ly only on the pages where they are need­ed via depen­den­cy man­age­ment. See the Load­JS as a Light­weight JavaScript Loader arti­cle for details on that.

Try­ing to do all of this by hand” would be pret­ty much impos­si­ble; and yet this is how mod­ern, per­for­mant web­pages are built.

Link Project Tree

It’s also use­ful to see what the project looks like in terms of what the var­i­ous direc­to­ries are used for, and the over­all organization.

Bonsai Project Tree

Here’s what the root of my project direc­to­ry looks like:

vagrant@homestead:~/sites/nystudio107$ tree -a -L 2 -I "node_modules|.git|scripts|.DS_Store|.idea" .
.
├── .babelrc
├── browserslist
├── build
│   ├── fonts
│   ├── html
│   └── js
├── craft
├── .csslintrc
├── .env.php
├── example.env.php
├── .git
├── .gitignore
├── .gitmodules
├── gulpfile.js
├── node_modules
├── package.json
├── public
│   ├── css
│   ├── favicon.ico
│   ├── favicon.png
│   ├── fonts
│   ├── htaccess
│   ├── imager
│   ├── img
│   ├── index.php
│   ├── js
│   ├── webappmanifest.json
│   └── web.config
├── readme.txt
├── scripts
├── src
│   ├── conf
│   ├── css
│   ├── fontello
│   ├── fonts
│   ├── img
│   ├── js
│   └── json
├── templates -> craft/templates/
└── yarn.lock

27 directories, 20 files

A cou­ple direc­to­ries deserve men­tion (these paths are all defined in the package.json):

  • src/ — this is the direc­to­ry where all of the things you author go. You own it, it’s the source for things that get built. The direc­to­ry struc­ture here mir­rors that of public/
  • build/ — an inter­me­dia direc­to­ry cre­at­ed by the build sys­tem for tem­po­rary file builds. The direc­to­ry struc­ture here mir­rors that of public/
  • public/js/ — where the built pub­lic dis­tri­b­u­tion JavaScript gets put by the build system
  • public/css/ — where the build pub­lic dis­tri­b­u­tion CSS gets put by the build system
  • node_modules/ — NPM pack­ages down­loaded via npm/yarn that are list­ed in the package.json that con­tain third par­ty CSS/JS used for the fron­tend as well as NPM pack­ages used for the build sys­tem itself

Link Taking a Gulp

So let’s have a look at how Gulp can be used to make our lives eas­i­er. This arti­cle is real­ly a sis­ter to the A Bet­ter package.json for the Fron­tend arti­cle, so if you haven’t read that yet, please do.

N.B.: This arti­cle gets pret­ty in the thick of things pret­ty quick. If you’re entire­ly new to Gulp, check out the Gulp for Begin­ners arti­cle for a primer.

The gulpfile.js pre­sent­ed here is what is used to build this very web­site that you’re read­ing now, and com­bined with the package.json file, they offer a nice base­line to start any project with.

This arti­cle assumes that you already have Node.js, NPM (or per­haps Yarn as well), and Gulp glob­al­ly installed in your devel­op­ment environment.

We’ll present var­i­ous chunks of our gulpfile.js out of order, but at the end, the full gulpfile.js will be shown for your reference.

Link Gulpfile.js Preamble

The very top of our gulpfile.js looks like this:

// package vars
const pkg = require("./package.json");

// gulp
const gulp = require("gulp");

// load all plugins in "devDependencies" into the variable $
const $ = require("gulp-load-plugins")({
    pattern: ["*"],
    scope: ["devDependencies"]
});

const onError = (err) => {
    console.log(err);
};

const banner = [
    "/**",
    " * @project        <%= pkg.name %>",
    " * @author         <%= pkg.author %>",
    " * @build          " + $.moment().format("llll") + " ET",
    " * @release        " + $.gitRevSync.long() + " [" + $.gitRevSync.branch() + "]",
    " * @copyright      Copyright (c) " + $.moment().format("YYYY") + ", <%= pkg.copyright %>",
    " *",
    " */",
    ""
].join("\n");

Much of this is dis­cussed in-depth in the A Bet­ter package.json for the Fron­tend arti­cle, but briefly:

  • First we require our package.json into the pkg con­stant, so we can access every­thing declared in the package.json con­ve­nient­ly from our gulpfile.js
  • Next we require gulp, so that we have access to its stream­ing API and can uti­lize the var­i­ous Gulp modules
  • Then we use the gulp-load-plu­g­ins mod­ule to load in all of the npm mod­ules that are list­ed as devDependencies, name­spaced under the $ vari­able. This just makes our package.json tidy, with­out requir­ing dozens of require() state­ments for all of the mod­ules that we use
  • We set the onError con­stant to be an anony­mous func­tion that sim­ply logs errors to con­sole, again for con­ve­nience sake
  • Final­ly, we set the banner con­stant to be nice ban­ner that we can add to the top of our JavaScript/​CSS that indi­cates when it was built, and so on.

If you’re com­ing from the fron­tend world, don’t get con­fused by the use of $ as a vari­able. It may look jQuery-ish, but it’s just a vari­able that could be named anything.

Link Primary Gulp tasks

So that was all of our gulpfile.js pre­am­ble, let’s jump down to the bot­tom of the file and look at the two major tasks that we exe­cute from the com­mand line:

// Default task
gulp.task("default", ["css", "js"], () => {
    $.livereload.listen();
    gulp.watch([pkg.paths.src.scss + "**/*.scss"], ["css"]);
    gulp.watch([pkg.paths.src.css + "**/*.css"], ["css"]);
    gulp.watch([pkg.paths.src.js + "**/*.js"], ["js"]);
    gulp.watch([pkg.paths.templates + "**/*.{html,htm,twig}"], () => {
        gulp.src(pkg.paths.templates)
            .pipe($.plumber({errorHandler: onError}))
            .pipe($.livereload());
    });
});

// Production build
gulp.task("build", ["download", "default", "favicons", "imagemin", "fonts", "criticalcss"]);

These two tasks are typ­i­cal­ly all that we use to set our fron­tend automa­tion into motion. They are rough­ly divid­ed into two sep­a­rate things:

  1. default — tasks that we use every day, which oper­ate quickly
  2. build — tasks that we rarely use (usu­al­ly just when doing an ini­tial build, or a final deploy to pro­duc­tion build), and can take some time to do their thing

A typ­i­cal day involves sit­ting down, typ­ing gulp and then work­ing on var­i­ous HTML, Twig, CSS, SCSS, JavaScript, etc. The default task builds our CSS & JavaScript, and then sits there watch­ing our CSS/SCSS/JS files. If we change any of them, it rebuilds our site CSS or JavaScript as appropriate.

The default task also looks for any changes to our tem­plates, and auto-reloads the web brows­er gulp-livere­load; you sim­ply need the livere­load Chrome exten­sion installed.

Link CSS Gulp Tasks

Let’s have a look at our css task, and any sub-tasks it triggers:

// scss - build the scss to the build folder, including the required paths, and writing out a sourcemap
gulp.task("scss", () => {
    $.fancyLog("-> Compiling scss");
    return gulp.src(pkg.paths.src.scss + pkg.vars.scssName)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.sourcemaps.init({loadMaps: true}))
        .pipe($.sass({
                includePaths: pkg.paths.scss
            })
            .on("error", $.sass.logError))
        .pipe($.cached("sass_compile"))
        .pipe($.autoprefixer())
        .pipe($.sourcemaps.write("./"))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.build.css));
});

// css task - combine & minimize any distribution CSS into the public css folder, and add our banner to it
gulp.task("css", ["scss"], () => {
    $.fancyLog("-> Building css");
    return gulp.src(pkg.globs.distCss)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.newer({dest: pkg.paths.dist.css + pkg.vars.siteCssName}))
        .pipe($.print())
        .pipe($.sourcemaps.init({loadMaps: true}))
        .pipe($.concat(pkg.vars.siteCssName))
        .pipe($.cssnano({
            discardComments: {
                removeAll: true
            },
            discardDuplicates: true,
            discardEmpty: true,
            minifyFontValues: true,
            minifySelectors: true
        }))
        .pipe($.header(banner, {pkg: pkg}))
        .pipe($.sourcemaps.write("./"))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.dist.css))
        .pipe($.filter("**/*.css"))
        .pipe($.livereload());
});

The first para­me­ter to a gulp.task() method is the name of the task; the sec­ond para­me­ter are any depen­dences (or deps). Depen­den­cies are tasks that need to be run before this task exe­cutes. In this way, you can chain tasks together.

So the first thing our css task does is it runs our scss task, to make sure all of our SCSS is com­piled. Our scss task ini­tial­izes our CSS sourcemaps, and then com­piles our SCSS with any include paths, and caches the result. In this way, we don’t both­er recom­pil­ing our SCSS if noth­ing has changed.

If some­thing has changed, then we run our auto­pre­fix­er that looks for a browserslist file in our project root to deter­mine what auto­pre­fix­ing it should be doing:

# Supported browsers

last 3 versions
iOS >= 8

Then it writes out the sourcemaps, logs some use­ful siz­ing infor­ma­tion, and writes out the built CSS to pkg.paths.build.css. We use an inter­me­di­ate build file here so that our css task can just include the com­piled CSS like any oth­er file for our site-wide CSS bundle.

Our css task then makes sure that each file in the pkg.globs.distCss is new­er than our built site-wide CSS, oth­er­wise it does­n’t both­er rebuild­ing it. Assum­ing we do have new­er file(s), it ini­tial­izes our sourcemaps, com­bines all of our CSS togeth­er, then mini­fies it all via cssnano, adds our banner as a head­er, writes out our sourcemaps, and builds the full site-wide CSS to our pub­lic dis­tri­b­u­tion fold­er at pkg.paths.dist.css.

The css task also auto­mat­i­cal­ly refresh­es the web­page’s CSS with­out a full brows­er reload via gulp-livere­load; you sim­ply need the livere­load Chrome exten­sion installed.

Link JS Gulp Tasks

Now let’s have a look at our js & asso­ci­at­ed tasks:

// Prism js task - combine the prismjs Javascript & config file into one bundle
gulp.task("prism-js", () => {
    $.fancyLog("-> Building prism.min.js...");
    return gulp.src(pkg.globs.prismJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.newer({dest: pkg.paths.build.js + "prism.min.js"}))
        .pipe($.concat("prism.min.js"))
        .pipe($.uglify())
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.build.js));
});

// babel js task - transpile our Javascript into the build directory
gulp.task("js-babel", () => {
    $.fancyLog("-> Transpiling Javascript via Babel...");
    return gulp.src(pkg.globs.babelJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.newer({dest: pkg.paths.build.js}))
        .pipe($.babel())
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.build.js));
});

// inline js task - minimize the inline Javascript into _inlinejs in the templates path
gulp.task("js-inline", () => {
    $.fancyLog("-> Copying inline js");
    return gulp.src(pkg.globs.inlineJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.if(["*.js", "!*.min.js"],
            $.newer({dest: pkg.paths.templates + "_inlinejs", ext: ".min.js"}),
            $.newer({dest: pkg.paths.templates + "_inlinejs"})
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.uglify()
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.rename({suffix: ".min"})
        ))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.templates + "_inlinejs"));
});

// js task - minimize any distribution Javascript into the public js folder, and add our banner to it
gulp.task("js", ["js-inline", "js-babel", "prism-js"], () => {
    $.fancyLog("-> Building js");
    return gulp.src(pkg.globs.distJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.if(["*.js", "!*.min.js"],
            $.newer({dest: pkg.paths.dist.js, ext: ".min.js"}),
            $.newer({dest: pkg.paths.dist.js})
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.uglify()
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.rename({suffix: ".min"})
        ))
        .pipe($.header(banner, {pkg: pkg}))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.dist.js))
        .pipe($.filter("**/*.js"))
        .pipe($.livereload());
});

The first thing that hap­pens when our js task is run is that the depen­dent tasks are executed:

  • js-inline — takes cer­tain JavaScripts and puts them in our Craft tem­plates direc­to­ry so that we can source or include them. These are JavaScripts that load oth­er things (so we want them inlined in our HTML) and/​or need to be parsed as Twig templates
  • js-babel — takes any JavaScript we author from the pkg.globs.babelJs and tran­spiles them via Babel into pkg.paths.build.js for lat­er processing
  • prism-js — this is one of the few func­tions in my gulpfile.js that I don’t use on all projects; it builds a cus­tom JavaScript bun­dle for Pris­mJS that includes only what we need. It’s used for dis­play­ing the fan­cy for­mat­ted code sam­ples on the website

It’s impor­tant to note that for Babel to work cor­rect­ly, you’ll need a .babelrc file in your project root to tell it what to tran­spile things down to. Here’s what mine looks like:

{
  "presets": ["es2015"],
  "compact": true
}

Final­ly, our js task runs and takes every­thing from pkg.globs.distJs, ugli­fies it, adds .min.js if nec­es­sary (it may already be ugli­fied), adds our banner head­er, and writes out the indi­vid­ual JavaScript files to our pub­lic dis­tri­b­u­tion fold­er at pkg.paths.dist.js.

The js task also looks for any changes to our JavaScripts, and auto-reloads the web brows­er gulp-livere­load; you sim­ply need the livere­load Chrome exten­sion installed.

Link Misc Gulp Tasks

In addi­tion to the basic CSS/JS build­ing, there are a num­ber of oth­er use­ful func­tions in our gulpfile.js that work gener­i­cal­ly based on the data in our package.json.

In no par­tic­u­lar order:

//favicons-generate task
gulp.task("favicons-generate", () => {
    $.fancyLog("-> Generating favicons");
    return gulp.src(pkg.paths.favicon.src).pipe($.favicons({
        appName: pkg.name,
        appDescription: pkg.description,
        developerName: pkg.author,
        developerURL: pkg.urls.live,
        background: "#FFFFFF",
        path: pkg.paths.favicon.path,
        url: pkg.site_url,
        display: "standalone",
        orientation: "portrait",
        version: pkg.version,
        logging: false,
        online: false,
        html: pkg.paths.build.html + "favicons.html",
        replace: true,
        icons: {
            android: false, // Create Android homescreen icon. `boolean`
            appleIcon: true, // Create Apple touch icons. `boolean`
            appleStartup: false, // Create Apple startup images. `boolean`
            coast: true, // Create Opera Coast icon. `boolean`
            favicons: true, // Create regular favicons. `boolean`
            firefox: true, // Create Firefox OS icons. `boolean`
            opengraph: false, // Create Facebook OpenGraph image. `boolean`
            twitter: false, // Create Twitter Summary Card image. `boolean`
            windows: true, // Create Windows 8 tile icons. `boolean`
            yandex: true // Create Yandex browser icon. `boolean`
        }
    })).pipe(gulp.dest(pkg.paths.favicon.dest));
});

//copy favicons task
gulp.task("favicons", ["favicons-generate"], () => {
    $.fancyLog("-> Copying favicon.ico");
    return gulp.src(pkg.globs.siteIcon)
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.dist.base));
});

The favicons task gen­er­ates the myr­i­ad of web­site fav­i­cons from a sin­gle source image, and also gen­er­ates the HTML nec­es­sary to display/​include them. This makes it super easy to gen­er­ate all of the var­i­ous fav­i­con for­mats that I hon­est­ly have a hard time keep­ing up with.

// imagemin task
gulp.task("imagemin", () => {
    return gulp.src(pkg.paths.dist.img + "**/*.{png,jpg,jpeg,gif,svg}")
        .pipe($.imagemin({
            progressive: true,
            interlaced: true,
            optimizationLevel: 7,
            svgoPlugins: [{removeViewBox: false}],
            verbose: true,
            use: []
        }))
        .pipe(gulp.dest(pkg.paths.dist.img));
});

The imagemin task opti­mizes all of the images in your pkg.paths.dist.img glob in situ. These are images that are part of the site itself, and are checked into your git repo. For images that the client will upload, they should be opti­mized serv­er-side as per the Cre­at­ing Opti­mized Images in Craft CMS article.

//generate-fontello task
gulp.task("generate-fontello", () => {
    return gulp.src(pkg.paths.src.fontello + "config.json")
        .pipe($.fontello())
        .pipe($.print())
        .pipe(gulp.dest(pkg.paths.build.fontello))
});

//copy fonts task
gulp.task("fonts", ["generate-fontello"], () => {
    return gulp.src(pkg.globs.fonts)
        .pipe(gulp.dest(pkg.paths.dist.fonts));
});

The fonts task first gen­er­ates a cus­tom icon font via fontel­lo via a config.json file that con­tains only the glyphs we need. Please don’t include a mas­sive 294k FontAwe­some font when you’re just using a half-dozen social media icons.

Then it just copies the fontel­lo font, and any oth­er fonts from pkg.globs.fonts into our pub­lic dis­tri­b­u­tion fold­er at pkg.paths.dist.fonts.

// Process data in an array synchronously, moving onto the n+1 item only after the nth item callback
function doSynchronousLoop(data, processData, done) {
    if (data.length > 0) {
        const loop = (data, i, processData, done) => {
            processData(data[i], i, () => {
                if (++i < data.length) {
                    loop(data, i, processData, done);
                } else {
                    done();
                }
            });
        };
        loop(data, 0, processData, done);
    } else {
        done();
    }
}

// Process the critical path CSS one at a time
function processCriticalCSS(element, i, callback) {
    const criticalSrc = pkg.urls.critical + element.url;
    const criticalDest = pkg.paths.templates + element.template + "_critical.min.css";

    let criticalWidth = 1200;
    let criticalHeight = 1200;
    if (element.template.indexOf("amp_") !== -1) {
        criticalWidth = 600;
        criticalHeight = 19200;
    }
    $.fancyLog("-> Generating critical CSS: " + $.chalk.cyan(criticalSrc) + " -> " + $.chalk.magenta(criticalDest));
    $.critical.generate({
        src: criticalSrc,
        dest: criticalDest,
        inline: false,
        ignore: [],
        css: [
            pkg.paths.dist.css + pkg.vars.siteCssName,
        ],
        minify: true,
        width: criticalWidth,
        height: criticalHeight
    }, (err, output) => {
        if (err) {
            $.fancyLog($.chalk.magenta(err));
        }
        callback();
    });
}

//critical css task
gulp.task("criticalcss", ["css"], (callback) => {
    doSynchronousLoop(pkg.globs.critical, processCriticalCSS, () => {
        // all done
        callback();
    });
});

The criticalcss task gen­er­ates our Crit­i­cal CSS that has the styles need­ed to ren­der our above the fold con­tent”. I won’t go into it here, as it’s described in detail in the Imple­ment­ing Crit­i­cal CSS on your web­site article.

// Run pa11y accessibility tests on each template
function processAccessibility(element, i, callback) {
    const accessibilitySrc = pkg.urls.critical + element.url;
    const cliReporter = require('./node_modules/pa11y/reporter/cli.js');
    const options = {
        log: cliReporter,
        ignore:
                [
                    'notice',
                    'warning'
                ],
        };
    const test = $.pa11y(options);

    $.fancyLog("-> Checking Accessibility for URL: " + $.chalk.cyan(accessibilitySrc));
    test.run(accessibilitySrc, (error, results) => {
        cliReporter.results(results, accessibilitySrc);
        callback();
    });
}

// accessibility task
gulp.task("a11y", (callback) => {
    doSynchronousLoop(pkg.globs.critical, processAccessibility, () => {
        // all done
        callback();
    });
});

The a11y task runs an acces­si­bil­i­ty audit on all of our web­site tem­plates. I won’t go into it here, as it’s described in detail in the Mak­ing Web­sites Bet­ter through Acces­si­bil­i­ty article.

// Process the downloads one at a time
function processDownload(element, i, callback) {
    const downloadSrc = element.url;
    const downloadDest = element.dest;

    $.fancyLog("-> Downloading URL: " + $.chalk.cyan(downloadSrc) + " -> " + $.chalk.magenta(downloadDest));
    $.download(downloadSrc)
        .pipe(gulp.dest(downloadDest));
    callback();
}

// download task
gulp.task("download", (callback) => {
    doSynchronousLoop(pkg.globs.download, processDownload, () => {
        // all done
        callback();
    });
});

The down­load task down­loads third par­ty JavaScript (such as Google Ana­lyt­ics) that I want to serve myself, so I have con­trol over the expires head­er. This is get­ting a bit off in the weeds, but it strikes a nice bal­ance between uti­liz­ing third par­ty JavaScripts, while still con­trol­ling how they are served and delivered.

In addi­tion to let­ting me con­trol the expires head­er, it also ensures that few­er DNS lookups need to be done in order to load every­thing on the webpage.

Link What’s Left Out?

There may be a few things left out of my fron­tend work­flow that some peo­ple might want or expect to be included:

  • CSS/SCSS lint­ing — I use Php­Storm as and edi­tor, so my CSS/SCSS lint­ing is done in-edi­tor. If you want, you can eas­i­ly add CSS/SCSS lint­ing as a step of the css task.
  • Browser­sync — I use livere­load, but it would­n’t be hard to swap in Browser­sync in the default task instead if you pre­fer it for mul­ti-device testing.

I’m sure there are oth­er niceties that peo­ple use as part of their build process; I’m just pre­sent­ing a min­i­mal set of what has worked out well for me.

Link The Full Monty

I real­ize that all of this is a lot to digest, but hope­ful­ly you’ve found it help­ful. The key take away is that your gulpfile.js has the code that does the build­ing of your web­sites, and the package.json con­tains what’s unique to that par­tic­u­lar website.

Done in this way, you’ll rarely have to be chang­ing or adding any­thing to your gulpfile.js, and as such can use it as the basis for all of the sites you build. How­ev­er, you have the flex­i­bil­i­ty to add to it or mod­i­fy it if you have spe­cif­ic behav­ior that you need.

With­out fur­ther ado, here’s the full mon­ty” of the entire gulpfile.js used to build this very website:

// package vars
const pkg = require("./package.json");

// gulp
const gulp = require("gulp");

// load all plugins in "devDependencies" into the variable $
const $ = require("gulp-load-plugins")({
    pattern: ["*"],
    scope: ["devDependencies"]
});

const onError = (err) => {
    console.log(err);
};

const banner = [
    "/**",
    " * @project        <%= pkg.name %>",
    " * @author         <%= pkg.author %>",
    " * @build          " + $.moment().format("llll") + " ET",
    " * @release        " + $.gitRevSync.long() + " [" + $.gitRevSync.branch() + "]",
    " * @copyright      Copyright (c) " + $.moment().format("YYYY") + ", <%= pkg.copyright %>",
    " *",
    " */",
    ""
].join("\n");

// scss - build the scss to the build folder, including the required paths, and writing out a sourcemap
gulp.task("scss", () => {
    $.fancyLog("-> Compiling scss");
    return gulp.src(pkg.paths.src.scss + pkg.vars.scssName)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.sourcemaps.init({loadMaps: true}))
        .pipe($.sass({
                includePaths: pkg.paths.scss
            })
            .on("error", $.sass.logError))
        .pipe($.cached("sass_compile"))
        .pipe($.autoprefixer())
        .pipe($.sourcemaps.write("./"))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.build.css));
});

// css task - combine & minimize any distribution CSS into the public css folder, and add our banner to it
gulp.task("css", ["scss"], () => {
    $.fancyLog("-> Building css");
    return gulp.src(pkg.globs.distCss)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.newer({dest: pkg.paths.dist.css + pkg.vars.siteCssName}))
        .pipe($.print())
        .pipe($.sourcemaps.init({loadMaps: true}))
        .pipe($.concat(pkg.vars.siteCssName))
        .pipe($.cssnano({
            discardComments: {
                removeAll: true
            },
            discardDuplicates: true,
            discardEmpty: true,
            minifyFontValues: true,
            minifySelectors: true
        }))
        .pipe($.header(banner, {pkg: pkg}))
        .pipe($.sourcemaps.write("./"))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.dist.css))
        .pipe($.filter("**/*.css"))
        .pipe($.livereload());
});

// Prism js task - combine the prismjs Javascript & config file into one bundle
gulp.task("prism-js", () => {
    $.fancyLog("-> Building prism.min.js...");
    return gulp.src(pkg.globs.prismJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.newer({dest: pkg.paths.build.js + "prism.min.js"}))
        .pipe($.concat("prism.min.js"))
        .pipe($.uglify())
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.build.js));
});

// babel js task - transpile our Javascript into the build directory
gulp.task("js-babel", () => {
    $.fancyLog("-> Transpiling Javascript via Babel...");
    return gulp.src(pkg.globs.babelJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.newer({dest: pkg.paths.build.js}))
        .pipe($.babel())
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.build.js));
});

// components - build .vue VueJS components
gulp.task("components", () => {
    $.fancyLog("-> Compiling Vue Components");
    return gulp.src(pkg.globs.components)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.newer({dest: pkg.paths.build.js, ext: ".js"}))
        .pipe($.vueify({}))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.build.js));
});

// inline js task - minimize the inline Javascript into _inlinejs in the templates path
gulp.task("js-inline", () => {
    $.fancyLog("-> Copying inline js");
    return gulp.src(pkg.globs.inlineJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.if(["*.js", "!*.min.js"],
            $.newer({dest: pkg.paths.templates + "_inlinejs", ext: ".min.js"}),
            $.newer({dest: pkg.paths.templates + "_inlinejs"})
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.uglify()
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.rename({suffix: ".min"})
        ))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.templates + "_inlinejs"))
        .pipe($.filter("**/*.js"))
        .pipe($.livereload());
});

// js task - minimize any distribution Javascript into the public js folder, and add our banner to it
gulp.task("js", ["js-inline", "js-babel", "prism-js"], () => {
    $.fancyLog("-> Building js");
    return gulp.src(pkg.globs.distJs)
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.if(["*.js", "!*.min.js"],
            $.newer({dest: pkg.paths.dist.js, ext: ".min.js"}),
            $.newer({dest: pkg.paths.dist.js})
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.uglify()
        ))
        .pipe($.if(["*.js", "!*.min.js"],
            $.rename({suffix: ".min"})
        ))
        .pipe($.header(banner, {pkg: pkg}))
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.dist.js))
        .pipe($.filter("**/*.js"))
        .pipe($.livereload());
});

// Process data in an array synchronously, moving onto the n+1 item only after the nth item callback
function doSynchronousLoop(data, processData, done) {
    if (data.length > 0) {
        const loop = (data, i, processData, done) => {
            processData(data[i], i, () => {
                if (++i < data.length) {
                    loop(data, i, processData, done);
                } else {
                    done();
                }
            });
        };
        loop(data, 0, processData, done);
    } else {
        done();
    }
}

// Process the critical path CSS one at a time
function processCriticalCSS(element, i, callback) {
    const criticalSrc = pkg.urls.critical + element.url;
    const criticalDest = pkg.paths.templates + element.template + "_critical.min.css";

    let criticalWidth = 1200;
    let criticalHeight = 1200;
    if (element.template.indexOf("amp_") !== -1) {
        criticalWidth = 600;
        criticalHeight = 19200;
    }
    $.fancyLog("-> Generating critical CSS: " + $.chalk.cyan(criticalSrc) + " -> " + $.chalk.magenta(criticalDest));
    $.critical.generate({
        src: criticalSrc,
        dest: criticalDest,
        inline: false,
        ignore: [],
        css: [
            pkg.paths.dist.css + pkg.vars.siteCssName,
        ],
        minify: true,
        width: criticalWidth,
        height: criticalHeight
    }, (err, output) => {
        if (err) {
            $.fancyLog($.chalk.magenta(err));
        }
        callback();
    });
}

//critical css task
gulp.task("criticalcss", ["css"], (callback) => {
    doSynchronousLoop(pkg.globs.critical, processCriticalCSS, () => {
        // all done
        callback();
    });
});

// Process the downloads one at a time
function processDownload(element, i, callback) {
    const downloadSrc = element.url;
    const downloadDest = element.dest;

    $.fancyLog("-> Downloading URL: " + $.chalk.cyan(downloadSrc) + " -> " + $.chalk.magenta(downloadDest));
    $.download(downloadSrc)
        .pipe(gulp.dest(downloadDest));
    callback();
}

// download task
gulp.task("download", (callback) => {
    doSynchronousLoop(pkg.globs.download, processDownload, () => {
        // all done
        callback();
    });
});

// Run pa11y accessibility tests on each template
function processAccessibility(element, i, callback) {
    const accessibilitySrc = pkg.urls.critical + element.url;
    const cliReporter = require('./node_modules/pa11y/reporter/cli.js');
    const options = {
        log: cliReporter,
        ignore:
                [
                    'notice',
                    'warning'
                ],
        };
    const test = $.pa11y(options);

    $.fancyLog("-> Checking Accessibility for URL: " + $.chalk.cyan(accessibilitySrc));
    test.run(accessibilitySrc, (error, results) => {
        cliReporter.results(results, accessibilitySrc);
        callback();
    });
}

// accessibility task
gulp.task("a11y", (callback) => {
    doSynchronousLoop(pkg.globs.critical, processAccessibility, () => {
        // all done
        callback();
    });
});

//favicons-generate task
gulp.task("favicons-generate", () => {
    $.fancyLog("-> Generating favicons");
    return gulp.src(pkg.paths.favicon.src).pipe($.favicons({
        appName: pkg.name,
        appDescription: pkg.description,
        developerName: pkg.author,
        developerURL: pkg.urls.live,
        background: "#FFFFFF",
        path: pkg.paths.favicon.path,
        url: pkg.site_url,
        display: "standalone",
        orientation: "portrait",
        version: pkg.version,
        logging: false,
        online: false,
        html: pkg.paths.build.html + "favicons.html",
        replace: true,
        icons: {
            android: false, // Create Android homescreen icon. `boolean`
            appleIcon: true, // Create Apple touch icons. `boolean`
            appleStartup: false, // Create Apple startup images. `boolean`
            coast: true, // Create Opera Coast icon. `boolean`
            favicons: true, // Create regular favicons. `boolean`
            firefox: true, // Create Firefox OS icons. `boolean`
            opengraph: false, // Create Facebook OpenGraph image. `boolean`
            twitter: false, // Create Twitter Summary Card image. `boolean`
            windows: true, // Create Windows 8 tile icons. `boolean`
            yandex: true // Create Yandex browser icon. `boolean`
        }
    })).pipe(gulp.dest(pkg.paths.favicon.dest));
});

//copy favicons task
gulp.task("favicons", ["favicons-generate"], () => {
    $.fancyLog("-> Copying favicon.ico");
    return gulp.src(pkg.globs.siteIcon)
        .pipe($.size({gzip: true, showFiles: true}))
        .pipe(gulp.dest(pkg.paths.dist.base));
});

// imagemin task
gulp.task("imagemin", () => {
    return gulp.src(pkg.paths.dist.img + "**/*.{png,jpg,jpeg,gif,svg}")
        .pipe($.imagemin({
            progressive: true,
            interlaced: true,
            optimizationLevel: 7,
            svgoPlugins: [{removeViewBox: false}],
            verbose: true,
            use: []
        }))
        .pipe(gulp.dest(pkg.paths.dist.img));
});

//generate-fontello task
gulp.task("generate-fontello", () => {
    return gulp.src(pkg.paths.src.fontello + "config.json")
        .pipe($.fontello())
        .pipe($.print())
        .pipe(gulp.dest(pkg.paths.build.fontello))
});

//copy fonts task
gulp.task("fonts", ["generate-fontello"], () => {
    return gulp.src(pkg.globs.fonts)
        .pipe(gulp.dest(pkg.paths.dist.fonts));
});

// Default task
gulp.task("default", ["css", "js"], () => {
    $.livereload.listen();
    gulp.watch([pkg.paths.src.scss + "**/*.scss"], ["css"]);
    gulp.watch([pkg.paths.src.css + "**/*.css"], ["css"]);
    gulp.watch([pkg.paths.src.js + "**/*.js"], ["js"]);
    gulp.watch([pkg.paths.templates + "**/*.{html,htm,twig}"], () => {
        gulp.src(pkg.paths.templates)
            .pipe($.plumber({errorHandler: onError}))
            .pipe($.livereload());
    });
});

// Production build
gulp.task("build", ["download", "default", "favicons", "imagemin", "fonts", "criticalcss"]);