Gulpifying Jekyll

It’s important to use the right tools for the job. Investing in good infrastructure and setup reduces future fiddling and makes actual work happen faster.

This post is a walkthrough for setting up a Gulp-driven Jekyll site. Jekyll is a static site generator, which turns organized posts and pages written in Markdown into a bunch of static HTML files. It’s makes a site extremely fast for visitors because there’s no server-side scripting, database, etc. Gulp is an efficient JavaScript task runner.

I use Jekyll to power Beer Review. At some point I fired up generator-jekyllrb, a Yeoman generator, to compile Sass, minify code, and optimize images within a Grunt-based workflow. It served me well, but builds run slowly, and the setup has tasks that I don’t need. Plus, I’ve grown to prefer the code-over-configuration approach of Gulp.

Set Up

Let’s get ready. Here’s what you’ll need:

  • Ruby (already installed)
  • Jekyll (2.5.3 as of this writing): gem install jekyll
  • Libsass: brew install libsass
  • Node and NPM: brew install node
  • Gulp: npm install -g gulp

(Commands assume you’re on a Mac and have Homebrew installed.)

First, we need a Jekyll project to work on. Use an existing one or make a new one by running jekyll new [project_directory], replacing [project_directory] with your desired directory name.

Now, we need a starter gulpfile.js and package.json. I really enjoy the generator-gulp-webapp: it’s maintained by the Yeoman team, and it always has the latest and greatest stuff. We’ll borrow their files.

Here’s how retrieve this generator’s files (alternatively, download them here):

  1. Install Yeoman: npm install -g yo
  2. Install the generator: npm install -g generator-gulp-webapp
  3. Make a new temporary directory and cd into it
  4. Use Yeoman to generate a new project: yo gulp-webapp
  5. Follow the prompts
  6. Stop Yeoman from installing dependencies

We’re ready to set up the Jekyll project directory. Move all Jekyll-assocated files and directories into a new directory named app (to follow the Yeoman standard). A couple exceptions: keep the configuration file (_config.yml), any READMEs, Gemfile, etc. in the project root. It should look something like this:

app
    _includes
    _layouts
    _posts
    _sass
    css
    feed.xml
    index.html
_config.yml

Now, move gulpfile.js and package.json from the temporary gulp-webapp directory into your Jekyll project directory. You can optionally move the dotfiles (.jshintrc, .editorconfig, etc.) if you want their functionality. It directory structure should now look like this:

app
    _includes
    _layouts
    _posts
    _sass
    css
    feed.xml
    index.html
.editorconfig
.jshintrc
_config.yml
gulpfile.js
package.json

Once everything’s in place, run npm install in the Jekyll directory to install the project’s dependencies.

Styles

First up is the project’s stylesheet (assuming Sass):

  1. Go into your root Sass file (app/css/main.scss in a default Jekyll install) and remove the front matter (everthing between and including the ---s). This is necessary for running Sass through Jekyll, but it chokes up regular Sass.
  2. Ensure the styles task in the gulpfile.js points to the right file:

    -  return gulp.src('app/styles/main.scss')
    +  return gulp.src('app/css/main.scss')
    
  3. Add the app/sass_ directory to Sass's includes:

    -       includePaths: ['.'],
    +       includePaths: ['.', 'app/_sass'],
    

Building Jekyll

Jekyll needs to know about the new directory structure. In keeping with Yeoman standards, the site’s code lives in app and the built and bundled code lives in dest. Jekyll doesn’t need worry about images or styles, so let’s set the appropriate excludes. Add these to your _config.yml file:

source: "app"
destination: "dist"
exclude: ["img", "css", "_sass", "js"]
keep_files: ["img", "css", "js"]

We’ll need a way to fire up Jekyll through Gulp. gulp-shell works perfectly for this. Install it: npm install gulp-shell --save-dev. Then, add a jekyll task to the gulpfile.js:

gulp.task('jekyll', function () {
  return gulp.src('_config.yml')
    .pipe($.shell([
      'jekyll build --config <%= file.path %>'
    ]))
    .pipe(reload({stream: true}));
});

Wire up the new task in your gulpfile.js:

- gulp.task('html', ['styles'], function () {
+ gulp.task('html', ['styles', 'jekyll'], function () {
- gulp.task('html', ['styles'], function () {
+ gulp.task('serve', ['styles', 'jekyll'], function () {

You’ll also want to change your gulp watches:

    gulp.watch([
-     'app/*.html',
-     'app/scripts/**/*.js',
+     'app/js/**/*.js',
-     'app/images/**/*',
+     'app/img/**/*',
-     '.tmp/fonts/**/*'
    ]).on('change', reload);

+   gulp.watch('app/**/*.{md,markdown,html}', ['jekyll']);

This will ensure BrowserSync reloads the server when your Jekyll files change.

Other Changes

Make sure your feed.xml isn’t overridden in the extras task:

  return gulp.src([
    'app/*.*',
+   '!app/feed.xml',
    '!app/*.html'
  ], {

I also dropped the fonts task and any references to Bootstrap, which comes from the generator-gulp-webapp by default.

A note on Base URL

Beer Review operates from a base url (/beer-review/). Unfortunately, because of the new setup, Jekyll’s baseurl configuration can’t be relied upon. I found a workaround that involves string replacing. It isn’t the prettiest, but it works.

npm install gulp-replace --save-dev

Add this to the html task:

+   var baseurl = 'beer-review'
+   var htmlPattern = /(href|src)(=["|']?\/)([^\/])/gi;
+   var htmlReplacement = '$1$2' + baseurl + '/$3';
+   var cssPattern = /(url\(['|"]?\/)([^\/])/gi;
+   var cssReplacement = '$1' + baseurl + '/$2';

      .pipe($.if('*.html', $.minifyHtml({conditionals: true, loose: true})))
+     .pipe($.if('*.html', $.replace(htmlPattern, htmlReplacement)))
+     .pipe($.if('*.css', $.replace(cssPattern, cssReplacement)))
      .pipe(gulp.dest('dist'));

Replace baseurl with your desired path.

That’s that! See the complete gulpfile.js here. Happy building!