Grunt and Hugo: A Fast Iteration Cookbook

This is a selection of code snippets that can be used to assemble a nice live editing environment for static sites using Grunt. The focus is on Hugo, LESS and CoffeeScript, but most of the code could easily be repurposed for similar tools. In addition to the markup and script compilation, we’ll add automatic image resizing using grunt-responsive-images.

The Gruntfile broken down here is in CoffeeScript since that’s what I happened to use, but it should be straightforward to transliterate into plain JavaScript if required.

Getting Started

First make sure node.js and npm are installed. npm is bundled with the standard node.js binary installer available here.

Initialize a package.json and install the required plugins by running the following commands in the project directory. Feel free to omit plugins you don’t need.

$ cd mysite
$ npm init
$ npm install grunt --save-dev
$ npm install grunt-contrib-watch --save-dev
$ npm install grunt-contrib-connect --save-dev
$ npm install grunt-contrib-copy --save-dev
$ npm install grunt-contrib-coffee --save-dev
$ npm install grunt-contrib-uglify --save-dev
$ npm install grunt-contrib-less --save-dev
$ npm install grunt-responsive-images --save-dev

All being well, the project now has a complete package.json, allowing the packages above to be installed at any time in the future with the command npm install. There’s no need to do this now.

If npm complains about the site lacking a README file, add "private": true to package.json to make it stop.

Finally, the grunt command used to actually run the build system comes from the grunt-cli package, which should be installed globally with npm install -g grunt-cli.

The Gruntfile

Paste the skeleton code below into a file called at the top of the project. (Alternatively, start from the full and delete the bits you don’t want). This basic setup code loads plugins and defines the tasks exposed by the Gruntfile.

