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 Gruntfile.coffee at the top of
the project. (Alternatively, start from the full
Gruntfile.coffee 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) ->
grunt.initConfig
pkg: grunt.file.readJSON('package.json')
# PUT CONFIGURATION SECTIONS HERE
grunt.loadNpmTasks plugin for plugin in [
'grunt-contrib-watch'
'grunt-contrib-connect'
'grunt-contrib-copy'
'grunt-contrib-coffee'
'grunt-contrib-uglify'
'grunt-contrib-less'
'grunt-responsive-images'
]
grunt.registerTask 'dev', [
'less:dev',
'coffee',
'copy:coffee',
'responsive_images',
'hugo:dev']
grunt.registerTask 'default', [
'less:dist',
'coffee',
'copy:coffee',
'uglify',
'responsive_images',
'hugo:dist']
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 http://127.0.0.1:8080/. |
The sections below provide configurations for various tools that can
be pasted into this basic Gruntfile.coffee 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=http://127.0.0.1:8080'
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.
connect:
mysite:
options:
hostname: '127.0.0.1'
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.
watch:
options:
atBegin: true
livereload: true
less:
files: ['less/*.less']
tasks: 'less:dev'
coffee:
files: ['coffee/*.coffee']
tasks: ['coffee', 'copy:coffee']
images:
files: ['img/**']
tasks: 'responsive_images'
hugo:
files: ['site/**']
tasks: 'hugo:dev'
all:
files: ['Gruntfile.coffee']
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.
LESS
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.
less:
dev:
paths: ['less/']
src: ['less/mysite.less']
dest: 'site/static/css/mysite.css'
dist:
paths: ['less/']
src: ['less/mysite.less']
dest: 'site/static/css/mysite.min.css'
options:
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.
CoffeeScript
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.
coffee:
options:
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.
build:
src: ['coffee/a.coffee', 'coffee/b.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 mysite.js.map 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.
copy:
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.
Minification
In the dist configuration we’ll use grunt-contrib-uglify to minify mysite.js.
uglify:
options:
sourceMap: true
sourceMapIn: 'site/static/js/mysite.js.map'
dist:
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.
responsive_images:
process:
options:
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
aspectRatiotofalseenables 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.
Download
The full Gruntfile.coffee is available here.
I hope you found this little guide helpful. Corrections, comments and questions are always welcome.