Creating a Build Script For a DocPad Site
Scope and Tools
Since my first DocPad project is slowly nearing completion, I decided it's time to create a configuration for it on my continuous integration server. I'm a big proponent of the idea that the build server should only run steps which can easily be repeated on the developer's machine, because it reduces dependency on a specific build server and makes troubleshooting the builds much easier.
This made it clear that I'll need to write a build script first. Although I had no previous experience with any JavaScript based make tools, I soon decided in favor of Grunt because of the large collection of tasks available for it, which should make it fairly easy to achieve everything I wanted to:
- generate the static site from my DocPad project,
- detect broken links in the site, and
- deploy static files to the web server.
Generating the Site
Grunt turned out to be really easy to get up and running thanks to its getting started guide. Once I completed it, I already had a working build script which cleaned the output directory out
in order to remove any obsolete files from the previous site generation. Since I already used CoffeeScript with DocPad, I decided to stick with it in Grunt as well:
module.exports = (grunt) ->
grunt.initConfig(
{
clean: ["out"]
}
)
grunt.loadNpmTasks('grunt-contrib-clean')
grunt.registerTask('default', ['clean'])
To make it work, I only had to install 2 NPM packages as development dependencies:
npm install grunt --save-dev
npm install grunt-contrib-clean --save-dev
Generating the site with DocPad turned out to be a bit more difficult. I had no luck finding a Grunt task for it, so I had to settle with a different solution: I took advantage of the fact that setting up DocPad on the machine includes installing it globally so that docpad
command is always in path. This made it possible to use shell
task to invoke docpad
directly:
module.exports = (grunt) ->
grunt.initConfig(
{
clean: ["out"]
shell:
docpad:
options:
stdout: true
command: "docpad generate --env static"
}
)
grunt.loadNpmTasks('grunt-contrib-clean')
grunt.loadNpmTasks('grunt-shell')
grunt.registerTask('generate', ['clean', 'shell:docpad'])
grunt.registerTask('default', ['generate'])
The first part of my build script was now complete.
Detecting Broken Links
link-checker
task was probably the main reason I chose Grunt as my make tool in the first place. Still; to make it work, I had to have the site running in a web server. Fortunately there's a Grunt task for that as well: connect
can run a web server for the duration of the build script - exactly what I needed.
I quickly configured additional 2 tasks in my build script. I set the port to 9778 to match DocPad's default value when invoking DocPad run
:
grunt.initConfig(
{
connect:
docpad:
options:
port: 9778
base: './out'
'link-checker':
docpad:
site: 'localhost'
options:
initialPort: 9778
}
)
grunt.loadNpmTasks('grunt-contrib-connect')
grunt.loadNpmTasks('grunt-link-checker')
grunt.registerTask('test', ['connect:docpad', 'link-checker:docpad'])
This a time I had 2 minor setbacks before I finally got it work:
link-checker
task had problems with minified JavaScript and CSS files in my project and kept finding bogus missing resources. After inspecting the files I decided to exclude them from the validation. This proved to be an easy task using the configuration options ofnode-simplecrawler
, which is being used under the covers:
grunt.initConfig(
{
'link-checker':
docpad:
site: 'localhost'
options:
initialPort: 9778
callback: (crawler) ->
crawler.addFetchCondition((url) -> !url.path.match(/\.min\./))
}
)
- A bug in
node-simplecrawler
madelink-checker
impossible to use with the latest version of Node.js (v0.12.0), because there was no way to avoid the following error:
Fatal error: "name" and "value" are required for setHeader().
For now I decided to keep using the previous version of Node.js (v0.10.36). Since the fix for the bug is already available, it shouldn't be long before a new version of the task is available which includes it.
Deployment to Web Server
In my case deployment of static files to the web server consists only of copying them to a network share, because both servers are in the same network. This can easily be done using copy
task. Grunt can take care of copying to a network share as long as the correct syntax is used. It's also important to preserve the relative folder structure, which can be done using the cwd
option. This is the end result in my case:
grunt.initConfig(
{
copy:
docpad:
cwd: 'out'
src: '**/*'
dest: '//servername/sharename/foldername/'
expand: true
}
)
grunt.loadNpmTasks('grunt-contrib-copy')
grunt.registerTask('deploy', ['copy'])
Having a build script with 3 tasks, corresponding to the build steps I wanted to achieve originally, was enough to thoroughly test them. I also managed to fix a couple of existing broken links in the site, thanks to it. I even configured a new internal site on the web server which I can use for testing with different mobile devices on the network. I still had to automate these steps on the build server, but that's already a subject for a different post.