module.exports = (grunt) ->
        pkg: grunt.file.readJSON('package.json')

    grunt.loadNpmTasks plugin for plugin in [
    grunt.registerTask 'dev', [
    grunt.registerTask 'default', [
    grunt.registerTask 'edit', ['connect', 'watch']

Together these tasks form the command line interface for our build system. They can be used as follows.

grunt Builds the site for distribution in build/dist.
grunt dev Builds the site for development in build/dev.
grunt edit Starts the local server and watches the filesystem for changes. The site can be viewed at

The sections below provide configurations for various tools that can be pasted into this basic and customized as required.

Building The Site

To keep things tidy, I like to put the Hugo site into a subdirectory called site under the project root. The rest of the article follows that convention, but you may of course choose your own directory, or just use the project root.

The following code registers a task that runs hugo to compile the site directory, placing the rendered result in either build/dev or build/dist (for development and deployment builds respectively). The target is chosen by referring to the task as hugo:dev or hugo:dist.

grunt.registerTask 'hugo', (target) ->
    done = @async()
    args = ['--source=site', "--destination=../build/#{target}"]
    if target == 'dev'
        args.push '--baseUrl='
        args.push '--buildDrafts=true'
        args.push '--buildFuture=true'
    hugo = require('child_process').spawn 'hugo', args, stdio: 'inherit'
    (hugo.on e, -> done(true)) for e in ['exit', 'error']

In the dev configuration, which builds the site for live editing on a local server, it’s important to supply the --baseUrl (-b) option to hugo. This overrides the BaseUrl specified in the site’s config.toml with the URL of our local development server. Without the override Hugo will generate absolute URLs referring to the site’s public server.

Development Server

Hugo comes with a built-in development web server usable via hugo server. While this is a good option for working on content alone, it’s awkward to make the current version live-reload while working on CSS or scripts.

For that reason we’ll use grunt-contrib-connect instead. This is a Grunt plugin that runs a static file server. The configuration below serves the file tree generated by Hugo in build/dev.

            hostname: ''
            port: 8080
            protocol: 'http'
            base: 'build/dev'
            livereload: true

The livereload option turns on a middleware that injects livereload.js into HTML responses generated by the server. This script connects back to the server on port 35729 and waits to be told that it should refresh the page. For this to work we’ll need to be running a LiveReload service on port 35729, which is the job of grunt-contrib-watch, as explained below.

Watching for Changes

With a development server and a task to run hugo in place, the final component of a basic live editing environment is grunt-contrib-watch, which watches the filesystem and runs Grunt tasks when something changes.

The configuration below simply watches for changes in the various directories containing source files (site, less, coffee and img), and runs the appropriate tool in response.

Changes will cascade so that, for example, when the LESS compiler updates a stylesheet in site/static/css, grunt-contrib-watch will notice and run hugo to rebuild the site, resulting in the new file being copied over to build/dev. This is perhaps not the most efficient arrangement, but it’s straightforward, and Hugo is quick enough that running it on every change doesn’t add much latency.

        atBegin: true
        livereload: true
        files: ['less/*.less']
        tasks: 'less:dev'
        files: ['coffee/*.coffee']
        tasks: ['coffee', 'copy:coffee']
        files: ['img/**']
        tasks: 'responsive_images'
        files: ['site/**']
        tasks: 'hugo:dev'
        files: ['']
        tasks: 'dev'

The two options supplied to the watch task are:

  • livereload, which tells grunt-contrib-watch to run a LiveReload service on port 35729. Recall that we’re using grunt-contrib-connect to inject livereload.js into our HTML pages. livereload.js will connect back to this service from the browser and refresh the page when things change.
  • atBegin, which causes all watch tasks to be run at startup, ensuring that the site is up to date when we start editing.


To compile LESS to CSS we’ll use the grunt-contrib-less plugin.

$ npm install grunt-contrib-less --save-dev
$ npm install less-plugin-clean-css --save-dev
$ npm install less-plugin-autoprefix --save-dev

The LESS compiler lessc supports plugins of its own which provide a very convenient way of post-processing CSS. This configuration makes use of the following two:

less-plugin-clean-css Minifies CSS.
less-plugin-autoprefix Adds vendor prefixed (-webkit-, -moz-, etc.) variants of CSS properties to improve browser compatibility.

Here’s the less configuration that runs lessc on our site’s main LESS file, less/mysite.less. It defines a pair of subtasks, less:dev and less:dist, the latter generating a prefixed and minified stylesheet for deployment.

        paths: ['less/']
        src: ['less/mysite.less']
        dest: 'site/static/css/mysite.css'
        paths: ['less/']
        src: ['less/mysite.less']
        dest: 'site/static/css/mysite.min.css'
            plugins: [
                new (require 'less-plugin-autoprefix') browsers: ['> 0.1%']
                new (require 'less-plugin-clean-css') {}

The browsers option to less-plugin-autoprefix determines the set of CSS properties to be prefixed, and should be set according to the level of compatibility required. See the documentation for details.


Here we configure grunt-contrib-coffee to compile scripts under the coffee directory and sandwich them into a single .js file, site/static/js/mysite.js. If using source maps the grunt-contrib-copy plugin is also required for reasons explained below.

$ npm install grunt-contrib-coffee --save-dev
$ npm install grunt-contrib-copy --save-dev

This is the configuration block.

        join: true             # Concatenate before, not after compilation.
        sourceMap: true        # Make a source map.
        sourceRoot: '/coffee/' # URL debugger should use to download .coffee files.
        inline: true           # Embed coffee source inside the source map.
        src: ['coffee/', 'coffee/', /* ADD FILES HERE */]
        dest: 'site/static/js/mysite.js'

Multiple .coffee files can be built by simply listing them in the src array. It’s inadvisable to use a wildcard (e.g. coffee/*), because the order in which the matched files are concatenated is not defined. Instead, list each file individually in the desired order.

The join option instructs the plugin to concatenate the .coffee files before passing them to the coffee compiler, rather than concatenating the resulting .js files after compilation. This is generally what is wanted because it allows the scripts to share a global scope.

Source Maps

Source maps are essential for a sane debugging experience. In the configuration above we’ve turned them on using sourceMap: true, which causes the coffee compiler to generate alongside mysite.js.

By default source maps don’t contain any actual source code. Instead they refer to source files (in our case, the .coffee files) by relative path, and the debugger in the browser must download the source files as required.

This is an awkward arrangement because, of course, for the debugger to download the .coffee files they have be made available on the server somewhere. An easier option is to have the compiler embed the source directly into the map by setting inline: true.

If you don’t care about debugging minified JavaScript (a dubious proposition at the best of times), I recommend setting inline: true and ignoring the rest of this section. Source maps with embedded code are self-contained so there’s no need to publish the .coffee files anywhere.

Publishing the .coffee Files

The disadvantage of inline is that UglifyJS seems incapable of using embedded CoffeeScript source when building a source map for the minified JavaScript. However, if the sources are referenced instead of embedded (i.e. inline is false), everything works well.

If we don’t embed the source code in the map, it’s necessary to publish the .coffee files on the server somewhere. This can be achieved very simply by configuring grunt-contrib-copy to copy our .coffee sources into the public site at site/static/coffee.

        src: 'coffee/*'
        dest: 'site/static/'

This makes the coffee directory accessible on the server at /coffee. Note that we’ve also set sourceRoot to /coffee/. This path is embedded in the source map, and tells the debugger where to download referenced files.


In the dist configuration we’ll use grunt-contrib-uglify to minify mysite.js.

        sourceMap: true
        sourceMapIn: 'site/static/js/'
        src: 'site/static/js/mysite.js'
        dest: 'site/static/js/mysite.min.js'

The sourceMap option tells UglifyJS to generate a source map for the minified script, and sourceMapIn provides it with the original map generated by the coffee compiler.

The rationale for providing the original map is that it allows the map for the compressed script to directly reference the .coffee source, instead of just the JavaScript generated by the coffee compiler, which makes for a nicer debugging experience. However, as described above, this doesn’t work properly when the CoffeeScript is embedded with inline.

It’s worth noting that even with a source map available, debugging minified JavaScript is difficult because the minifier can fold many source lines into one to save space. If at all possible, debug using the unminified script.

Image Resizing

A great convenience offered by platforms like Wordpress is automatic image resizing and cropping. The handy grunt-responsive-images plugin can provide a similar facility for working with static sites.

The basic idea is to put source images into a directory called img at the top of the project, and configure the plugin to generate multiple cropped and resized variants under site/static/img. Thanks to grunt-contrib-watch, the image processing happens automatically as soon as a new image is copied into img.

First make sure grunt-responsive-images is installed.

$ npm install grunt-responsive-images --save-dev

The actual image processing work is done by ImageMagick or GraphicsMagick, one of which must be installed and available on the system PATH for image resizing to work. It makes no difference which.

Here’s the configuration block. See below for instructions on what to put in the sizes array.

      engine: 'gm'
      separator: '_'
      newFilesOnly: true
      sizes: [ /* PUT IMAGE SPECIFICATIONS HERE */ ]
    files: [
      expand: true
      cwd: 'img'
      src: '**.{png,jpg,jpeg,gif}'
      dest: 'site/static/img'

The important options to note are:

engine gm for GraphicsMagick, im for ImageMagick
separator Delimiter used between the name stem and the suffix specified in the size definition.
newFilesOnly Set to false to force regeneration.
sizes An array of size specifications (see below).

All that remains is to add a specification to the sizes array for each image variant required. The following templates cover the most useful operations. Copy, paste and customize!

  • Copy the Source Image

    Copies the source image, unmodified, into the destination directory.

    { rename: false, width: '100%', height: '100%' }

  • Crop to an Exact Size

    Generates a thumbnail of exactly 64×64 pixels. Setting aspectRatio to false enables cropping.

    { name: '64x64', width: 64, height: 64, aspectRatio: false }

  • Proportional Scale (Single Axis Constraint)

    Generates an image exactly 300px wide, with a height determined by the source image’s aspect ratio.

    { name: '300', width: 300 }

  • Proportional Scale (Both Axes Constrained)

    Scales the image down proportionally until it is neither wider than 400px nor taller than 250px.

    { name: '450x250', width: 400, height: 250, aspectRatio: true }

Full documentation for grunt-responsive-images is here.


The full is available here.

I hope you found this little guide helpful. Corrections, comments and questions are always welcome.