Jekyll Minibundle plugin

A straightforward asset bundling plugin for Jekyll, utilizing external minification tool of your choice. It provides asset concatenation for bundling and asset fingerprinting with MD5 digest for cache busting.

There are no runtime dependencies, except for the minification tool used for bundling (fingerprinting has no dependencies).

The plugin requires Jekyll version 3.x. It is tested with Ruby MRI 2.x. Ruby 1.8 and 1.9 are not supported.

The plugin works with Jekyll's watch mode (auto-regeneration, Jekyll option --watch), but not with incremental feature enabled (Jekyll option --incremental).

Gem version Build status

Features

There are two features: asset fingerprinting with MD5 digest over the contents of the asset, and asset bundling combined with the first feature.

Asset bundling consists of concatenation and minification. The plugin implements concatenation and leaves choosing the minification tool up to you. UglifyJS2 is a good and fast minifier, for example. The plugin connects to the minifier with standard unix pipe, feeding asset file contents to it in desired order via standard input, and reads the result from standard output.

Why is this good? A fingerprint in asset's path is the recommended way to handle caching of static resources, because you can allow browsers and intermediate proxies to cache the asset for a very long time. Calculating MD5 digest over the contents of the asset is fast and the resulting digest is reasonably unique to be generated automatically.

Asset bundling is good for reducing the number of requests to the backend upon page load. The minification of stylesheets and JavaScript sources makes asset sizes smaller and thus faster to load over network.

Usage

The plugin ships as a RubyGem. To install:

$ gem install jekyll-minibundle

(You should use Bundler to manage the gems in your project.)

Then, instruct Jekyll to load the gem by adding this line to the configuration file of your Jekyll site project (_config.yml):

gems: ['jekyll/minibundle']

An alternative to using the gems configuration setting is to add _plugins/minibundle.rb file to your site project with this line:

require 'jekyll/minibundle'

You must allow Jekyll to use custom plugins. That is, do not enable Jekyll's safe setting.

Asset fingerprinting

If you just want to have an MD5 fingerprint in your asset's path, use ministamp Liquid tag. For example:

<link href="{{ site.baseurl }}{% ministamp _assets/site.css assets/site.css %}" rel="stylesheet" media="screen, projection">

When it's time to render the ministamp tag, the plugin copies the source file (_assets/site.css, the first tag argument) to the specified destination path (assets/site.css, the second tag argument) in Jekyll's output directory. The filename will contain a fingerprint.

Tag output, when site.baseurl is /:

<link href="/assets/site-390be921ee0eff063817bb5ef2954300.css" rel="stylesheet" media="screen, projection">

