Intro to Grunt via the Cabin static site generator

Cabin logo

Cabin is a static site generator powered by Grunt. It uses the Grunt plugin ecosystem to automate all of its required build tasks, resulting in a great workflow. In this post, I will talk about how Grunt works in the context of generating static sites with Cabin.

What is Grunt?

Grunt is a JavaScript task runner to automate dev tasks.

Grunt allows users to automate repetitive tasks which are required for modern web development. When using Grunt, tasks like compiling templates, preprocessing styles, running tests, and building production versions of assets can all be configured in one place and run with a single command.

Grunt is the best task automation tool because of its rich plugin ecosystem. If there is a command line tool you use, there is likely a Grunt plugin which makes it easy to configure with the rest of your tasks.

Getting started with Cabin and Grunt

Getting started with Cabin and Grunt only requires a few commands. First install Cabin and Grunt globally with this command:

Note: To use Cabin you must have node.js, Python (for Pygments), and Compass installed.

npm install -g cabin grunt-cli

Then scaffold a static site with Cabin using the cabin new command:

Note: you can select either the jade or EJS template engine to follow this tutorial.

cabin new site CabinJS/Candy --deployTask GitHub\ Pages

Once the static site generator has been scaffolded, change to the site directory and generate the site using the default Grunt task(more on default tasks later):

cd site && grunt

Understanding the Gruntfile

Now that there is a working site generated using Grunt, let's open the Gruntfile.js file in the site folder and see how it's configured.

After exposing the Gruntfile as a module via module.exports to be called with the grunt object, grunt.initConfig is called to configure the project based on the passed in config; this is where you will spend most of your time when using Grunt.

module.exports = function (grunt) {
  grunt.initConfig({
    // where you configure Grunt tasks
  });
};

Configuring tasks to build the site

The first task configured is the grunt-pages plugin which builds HTML files from markdown posts and templates. To configure grunt-pages, the pages task specifies a posts target(more on targets below) with a few required properties like the post src folder and the dest output folder as well as some optional properties like pageSrc which points to the folder containing page templates.

pages: {
  posts: {
    src: 'posts',
    dest: 'dist',
    layout: 'src/layouts/post.jade',
    url: 'posts/:title/',
    options: {
      pageSrc: 'src/pages',
      data: {
        baseUrl: '/'
      },
      pagination: {
        postsPerPage: 2,
        listPage: 'src/pages/index.jade'
      }
    }
  }
}

Next is the grunt-contrib-compass plugin which compiles Sass files into CSS. The task uses a sassDir option to specify where the source Sass files are located and a cssDir option to specify a directory to output the resulting CSS files.

compass: {
  dist: {
    options: {
      sassDir: 'src/styles',
      cssDir: 'dist/styles'
    }
  }
}

The final build task is the grunt-contrib-copy plugin which copies all files that aren't processed by the above two tasks into the dist folder. Here, the Grunt file configuration is used to copy an array of file globs from the src directory into the dest directory maintaining their relative paths inside of src by using the cwd property.

copy: {
  dist: {
    files: [{
      expand: true,
      cwd: 'src',
      dest: 'dist',
      src: [
        'images/**',
        'scripts/**',
        'styles/**.css',
        'styles/fonts/**',
      ]
    }]
  }
}

Now that everything is configured to build the site, we need a way to automatically rebuild it when source files change. To accomplish this, the grunt-contrib-watch plugin maps a group of files to a task to be run when they change. For example, the pages watch target watches for changes to files in the posts, src/layouts, and src/pages folders then runs the pages task.

watch: {
  pages: {
    files: [
      'posts/**',
      'src/layouts/**',
      'src/pages/**'
    ],
    tasks: ['pages']
  },
  compass: {
    files: ['src/styles/**'],
    tasks: ['compass']
  },
  copy: {
    files: [
      'src/images/**',
      'src/scripts/**',
      'src/styles/**.css',
      'src/styles/fonts/**'
    ],
    tasks: ['copy']
  },
  dist: {
    files: ['dist/**'],
    options: {
      livereload: true
    }
  }
}

There is one target, dist, which doesn't run any tasks when files change, instead it LiveReloads the browser. Since it looks at the files-to-be-served in the dist folder instead of source files, it performs the proper action based on the file extension. Compiled .css files and images are directly injected into the page while everything else triggers a browser refresh.

Task targets

Most grunt plugins are multi tasks, which means they support multiple discrete configurations or targets for the task. For example, the above watch task configures multiple targets to each run a different task based on the location of the changed file.

You could run the first target with the grunt watch:pages command or the second target with the grunt watch:compass command. Runnning grunt watch with no target specified will run all targets in the order specified in the config.

Server task configuration

To serve the site during development, the grunt-contrib-connect plugin is used to start a static file server. In the config, the hostname and port number are specified to serve the site, as well as a base folder of files to serve, and a boolean to opt in for injecting the LiveReload script.

connect: {
  dist: {
    options: {
      port: 5455,
      hostname: '0.0.0.0',
      base: 'dist',
      livereload: true
    }
  }
}

To view the site once the server is started, the grunt-open plugin opens a browser tab to the site's root url.

open: {
  dist: {
    path: 'http://localhost:5455'
  }
}

Deployment task configuration

The grunt-contrib-clean plugin is configured to remove all files in the dist folder so that old files are removed when the site is rebuilt.

clean: {
  dist: 'dist'
}

Finally, the grunt-gh-pages plugin is configured to deploy the site to GitHub. All files in the base dist directory are pushed to GitHub Pages when this task is run.

'gh-pages': {
  options: {
    base: 'dist'
  },
  src: ['**']
}

Custom tasks

Now that all the plugin tasks are configured, there needs to be a simple interface of custom tasks to run sets of plugin tasks in a particular order. Cabin uses four custom tasks, build, deploy, server, and default to create an easy interface to build a site.

build

Cabin's build task cleans the target folder, creates HTML pages, preprocesses styles, and then copies assets into the dist folder to be served. The custom build task has become a convention for Gruntfiles, so it is usually a good idea to have grunt build run a sequence of tasks to create a servable/deployable version of your project.

grunt.registerTask('build', [
  'clean',
  'pages',
  'compass',
  'copy'
]);

deploy

Cabin's deploy task simply runs build to create the site and then gh-pages to deploy it.

grunt.registerTask('deploy', ['build', 'gh-pages']);

server

Cabin's server task runs the build task, starts a connect server, opens a browser tab to view the site, and then watchs source files for any changes so that the site can be rebuilt.

grunt.registerTask('server', [
  'build',
  'connect',
  'open',
  'watch'
]);

default

The default task is run when a user enters grunt into the terminal, so this task should capture the most common use case. In Cabin's Gruntfile, the server task is aliased as the default.

grunt.registerTask('default', 'server');

Loading tasks

Cabin uses load-grunt-tasks to load all Grunt tasks instead of manually calling grunt.loadNpmTask on each grunt task.

require('load-grunt-tasks')(grunt);

Advanced Grunt resources

I published a blog post about Advanced Grunt Tooling if you feel like taking a deeper dive and learning how to optimize your personal Grunt usage. I also suggest using yeoman to generate a project to see some serious Gruntfiles in action. If you are curious about how Grunt works, check out the Grunt API docs.

Conclusion

I hope this post helps you understand Grunt better by seeing a real life example which demonstrates many core Grunt concepts. I wish you the best in utilizing Grunt to make your dev workflow better!