/ ember

Generating a static site build with Broccoli.js

I've used many tools over the years to build and deploy static sites.

I recently had to reboot a site I'd made many years ago and found myself in the position of having to update a build tool I rarely used and have very little retained knowledge of.

In an effort to reduce API cruft in my brain I decided to compile and deploy the new site using broccoli instead, a tool I use every day (albeit indirectly) with ember cli.

In this tutorial you'll learn how to

  • compile handlebars to html
  • minify css
  • setup livereload and dev server
  • create a production build

There is already an excellent framework that does all this for you called taco so if you're not interested in how broccoli works just use that instead, otherwise read on!

Broccoli

Broccoli is an asset pipeline similar to gulp or webpack but with a different approach to rebuilding assets.

One or multiple trees are accepted as inputs to each plugin in the pipeline and one or multiple trees are returned. Finally the trees are merged and a final build written to file.

This approach is used to optimize caching of files which have one or many dependent files - the actual caching logic is handled by each plugin.

You can read a more in-depth explanation here.

Setup

# if you haven't brocolli already installed
npm install -g broccoli-cli

# init
cd my-site
npm init
npm install --save-dev \
    broccoli \
    broccoli-clean-css \
    broccoli-env \
    broccoli-funnel \
    broccoli-handlebars \
    broccoli-livereload \
    broccoli-merge-trees \
    broccoli-source

Brocfile

Add a file called Brocfile.js to the root folder of your site. This one file will contain all the necessary configuration for the build.

Let's look at the Brocfile step by step:

Plugins

const { WatchedDir } = require('broccoli-source');
const Funnel = require('broccoli-funnel');
const merge = require('broccoli-merge-trees');
const LiveReload = require('broccoli-livereload');
const env = require('broccoli-env').getEnv();
const BrocHbs = require('broccoli-handlebars');
const BrocCleanCss = require('broccoli-clean-css');

At the start of the Brocfile we require build plugins.

Source

const site = new WatchedDir('./');

Create a Broccoli tree referring to a directory on disk. The Broccoli watcher used by broccoli serve will watch the directory and all subdirectories, and trigger a rebuild whenever something changes.

Funnel

const public = new Funnel(site, {
  files: ['favicon.ico', 'robots.txt', 'sitemap.xml']
});

Funnel takes a tree of files and funnels it into a smaller tree. Here the input is the complete site directory tree and the output is a tree containing only the three named files.

Handlebars

let html = new BrocHbs([site], ['**/*.hbs'], {
  partials: '/partials'
});

html = new Funnel(html, {
  exclude: ['node_modules', 'partials'],
  include: ['**/*.html']
});

The Handlebars compiler expects an array of trees and an array of handlebars files as arguments.

The partials option is required to compile partial templates, for example a site layout for each webpage:

# layout.hbs
<!doctype html>
<html lang="en">
  <head></head>
  <body class={{bodyClass}}>
      # this guard is required when layout.hbs is parsed individually i.e. without a block present
      {{#if @partial-block}}
        {{> @partial-block }}
      {{/if}}
  </body>
</html>

# index.hbs
{{#> layout bodyClass="homepage"}}
    <h1>My Blog</h1>
    <article>Hello World!</article>
{{/layout}}

Finally the outputted tree is funneled to remove other files and folders created during the compilation process.

CSS

let css = new Funnel(site, {
  include: ['*.css'],
  destDir: '/assets'
});

if (env === 'production') {
  css = new BrocCleanCss(css);
}

CSS files are funneled from the site tree and moved to a new directory called "assets".

The assets directory will be a sub-directory of the build location which is specified via the command line.

CSS is minified using the clean css library but only for production.

Images

Images are funneled from the images directory within the site tree and moved to the assets folder withing the build folder.

const images = new Funnel(site, {
  srcDir: 'images',
  include: ['*.png', '*.jpg', '*.gif'],
  destDir: '/assets'
});

Live reload

When running a development server we want to reload on each new build. This plugin will start a livereload server and inject the client script into the specified target file (you can use a regex to target multiple files).

if (env === 'development') {
  tree = new LiveReload(tree, {
    target: 'index.html',
  });
}

Merging

The last step in the build process is to merge all of the current trees into a single tree which is then exported and written to file.

let tree = merge([html, css, images, public]);

module.exports = tree;

Debugging

Broccoli has two libraries to aid debugging: broccoli-stew and broccoli-debug.

TL;DR is you can write intermediate trees to debug folders and inspect them, for more refer to these articles:

Commands

# package.json
"scripts": {
    "build": "rm -rf dist && broccoli build dist",
    "serve": "broccoli serve",
    "build-prod": "rm -rf dist && BROCCOLI_ENV=production broccoli build dist"
  },

You can run the above commands with npm or yarn, e.g.

yarn run server

This will watch the source files and continuously serve the build output on localhost.

The build commands take the build destination path as an argument, in this case the imaginatively named "dist" folder.

Wrap up

Broccoli is uncomplicated to work with and easy to extend as the plugin api is very straightforward. If you are an ember-cli user and need to build a static site consider broccoli!

PS: This is another really good overview of broccoli:

Eat your greens! A beginners guide to Broccoli.js