How to build a static website with Grunt, Jekyll, and LibSass

A 9 minute read written by Graeme April 20, 2015

An illustration of a bash command line with two faux commands that read Grunt Jekyll LibSass you rock.

Who doesn’t want to have super hero skills they can put into action whenever they want? With Grunt, Jekyll, and LibSass (which pretty much already sound like super heroes) you get powerful tools right at your fingertips. This tutorial outlines how to get a static site up and running on Jekyll, with basic automation powered by Grunt, and lightning-fast Sass preprocessing with LibSass.

For anyone that isn’t familiar, Jekyll is a static site generator powered by Ruby that doesn’t require a database or server-side languages to render on the web. It takes basic text files in a variety of formats and converts them to a ready to deploy, flat-file package that is very lightweight, fast, and secure.

As for Grunt, it is a JavaScript powered task runner that simplifies and automates repetitive tasks like minification, compilation, testing, etc. In fact, it’s so powerful that you can automate just about anything. The great thing is that people like you and I can spend less time on mundane tasks and more time writing important code.

Before you start: dependencies

To leverage the basics of Jekyll and Grunt you’ll need to have Ruby, RubyGems, and Node.js installed on your computer. Hopefully, you’re also using a Ruby version manager like rbenv or RVM. If not, have a look at some of the managers listed at ruby-lang.org.

Installing Jekyll and Grunt CLI

In a terminal, install Jekyll:

gem install jekyll

and install the Grunt CLI globally:

npm install -g grunt-cli

Scaffolding a basic Jekyll site

Create your basic site files and folders in a location of your choice and change directory to the new site root:

jekyll new mysitename
cd mysitename

At this point you’ll have a really basic, ready to edit and build site with default Jekyll commands. I won’t elaborate on how to configure and run it, but check out the documentation on Jekyll’s site if you’re unfamiliar.

Setting up Grunt

Before you can start creating tasks for Grunt to run, you’ll need two files in the root of your project: a package.json file and a Gruntfile.js.

package.json

This file is used by npm (installed with node.js) to handle information about your project and its plugin dependencies. Plugins are how you extend Grunt to perform a huge variety of pre-built tasks. By listing plugin names and version numbers, npm can install the required versions of any plugins needed by your project to run.

Start your package.json file with a few basic plugins like so:

{
  "name": "my-site-name",
  "description": "My new Jekyll site.",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-watch": "~0.6.1",
    "grunt-shell": "^1.1.2",
    "load-grunt-tasks": "~0.6.0",
    "time-grunt": "~0.4.0"
  }
}

At this point you need to have npm install these dependencies locally to your project by running:

npm install

You’ll notice this creates a node_modules folder in the root of your project. This is where your plugin dependencies are stored to be accessed locally by your project.

The plugins you’ve installed so far and their purpose are as follows:

  • Grunt: Grunt itself, not the CLI.
  • grunt-contrib-watch: Allows Grunt to constantly watch for files to change, and run tasks when a change happens.
  • grunt-shell: Lets Grunt run shell commands via the terminal.
  • load-grunt-tasks: Saves a bit of work and clutter in the Gruntfile by automatically loading the plugins found in the package.json file into the Gruntfile. Otherwise you have to manually load each one into the Gruntfile.
  • time-grunt: Renders the time it takes to execute Grunt tasks in the terminal to help visualize performance.

At this point we can build our Gruntfile and start putting Grunt to work.

Gruntfile.js

This file is where you write all of your Grunt tasks and make the magic happen. Here you’ll start simple, but the sky’s the limit when it comes to to how much you can do inside your Gruntfile. When we run Grunt commands from the terminal, this is the file it uses as its instruction manual.

Start out your Gruntfile with a basic outline like so:

'use strict';

module.exports = function (grunt) {

    // Show elapsed time after tasks run to visualize performance
    require('time-grunt')(grunt);
    // Load all Grunt tasks that are listed in package.json automagically
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        // This is where our tasks are defined and configured

    });

};

