Dev Notes

Software Development Resources by David Egan.

Grunt Uncss and Jekyll


Bootstrap, Grunt, Jekyll, Uncss
David Egan

Front-end frameworks like Bootstrap and Foundation provide a really good way to deliver high-quality modern responsive websites. They take care of the grunt work in terms of layout and common design elements and thereby drastically speed up development time.

I don’t buy the argument that they constrain design. You can style and override these frameworks as required.

They do have a major disadvantage -size. You’re often including (a lot of) rules in your stylesheet that are not used in the current project. Enter Uncss. This article describes how I use Uncss with Grunt on Jekyll projects.

Uncss

Uncss is a node module that removes unused CSS from stylesheets.

On a (fairly typical) recent Jekyll project, Uncss helped me to trim a 190 kB stylesheet down to 32 kB. This is great for user experience, as it speeds up page-load time. It’s probably better for the environment as well, as we’re zapping less bytes down the wire to get the same result.

How Uncss works:

  • HTML files are loaded by PhantomJS, a headless browser.
  • Stylesheets are parsed by PostCSS - a tool that allows JS plugins to transform styles.
  • If a selector is not found in the HTML files, it is removed.
  • Remaining rules are converted back to CSS.

You can specify rules to be ignired by Uncss - for example, styles that are added by JavaScript.

Grunt & Uncss

We use Grunt as a front end task runner on our Jekyll projects. No, I don’t care that it’s old-fashioned (!). It performs really well for us and there is no need to change it for a shinier tool in this context.

We use the grunt-uncss grunt task to run Uncss in development.

To install:

# npm
npm install grunt-uncss --save-dev

# better yet, use Yarn - it's quicker:
yarn add grunt-uncss --dev

Example grunt-uncss Task

We split out grunt tasks into separate files. Contents of a typical config/uncss.js file:

module.exports = {
  dist: {
    options: {
      ignore: [
        /\w\.in/,
        '.top-nav-collapse',
        'img-caption',
        '.top-nav-collapse',
        '.fade',
        '.collapse',
        '.collapsing',
        /(#|\.)navbar(\-[a-zA-Z]+)?/,
        /(#|\.)dropdown(\-[a-zA-Z]+)?/,
        '.bs.carousel',
        '.slid.bs.carousel',
        '.slide.bs.carousel',
        '.fade',
        '.fade.in',
        '.collapse',
        '.collapse.in',
        '.collapsing',
        '.alert-danger',
        '.carousel-inner > .next',
        '.carousel-inner > .prev',
        '.carousel-inner > .next.left',
        '.carousel-inner > .prev.right',
        '.carousel-inner > .active.left',
        '.carousel-inner > .active.right',
        '.navbar-fixed-top'
      ],
      stylesheets: ['../css/main.css'],
      ignoreSheets: [/fonts.googleapis/]
    },
    files: {
      'css/main.un.css': ['_site/index.html']
    }
  }
};

Grunt Uncss with Jekyll

We manage all our Jekyll builds by means of Grunt tasks. Specifically, we use the grunt-shell package to build and deploy Jekyll. This provides a convenient way of specifying different configurations for different environments.

We generally include the uncss task on staging and production builds, but not on development builds. Actually we keep the staging build pretty much identical to production for better quality assurance.

Before Grunt Uncss can work, it needs something to work on, which is why the staging grunt task below starts with a Jekyll build ('shell:jekyllNoMap'). More on the “NoMap” issue later…

// Taken from `/Gruntfile.js`

// Deploy to Staging with `grunt deployStaging` then reset the local copy.
    grunt.registerTask( 'deployStaging', [
        'shell:jekyllNoMap',    // Build the site (to _site)
        'sass:production',      // Run sass task to build CSS
        'uncss:dist',           // Run uncss on the built HTML/CSS
        'cssmin',               // Minify the processed CSS
        'uglify:dist',          // Uglify site JS
        'shell:jekyllStaging',  // Run the staging build (CSS previously generated)
        'copy:fonts',           // Copy fonts to _site
        'shell:rsyncStaging',   // Deploy to staging
        'sass:development',     // After deployment, run dev tasks (for convenience)
        'concat',
        'shell:jekyllDev',
        'copy:fonts',
        'watch'
    ]);

Problems with Google Maps

We recently experienced a problem with gulp-uncss and Google maps. After updating the project, the Uncss task was providing an incomplete style sheet - with no error messages.

Some ideas for debugging:

  • Look through the generated (un)css file and search for a style you know should be there: if it isn’t, uncss has encountered a problem
  • Make sure your HTML and CSS is valid
  • Comment out scripts and go through a process of elimination

In this particular case, the problem was caused by a Google maps call in the <head>:

<script src="https://maps.googleapis.com/maps/api/js?key={{site.data.map.api-key}}"></script>

With this line commented out, uncss worked perfectly. I have a feeling that this issue is caused by PhantomJS and the fact that the site is running on the http protocol, whereas the link to Google is https.

To be honest, sometimes you can’t justify the time to get to the bottom of issues like this - you just need a quick fix. Fortunately Jekyll makes it pretty easy to define environments by including multiple config files. For example, to build a dev environment you include a _config_dev.yml config file in your Jekyll build. For example, see our shell.js config:

module.exports = {
  jekyllNoMap: {
    command: 'rm -rf _site/*; jekyll build --config _config.yml,_config_staging.yml,_config_no_map.yml',
    stdout: true
  },
  jekyllStaging: {
    command: 'rm -rf _site/*; jekyll build --config _config.yml,_config_staging.yml',
    stdout: true
  },
  jekyllDev: {
    command: 'rm -rf _site/*; jekyll build --config _config.yml,_config_dev.yml',
    stdout: true
  },
  jekyllProduction: {
    command: 'rm -rf _site/*; jekyll build --config _config.yml,_config_production.yml',
    stdout: true
  },
  rsyncStaging: {
    command: 'deploy-staging'
  },
  rsyncProduction: {
    command: 'deploy-production'
  }
};

The noMap config file _config_no_map.yml looks like this:

url: "http://example.com/staging-site"
baseurl: "/staging-site"
production: true
environment: staging
include-map: false

You can then exclude the map script if include-map returns false:

<!-- in <head> -->

{% if page.map-height != nil and site.include-map != false %}
    {% include partials/map-scripts.html %}
{% endif %}

comments powered by Disqus