Setting up a CSS Production Line Using Gulp

When To Use These Instructions

We can follow these exact instructions if we:

  1. are using Eleventy
  2. want to compress/minimize our CSS
  3. want to organize our CSS into critical and non-critical CSS
  4. want to set up regular gulp tasks and production gulp tasks

A Huge Shoutout

A gigantic thank you to Andy Bell for teaching me almost everything I know about Eleventy. This code and setup is essentially his, with modifications because I'm writing in regular ol' CSS instead of Sass. Take his Eleventy course, it's fantastic.


1 — Install Gulp

In our main project folder, use npm install gulp.

2 — Make a gulp-tasks folder and create a file for this production line

We'll call the folder gulp-tasks and the CSS production line file called css.js.

Note: every time we create a new production line (say for images), we'll make a new file in the gulp-tasks folder.

3 — Add the task code to the file

Here's the code:

const {dest, src} = require('gulp');
const cleanCSS = require('gulp-clean-css');

// Flags whether we compress the output etc
const isProduction = process.env.NODE_ENV === 'production';

// An array of outputs that should be sent over to includes
const criticalStyles = ['critical.css', 'page.css'];

// Takes the arguments passed by `dest` and determines where the output file goes
const calculateOutput = ({history}) => {
// By default, we want a CSS file in our dist directory, so the
// HTML can grab it with a <link />
let response = './dist/css';

// Get everything after the last slash
const sourceFileName = /[^/]*$/.exec(history[0])[0];

// If this is critical CSS though, we want it to go
// to the _includes directory, so nunjucks can include it
// directly in a style element
if (criticalStyles.includes(sourceFileName)) {
    response = './src/_includes/css';

return response;

// Grabs all root CSS files,
// processes them, then sends them to the output calculator
const css = () => {
    return src('./src/css/*.css')
            ? {
                level: 2
            : {}
    .pipe(dest(calculateOutput, {sourceMaps: !isProduction}));

module.exports = css;


Include all the files that are critical CSS in the const criticalStyles = ['critical.css', 'page.css']; array. This will change depending how we organize our CSS.

4 — Install the dependency we're using to minify the CSS

Use npm install gulp-clean-css.

5 — Allow Eleventy (but not Git) to access the critical.css file

Note: Eleventy ignores anything in the .gitignore file. I'm not using Git right now, because it's still something I'm learning. That said, I'll pretend I have one here, because it creates a little problem with critical CSS.

We've want Git to ignore our src/_includes/css folder, so Eleventy will also ignore it, and not be able to use it to grab out critical CSS and put it in a <style> element in our head. So we add a line to our .eleventy.js file just before the return:

// Tell 11ty to use the .eleventyignore and ignore our .gitignore file

Then we create a .eleventyignore file and add a single line: node_modules

6 — Create a Gulpfile

Create a file called gulpfile.js in our main project folder. This controls gulp and is where we create our tasks.

Add this code:

const {parallel, watch} = require('gulp');

// Pull in each task
const css = require('./gulp-tasks/css.js');

// Set each directory and contents that we want to watch and
// assign the relevant task. `ignoreInitial` set to true will
// prevent the task being run when we run `gulp watch`, but it
// will run when a file changes.
const watcher = () => {
watch('./src/css/**/*.css', {ignoreInitial: true}, css);

// The default (if someone just runs `gulp`) is to run each task in parrallel
exports.default = parallel(css);

// This is our watcher task that instructs gulp to watch directories and
// act accordingly = watcher;

7 — Update npm scripts

In our package.json file, add the following scripts. It might mean replacing a 'start' script that already exists.

"start": "npx gulp && concurrently \"npx gulp watch\" \"npx eleventy --serve\"",
"production": "NODE_ENV=production npx gulp && NODE_ENV=production npx eleventy"

One of the biggest differences between using npm start and npm run production is whether or not the CSS gets compressed.

8 — Install the dependency we need for the new start script

Use npm install concurrently

9 — Put our CSS on the page

Add the following code to our <head>, right before the close. For me, this is in my base.html file.

<style>{% include "css/critical.css" %}</style>

{# Add facility for pages to delare an array of critical styles #}
{% if pageCriticalStyles %}
{% for item in pageCriticalStyles %}
    <style>{% include item %}</style>
{% endfor %}
{% endif %}

<link rel="stylesheet" media="print" href="/fonts/fonts.css?{{ assetHash }}" onload="'all'" />

{# Add facility for pages to declare an array of stylesheet paths #}
{% if pageStylesheets %}
{% for item in pageStylesheets %}
    <link rel="stylesheet" media="print" href="{{ item }}?{{ assetHash }}" onload="'all'" />
{% endfor %}
{% endif %}

Our critical CSS will now be added.

In our page layouts, we can declare pageCriticalStyles as an array of paths to other critical stylesheets, and the CSS will be included in our head as well, on those specific pages.

In our page layouts, we can also declare pageStylesheets, which are non-critical CSS that gets linked to normally using a <link rel="stylesheet">.

10 — Make our assetHash work

At the top of base.html (where our <head> is), add the following:

{% set assetHash = global.random() %}

Then, create a global.js file in our _data folder and add the following:

module.exports = {
random() {
    const segment = () => {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
    return `${segment()}-${segment()}-${segment()}`;

It returns a random string, and helps us make sure our users aren't seeing an outdated CSS file.