4 minute read

I know. What an unholy union. Why would anyone do this? Why would anyone want that?

Well, first for science. Obviously.

Second, believe it or not, there are actual good reasons for combining Esbuild with Jekyll. I like Jekyll. It’s mature, has many plugins, and a vibrant ecosystem. Also, it’s built on Ruby, and Ruby is fantastic. Most importantly, it’s simple.

But.

Sometimes, you want that extra bit of visual oomph. Sometimes, you want to do weird or cool things, and a bundler might be necessary under those circumstances. Esbuild is modern and efficient. If you’re working with Ruby on Rails, you might already be used to it. Coincidentally, it’s also simple. Relatively speaking.

If you look at it like that, maybe Jekyll and Esbuild are meant to work together after all?

Why not Hugo? Or Bridgetown? Or any other static site generator that’s not Jekyll? Look, I don’t know what else to say. For science. I like Jekyll. It works for me 🙃

What we’ll do

The idea is this. We want to use Esbuild for bundling JS and CSS and let Jekyll take care of the rest. We’ll also add some plugins to Esbuild (autoprefixer, build-sass-plugin, postcss). First, so we can keep using the SCSS that Jekyll already provides, second for demonstration purposes.

We’ll also make sure we end up with a pleasant developer experience. Jekyll and Esbuild might not want to play nice, but we’ll make them 😈

Getting set up

Heads up! This guide assumes you have Ruby, Bundler, and Node set up on your machine. Also, Jekyll should already be installed. If that’s not the case, take care of that before you continue.

We’ll forego any plugins and start with an empty Jekyll scaffold to keep things simple.

jekyll new jekyll-esbuild --blank && cd jekyll-esbuild

Let’s create an empty Javascript file for demonstration purposes. Let’s also install Esbuild and the plugins we are going to use.

mkdir assets/javascript && touch assets/javascript/main.js
npm i -D --save-exact esbuild
npm i -D autoprefixer esbuild-sass-plugin postcss

When we run jekyll serve --watch we should see something on localhost:4000.

A Jekyll site

Bundling Assets with Esbuild

To use Esbuild with our CSS plugins, using the command line won’t do. We’ll have to create a small build script.

// scripts/build.mjs

import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from "postcss";
import autoprefixer from "autoprefixer";

await esbuild.build({
  entryPoints: ["assets/css/main.scss", "assets/javascript/main.js"],
  outdir: "_site/assets",
  bundle: true,
  plugins: [
    sassPlugin({
      async transform(source) {
        const { css } = await postcss([autoprefixer]).process(source, {
          from: undefined,
        });
        return css;
      },
      loadPaths: ["_sass"],
    }),
  ],
});

Once we update our package.json, we can build our assets.

{
  "devDependencies": {
    ...
  },
  "scripts": {
    "build": "node scripts/build.mjs",
  }
}

If we were to run npm run build now, we’d get an error. Our assets/main.scss still contains YML markup that we need to remove.

--- <<< DELETE ME
--- <<<

@import "base";

After that, we should be able to build assets without any issues.

Configuring Jekyll

Now, if you keep a close eye on the _site folder and make some changes to any watched files, you’ll notice that your built files will be changed. Makes sense because, as things stand, Jekyll still feels responsible for assets, thus overwriting or deleting the files created by Esbuild.

Let’s fix that. The simplest way to tell Jekyll to not worry about CSS and JS assets is to exclude the respective folders by updating _config.yml.

exclude:
  - _sass # Let build handle CSS
  - assets/css # Let build handle CSS
  - assets/javascript # Let esuild handle JS
  - scripts
  - package.json
  - package-lock.json
keep_files:
  - assets/css # Let build handle CSS
  - assets/javascript # Let esuild handle JS

Notice that we also told Jekyll to not delete CSS and JS assets when rebuilding. We also excluded some additional files that our Jekyll site doesn’t need. See configuration options if you need more details.

This won’t do if you’re using a theme. By excluding asset folders, theme styles won’t be processed appropriately. There are ways to fix that, but it’s a bit much for this blog post.

Let’s bundle and serve again to make sure everything is still working.

npm run build
jekyll serve --watch

Changing anything (e.g. CSS) will no longer overwrite files created by Esbuild. Success! Unfortunately, our changes won’t be reflected on your site. Let’s change that by adding watch mode to Esbuild.

Improving Developer Experience

Until the beginning of 2023, adding watch mode was a simple matter of adding watch: true to the arguments of Esbuild. Now, it’s a bit different. I opted to change the script behavior based on input arguments, but you can also create a separate script.

The updated scripts/build.mjs looks something like this:

const args = process.argv.slice(2);
const watch = args.includes("--watch");

const context = await esbuild.context({
  // ...
});

if (watch) {
  context.watch();
  console.log("Watching!");
} else {
  context.rebuild();
  context.dispose();
  console.log("Build done!");
}

After updating package.json once again, we can start watching our file changes by running npm run watch.

{
  "devDependencies": {
    ...
  },
  "scripts": {
    "watch": "node scripts/build.mjs --watch"
  }
}

We’re already using Ruby anyway, so we might as well use Foreman to run everything we need with a simple command. Note that I also added browser-sync for live reloading to the Procfile.

jekyll: jekyll serve --watch
build: npm run watch
browser:  browser-sync start --proxy localhost:4000 --files "**/*"

Run foreman start and your site should reload whenever you change your CSS, JS, or content. Mission accomplished!

Updated: