Using Jekyll with Esbuild
Published: 2024-02-09 18:57:22

---
title: Using Jekyll with Esbuild
published: true
date: 2024-02-09 09:00:00 UTC
tags: 
canonical_url: https://hansschnedlitz.com/2024/02/09/using-jekyll-with-esbuild.html
---

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](https://github.com/postcss/autoprefixer), [build-sass-plugin](https://www.npmjs.com/package/esbuild-sass-plugin), [postcss](https://postcss.org/)). 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.

```bash
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.

```bash
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](http://localhost:4000).

![A Jekyll site](https://hansschnedlitz.com/assets/images/posts/2024-jekyll-esbuild/jekyll.webp)

## 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.

```js
// 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.

```json
{
  "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`.

```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](https://jekyllrb.com/docs/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.

```bash
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:

```js
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](https://github.com/ddollar/foreman) to run everything we need with a simple command. Note that I also added [browser-sync](https://browsersync.io/) 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!