This outlines your Gruntfile so that it’s ready to have tasks defined and configured. To learn more about the basic structure of a Gruntfile, read Grunt’s getting started page.

Adding Grunt tasks

To make the automagic happen, you need to define some tasks for Grunt to run. Start by letting Grunt take control of the basic Jekyll “serve” and “build” commands. To do this you’ll leverage the Grunt-shell plugin and define a task called “shell” with a configuration for each Jekyll command.

'use strict';

module.exports = function (grunt) {

    // Show elapsed time after tasks run to visualize performance
    require('time-grunt')(grunt);
    // Load all Grunt tasks that are listed in package.json automagically
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        // shell commands for use in Grunt tasks
        shell: {
            jekyllBuild: {
                command: 'jekyll build'
            },
            jekyllServe: {
                command: 'jekyll serve'
            }
        }

    });

};

Now you need to register a couple of Grunt tasks and assign specific tasks for them to perform.

'use strict';

module.exports = function (grunt) {

    // Show elapsed time after tasks run to visualize performance
    require('time-grunt')(grunt);
    // Load all Grunt tasks that are listed in package.json automagically
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        // shell commands for use in Grunt tasks
        shell: {
            jekyllBuild: {
                command: 'jekyll build'
            },
            jekyllServe: {
                command: 'jekyll serve'
            }
        }

    });

    // Register the grunt serve task
    grunt.registerTask('serve', [
        'serve'
    ]);

    // Register the grunt build task
    grunt.registerTask('build', [
        'shell:jekyllBuild',
        'sass'
    ]);

    // Register build as the default task fallback
    grunt.registerTask('default', 'build');

};

Now Grunt has taken control of Jekyll, so to serve or build your site you can run grunt serve or grunt build, respectively. Go ahead and run grunt serve and pull up http://localhost:4000 in your browser, and you should see the lovely Jekyll starter site running on a local development server launched by Jekyll.

Now to add LibSass

As promised, and because you’re awesome and believe in CSS preprocessing, you now need to get LibSass installed and set up. To do this you’re going to remodel the Jekyll project slightly so that Grunt can take complete control over the Sass files and CSS generation.

The first thing to do is to tell Jekyll to no longer watch for Sass file changes or compile them at all by adding the following to your Jekyll _config.yml file:

exclude: [‘css’, ‘_scss’]

While you’re at it, also tell Jekyll not to erase the CSS directory in the _site output folder each time it rebuilds by adding this to _config.yml:

keep_files: ['_site/css']

Out of the box, Jekyll has a separate CSS directory in the root of the project that contains the main.scss file. For simplicity sake, and because Jekyll no longer has to worry about the Sass files, I’ve moved main.scss into the _sass directory and gotten rid of the empty CSS directory. I recommend you do this as well so the upcoming Sass config path examples are consistent with your own.

Finally, for Jekyll to read Sass files it requires you to have blank Front Matter at the top of your main.scss file in the form of a set of triple dashes. Libsass on the other hand does not need Front Matter and will return an error unless you remove it.

Update the Gruntfile

With the Jekyll housekeeping out of the way, install the relevant Grunt plugins and set up tasks to compile and watch for changes in our Sass files.

To install new Grunt plugins to your project, you can either manually add them to your package.json and run npm install, or install them from terminal with the following pattern:

npm install <plugin name> --save-dev

This will both install the plugin’s most recent version locally, and inject it into your package.json file for you.

The two Grunt plugins that you will need are grunt-concurrent, to allow tasks to run simultaneously, and grunt-sass, to run the node-sass compiler. Install them like so:

npm install grunt-concurrent grunt-sass --save-dev

Add corresponding Sass and concurrent tasks to your Gruntfile to configure the relevant Sass settings and define tasks that will run in parallel:

'use strict';

