Learning Assemble, Step 3 - Grunt Workflow

2014-10-24

Our static web site project needs a lot more than just Assemble, and we're going to take a quick break to set up some non-Assemble tasks in Grunt. We need Grunt to set up a useful workflow, including some way of previewing and debugging our static site. In this post, we'll go through the configuration of some additional Grunt tasks for a basic static site workflow.

  • Watch
  • Connect
  • Combined Build and Server Tasks
  • Livereload
  • Clean

Watch

Watch (grunt-contrib-watch) helps update your output files as you make changes to the source files. Watch is the foundation for a very convenient preview and feedback cycle. Add watch to your project using NPM:

npm install grunt-contrib-watch --save-dev

Then update Gruntfile.js to configure Watch:


module.exports = function (grunt) {
    grunt.initConfig({
        assemble: {
            hello: {
                options: {
                    layoutdir: 'source/templates/layouts',
                    layout: 'default.hbs',
                    partials: 'source/templates/partials/**/*.hbs'
                },
                files: [
                    { expand: true, cwd: 'source/templates/pages', src: '**/*.hbs', dest: 'output/' }
                ]
            }
        },
        watch: {
            templates: {
                files: ['source/templates/**/*.hbs'],
                tasks: ['assemble']
            }
        }
    });
    grunt.loadNpmTasks('assemble');
    grunt.loadNpmTasks('grunt-contrib-watch');
};

At the bottom of the file, we loaded the default task for grunt-contrib-watch, which is executed as grunt watch. This starts the never-ending watch loop, based on the configuration you supply about which files to watch and what to do when they change. We specified a file spec for our Handlebars templates, and to run the 'assemble' task when any of those change. As our project grows, we will have many more targets under the watch configuration, each specifying some group of source files and their related processing commands.

Go ahead and try running grunt watch.

$ grunt watch
Running "watch" task
Waiting...

It doesn't look like much. You have to make a change to a file to see some action. Let's type some gibberish in the index.hbs page template, and save the file.

>> File "source/templates/pages/index.hbs" changed.
Running "assemble:hello" (assemble) task
Assembling output/about.html OK
Assembling output/features.html OK
Assembling output/index.html OK
Assembling output/pricing.html OK
>> 4 pages assembled.

Done, without errors.
Completed in 1.999s at Thu Oct 23 2014 14:35:51 GMT-0700 (PDT) - Waiting...

Not bad at all. This goes on forever until you stop it with Ctrl-C, or the process crashes.

Connect

Connect (grunt-contrib-connect) is a very useful web server component for Grunt. It sets up a simple server so we don't have to have any web server code of our own. Since we are only deploying the static content, we don't need anything more complicated. Add Connect to your project using NPM:

npm install grunt-contrib-connect --save-dev

Then update Gruntfile.js to configure Connect:


module.exports = function (grunt) {
    grunt.initConfig({
        assemble: {
            hello: {
                options: {
                    layoutdir: 'source/templates/layouts',
                    layout: 'default.hbs',
                    partials: 'source/templates/partials/**/*.hbs'
                },
                files: [
                    { expand: true, cwd: 'source/templates/pages', src: '**/*.hbs', dest: 'output/' }
                ]
            }
        },
        connect: {
            preview: {
                options: {
                    base: ['output'],
                    port: 9000,
                    hostname: 'localhost',
                    keepalive: true
                }
            }
        },
        watch: {
            templates: {
                files: ['source/templates/**/*.hbs'],
                tasks: ['assemble']
            }
        }
    });
    grunt.loadNpmTasks('assemble');
    grunt.loadNpmTasks('grunt-contrib-connect');
    grunt.loadNpmTasks('grunt-contrib-watch');
};

Our Connect task configuration specifies that Connect will serve files from the 'output' folder to http://localhost:9000/. Try running grunt connect and aiming a browser at localhost:9000 to see our generated pages. Because we specified the 'keepalive: true' option, Connect keeps running forever, until we shut it down, just like Watch did. If we were using Connect in isolation, that would be great. Instead, we want Watch and Connect to play nice together.

Combined Build and Server Tasks

Grunt supports alias tasks that will run run multiple Grunt tasks in sequence. We're going to use these to coordinate our individual tasks into workflows. Let's create two to start, one to build our output files, and one to run the preview server.


module.exports = function (grunt) {
    grunt.initConfig({
        assemble: {
            hello: {
                options: {
                    layoutdir: 'source/templates/layouts',
                    layout: 'default.hbs',
                    partials: 'source/templates/partials/**/*.hbs'
                },
                files: [
                    { expand: true, cwd: 'source/templates/pages', src: '**/*.hbs', dest: 'output/' }
                ]
            }
        },
        connect: {
            preview: {
                options: {
                    base: ['output'],
                    port: 9000,
                    hostname: 'localhost',
                    keepalive: false
                }
            }
        },
        watch: {
            templates: {
                files: ['source/templates/**/*.hbs'],
                tasks: ['assemble']
            }
        }
    });
    grunt.loadNpmTasks('assemble');
    grunt.loadNpmTasks('grunt-contrib-connect');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.registerTask('build', ['assemble']);
    grunt.registerTask('server', ['build','connect','watch']);
};

You can see our alias tasks at the bottom of the file, created with the registerTask function.

  • Our build task is a bit light for now, it just runs Assemble. But it will grow.
  • The server task first runs the build to baseline the output, then starts a Connect server, and finally runs Watch to keep everything going in a monitoring loop. We are now changing the Connect 'keepalive' setting to false so that Connect will defer to Watch.

Give this a try by running grunt server. Point your browser at localhost:9000. Modify some of the page files to see Watch run Assemble after changes, and refresh your browser to see the results. You'll notice how painful it was to push the refresh button on your browser, and you'll be happy to know that the Livereload feature can help relieve you of that heavy burden.

Livereload

Livereload is an optional feature of grunt-contrib-connect and grunt-contrib-watch that maintains a socket connection with your browser to cause it to refresh when server files have changed. If you can put your browser on a second screen, you can watch your static site auto-magically update as you work. All we need is some additional Gruntfile configuration:


module.exports = function (grunt) {
    grunt.initConfig({
        assemble: {
            hello: {
                options: {
                    layoutdir: 'source/templates/layouts',
                    layout: 'default.hbs',
                    partials: 'source/templates/partials/**/*.hbs'
                },
                files: [
                    { expand: true, cwd: 'source/templates/pages', src: '**/*.hbs', dest: 'output/' }
                ]
            }
        },
        connect: {
            preview: {
                options: {
                    base: ['output'],
                    port: 9000,
                    hostname: 'localhost',
                    keepalive: false,
                    livereload: 35729
                }
            }
        },
        watch: {
            templates: {
                files: ['source/templates/**/*.hbs'],
                tasks: ['assemble']
            },
            livereload: {
                options: {
                    livereload: '<%= connect.preview.options.livereload %>'
                },
                files: ['output/**.*']
            }
        }
    });
    grunt.loadNpmTasks('assemble');
    grunt.loadNpmTasks('grunt-contrib-connect');
    grunt.loadNpmTasks('grunt-contrib-watch');

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

We made only a few changes. We added the livereload port number to the Connect configuration. We created a new Watch target, to watch the output files, and set it up to reference the livereload port from the Connect configuration. Specifying a port effectively turns it on.

Try repeating your exercise of running the grunt server task, opening a page in your browser, then saving a change to the source page file. Not only will Watch run Assemble to compile your page, but livereload will refresh your browser. If you are curious, view the source of the page from your browser, you can see the script injected for livereload.

<!DOCTYPE html>
<html>
<head>
    <title>Home</title>
</head>
<body>
<div>
    <a href="/">Home</a> |
    <a href="/features.html">Features</a> |
    <a href="/pricing.html">Pricing</a> |
    <a href="/about.html">About</a>
</div>

<h1>Home</h1>
<p>This is the home page</p>


<script>//<![CDATA[
document.write('<script src="//' + (location.hostname || 'localhost') + ':35729/livereload.js?snipver=1"><\/script>')
//]]></script>
</body>
</html>

Clean

Last, but not least, grunt-contrib-clean will help us make sure our output is the true reflection of generating the recent source files. Add grunt-contrib-clean to the project:

npm install grunt-contrib-clean --save-dev

For now, we'll just run Clean at the start of the build task. Configuring Clean is pretty simple, it accepts a list of file specifications to be cleaned out, which is just the output folder and its contents for now. In a complex workflow, we might want to be careful about deleting everything from the output because of the time required to regenerate it all. We don't have that problem yet.


module.exports = function (grunt) {
    grunt.initConfig({
        assemble: {
            hello: {
                options: {
                    layoutdir: 'source/templates/layouts',
                    layout: 'default.hbs',
                    partials: 'source/templates/partials/**/*.hbs'
                },
                files: [
                    { expand: true, cwd: 'source/templates/pages', src: '**/*.hbs', dest: 'output/' }
                ]
            }
        },
        clean: ['output/**'],
        connect: {
            preview: {
                options: {
                    base: ['output'],
                    port: 9000,
                    hostname: 'localhost',
                    keepalive: false,
                    livereload: 35729
                }
            }
        },
        watch: {
            templates: {
                files: ['source/templates/**/*.hbs'],
                tasks: ['assemble']
            },
            livereload: {
                options: {
                    livereload: '<%= connect.preview.options.livereload %>'
                },
                files: ['output/**.*']
            }
        }
    });
    grunt.loadNpmTasks('assemble');
    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-connect');
    grunt.loadNpmTasks('grunt-contrib-watch');

    grunt.registerTask('build', ['clean', 'assemble']);
    grunt.registerTask('server', ['build','connect','watch']);
};

Give that a try. In the next post, we'll get back to Assemble.

For the code, please see the learning-assemble repository on GitHub. The branch step03-grunt-workflow contains the completed code from this post.

Next: Learning Assemble, Step 4 - Data