This feature is useful when combined with asset generation tools external to Jekyll. For example, you can configure Sass to take input files from _assets/styles/*.scss and to produce output to _tmp/site.css. Then, you use ministamp tag to copy the file with a fingerprint to Jekyll's output directory:

<link href="{{ site.baseurl }}{% ministamp _tmp/site.css assets/site.css %}" rel="stylesheet">

Asset bundling

This is a straightforward way to bundle assets with any minification tool that supports reading input from STDIN and writing the output to STDOUT. You write the configuration for input sources directly into the content file where you want the markup tag for the bundle file to appear. The markup tag contains the path to the bundle file, and the Jekyll's output directory will have the bundle file at that path. The path will contain an MD5 fingerprint.

Place minibundle Liquid block into your content file where you want the generated markup to appear. Write bundling configuration inside the block in YAML syntax. For example, to bundle a set of JavaScript sources:

{% minibundle js %}
source_dir: _assets/scripts
destination_path: assets/site
baseurl: {{ site.baseurl }}
assets:
  - dependency
  - app
attributes:
  id: my-scripts
  async:
{% endminibundle %}

Then, specify the command for launching your favorite minifier in _config.yml:

baseurl: /

minibundle:
  minifier_commands:
    js: node_modules/.bin/uglifyjs -

When it's time to render the minibundle block, the plugin launches the minifier and connects to it with a Unix pipe. The plugin feeds the contents of the asset files in source_dir directory as input to the minifier (STDIN). The feeding order is the order of the files in the assets key in the block configuration. The plugin expects the minifier to produce output (STDOUT) and writes it to the file at destination_path in Jekyll's output directory. The filename will contain a fingerprint.

Block output in the content file:

<script src="/assets/site-8e764372a0dbd296033cb2a416f064b5.js" type="text/javascript" id="my-scripts" async></script>

You can pass custom attributes, like id="my-scripts" and async above, to the generated markup with attributes map inside the minibundle block.

For bundling CSS assets, use css as the argument to the minibundle block:

{% minibundle css %}
source_dir: _assets/styles
destination_path: assets/site
baseurl: {{ site.baseurl }}
assets:
  - reset
  - common
attributes:
  media: screen
{% endminibundle %}

And then specify the minifier command in _config.yml:

minibundle:
  minifier_commands:
    css: _bin/remove_whitespace
    js: node_modules/.bin/uglifyjs -

Minifier command specification

You can specify minifier commands in three places:

  1. in _config.yml (as shown earlier):
   minibundle:
     minifier_commands:
       css: _bin/remove_whitespace
       js: node_modules/.bin/uglifyjs -
  1. as environment variables:
   export JEKYLL_MINIBUNDLE_CMD_CSS=_bin/remove_whitespace
   export JEKYLL_MINIBUNDLE_CMD_JS="node_modules/.bin/uglifyjs -"
  1. inside the minibundle block with minifier_cmd setting, allowing blocks to have different commands from each other:
   {% minibundle js %}
   source_dir: _assets/scripts
   destination_path: assets/site
   minifier_cmd: node_modules/.bin/uglifyjs -
   assets:
     - dependency
     - app
   attributes:
     id: my-scripts
   {% endminibundle %}

These ways of specification are listed in increasing order of specificity. Should multiple commands apply to a block, the most specific one wins. For example, the minifier_cmd setting inside minibundle js block overrides the setting in $JEKYLL_MINIBUNDLE_CMD_JS environment variable.

It's recommended that you exclude the files you use as asset sources from Jekyll itself. Otherwise, you end up with duplicate files in the output directory.

For example, in the following snippet we're using assets/src.css as asset source to ministamp tag:

<!-- BAD: unless assets dir is excluded, both src.css and dest.css will be copied to output directory -->
<link href="{{ site.baseurl }}{% ministamp assets/src.css assets/dest.css %}" rel="stylesheet" media="screen, projection">

By default, Jekyll includes this file to the output directory. As a result, there will be both src.css and dest-<md5>.css files in _site/assets/ directory, which you probably do not want.

In order to avoid this, exclude the asset source file from Jekyll. Because Jekyll excludes directories beginning with underscore character (_), consider using the following directory layout:

  • _assets/ for JavaScript and CSS assets handled by the plugin that are in version control
  • _tmp/ for temporary JavaScript and CSS assets handled by the plugin that are not in version control (for example, Sass output files)
  • assets/ for images and other assets handled by Jekyll directly

See Jekyll configuration for more about excluding files and directories.

Development mode

If you set $JEKYLL_MINIBUNDLE_MODE environment variable to development, then the plugin will copy asset files as is to Jekyll's output directory and omit fingerprinting. The destination_path setting in minibundle block sets the destination directory for bundled files. This is useful in development workflow, where you need the filenames and line numbers of the original asset sources.

$ JEKYLL_MINIBUNDLE_MODE=development jekyll serve --watch

Alternatively, you can enable development mode from _config.yml:

minibundle:
  mode: development

Should both be defined, the setting from the environment variable wins.

Capturing Liquid output

Use Liquid's capture block to store output rendered inside the block to a variable, as a string. Then you can process the string.

For example:

{% capture sitecss %}{% ministamp _assets/site.css assets/site.css %}{% endcapture %}
<link href="{{ site.cdnbaseurl }}{{ sitecss | remove_first: "assets/"}}" rel="stylesheet" media="screen, projection">

Output, provided site.cdnbaseurl is set to https://cdn.example.com/:

<link href="https://cdn.example.com/site-390be921ee0eff063817bb5ef2954300.css" rel="stylesheet" media="screen, projection">

Example site

See the sources of an example site.

Known caveats

The plugin does not work with Jekyll's incremental rebuild feature (Jekyll option --incremental).

License

MIT. See LICENSE.txt.