module.exports = function (grunt) {

    // Show elapsed time after tasks run to visualize performance
    require('time-grunt')(grunt);
    // Load all Grunt tasks that are listed in package.json automagically
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        // shell commands for use in Grunt tasks
        shell: {
            jekyllBuild: {
                command: 'jekyll build'
            },
            jekyllServe: {
                command: 'jekyll serve'
            }
        },

        // sass (libsass) config
        sass: {
            options: {
                sourceMap: true,
                relativeAssets: false,
                outputStyle: 'expanded',
                sassDir: '_sass',
                cssDir: '_site/css'
            },
            build: {
                files: [{
                    expand: true,
                    cwd: '_sass/',
                    src: ['**/*.{scss,sass}'],
                    dest: '_site/css',
                    ext: '.css'
                }]
            }
        },

        // run tasks in parallel
        concurrent: {
            serve: [
                'sass',
                'watch',
                'shell:jekyllServe'
            ],
            options: {
                logConcurrentOutput: true
            }
        },

    });

    // Register the grunt serve task
    grunt.registerTask('serve', [
        'shell:jekyllServe'
    ]);

    // Register the grunt build task
    grunt.registerTask('build', [
        'shell:jekyllBuild'
    ]);

    // Register build as the default task fallback
    grunt.registerTask('default', 'build');

};

You want certain tasks to run in parallel so that Grunt can watch for Sass files to change and recompile whenever they do, and so that the Jekyll serve command can also continue to watch for changes to the rest of the source files. You’ll also notice that sourceMap is set to true so that we can more easily debug our Sass in the browser.

All that’s left now is to add the watch task mentioned above, and update the “serve” and “build” Grunt tasks to reflect our new Sass and concurrent tasks:

'use strict';

module.exports = function (grunt) {

    // Show elapsed time after tasks run to visualize performance
    require('time-grunt')(grunt);
    // Load all Grunt tasks that are listed in package.json automagically
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        // shell commands for use in Grunt tasks
        shell: {
            jekyllBuild: {
                command: 'jekyll build'
            },
            jekyllServe: {
                command: 'jekyll serve'
            }
        },

        // watch for files to change and run tasks when they do
        watch: {
            sass: {
                files: ['_sass/**/*.{scss,sass}'],
                tasks: ['sass']
            }
        },

        // sass (libsass) config
        sass: {
            options: {
                sourceMap: true,
                relativeAssets: false,
                outputStyle: 'expanded',
                sassDir: '_sass',
                cssDir: '_site/css'
            },
            build: {
                files: [{
                    expand: true,
                    cwd: '_sass/',
                    src: ['**/*.{scss,sass}'],
                    dest: '_site/css',
                    ext: '.css'
                }]
            }
        },

        // run tasks in parallel
        concurrent: {
            serve: [
                'sass',
                'watch',
                'shell:jekyllServe'
            ],
            options: {
                logConcurrentOutput: true
            }
        },

    });

    // Register the grunt serve task
    grunt.registerTask('serve', [
        'concurrent:serve'
    ]);

    // Register the grunt build task
    grunt.registerTask('build', [
        'shell:jekyllBuild',
        'sass'
    ]);

    // Register build as the default task fallback
    grunt.registerTask('default', 'build');

};

Now when you run grunt serve you should be able to change any Sass file or site source file and see Grunt automatically compile. Refresh the site in your browser to see your changes!

When it comes time to deploy to your server, run grunt build and copy the contents of your _site directory to your server.

What’s next?

At this point you have a very basic Jekyll foundation to build out your static site. The real beauty of it though is that you can use Grunt to expand its capabilities considerably. Admittedly, Jekyll has some pretty decent plugins and features available now, but they don’t compare to the possibilities available with Grunt.

Next you may want to consider tasks like code linting, unit testing, LiveReload/BrowserSync, concatenation, minification, etc. You could even have it automatically deploy your site files to a server using something like rsync. The choice is yours.

Ready to leap tall buildings in a single bound? Take your package management to the next level by checking out Kyle’s post to get an intro on using Bower with Grunt and Node.js.

If you’d like to learn more about Jekyll, static sites, or Grunt, feel free to comment or send me an email.