Web · · 10 min read

Easy Asset Pipeline For Jekyll Using Vite

Or how I made peace with JS buildtools

Have you spent countless hours fine-tuning webpack to do just the right thing, except it didn’t? Have you gone down the rabbit holes of configuration documentation to understand why things happen the way they do? Have you tried to wire JS toolchain within Ruby world only to blow up your build spectacularly?

I sadly have, and, it seems, I have made every proverbial mistake a newbie and a semi-pro might make in that area. As a result, I thought I’d share my experiences from a recent foray into Vite from Jekyll’s perspective.

To be fair, Webpack version 4 and later has made giant strides in improving usability. The documentation has also improved tremendously and should, in my mind, be considered as best-in-class. Still, when I consider how long it took me to learn Webpack, and how long it took me to level up and wire up various Webpack projects I had worked on, I must say that Vite is like a breath of fresh air.[1] This coming from a reluctant and cantankerous JS user.

So let’s dive in:

Vite is a relatively new Javascript build framework in the ever-changing world of Javascrit frameworks. As its name indicates in French, Vite is fast. It supports Hot Module Replacement (HRM) and outputs highly optimized assets served via Rollup. It also requires minimal configuration. What’s not to like?

Thanks to the tireless work of Máximo Mussini, Ruby world has a first-class seat to the Vite ecosystem.

Back in the day, when I was setting up Jekyll with Webpack to use the PostCSS pipeline for the project backed by Tailwind, I spent a good weekend making the build tick. Ah, the joys of Webpack 3.

To get the same results in Vite? 1 hour. No kidding.

Again, I can’t claim to be the sharpest tool in the JS shed, but since Vite pre-bakes a lot of functionality and configuration for you, I think it will save you a lot of aggravation/PTSD when you greenfield a project.

Ok, enough of jibber-jabbering. To work!

In this, erm, short write-up, I will show you how to include the Vite asset pipeline in the Jekyll blog. We will:

Finished repo is available here.

Let’s get going!

A new blog

Let's generate a new blog. I'll keep the location of the blog in the BLOG_DIR variable.

jekyll new jekyll-vite-blog
cd jekyll-vite-blog
export BLOG_DIR=$PWD
bx jekyll s

Oh, and bx stands for bundle exec, as in alias bx='bundle exec'.

If you are using Ruby 3.0.0 or higher, you will need to add gem webrick to the list of gems used by our new jekyll-vite-blog. Otherwise, when you run bundle exec jekyll serve, you might get an error similar to:

Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
            Source: /Users/pablo/Dev/Blog/jekyll-vite-blog
       Destination: /Users/pablo/Dev/Blog/jekyll-vite-blog/_site
 Incremental build: disabled. Enable with --incremental
      Generating...
       Jekyll Feed: Generating feed for posts
                    done in 0.262 seconds.
 Auto-regeneration: enabled for '/Users/pablo/Dev/Blog/jekyll-vite-blog'
                    ------------------------------------------------
      Jekyll 4.2.0   Please append `--trace` to the `serve` command
                     for any additional information or backtrace.
                    ------------------------------------------------
/Users/pablo/.gem/ruby/3.0.2/gems/jekyll-4.2.0/lib/jekyll/commands/serve/servlet.rb:3:in `require': cannot load such file -- webrick (LoadError)
(...)

Let's fix our blog by running the following commands:

# in $BLOG_DIR
bundle add webrick
bx jekyll s

Result?

Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
            Source: /Users/pablo/Dev/Blog/jekyll-vite-blog
       Destination: /Users/pablo/Dev/Blog/jekyll-vite-blog/_site
 Incremental build: disabled. Enable with --incremental
      Generating...
       Jekyll Feed: Generating feed for posts
                    done in 0.253 seconds.
 Auto-regeneration: enabled for '/Users/pablo/Dev/Blog/jekyll-vite-blog'
    Server address: http://127.0.0.1:4000/
  Server running... press ctrl-c to stop.

Huzzah! Our Jekyll blog is alive!

Our blog includes a sample new post Hello World which is saved in _posts subdirectory.

If you look at the Gemfile, you will notice minima gem which provides us with a nice Jekyll theme. Before we monkey with this theme or CSS in general, let's set up Vite by following instructions from the Jekyll Vite documentation.

Add jekyll-vite gem

To support running vite in Jekyll, we will use jekyll-vite.

We follow the directions to add:

# in $BLOG_DIR
bundle add jekyll-vite

The bundle add added jekyll-vite to our Gemfile:

+gem "jekyll-vite", "~> 3.0"

Now let's run bx vite install and investigate its output:

# in $BLOG_DIR
bx vite install
Creating binstub
Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
Check that your vite.json configuration file is available in the load path:

	No such file or directory @ rb_sysopen - /Users/pablo/Dev/Blog/jekyll-vite-blog/config/vite.json

Creating configuration files
Installing sample files
Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
Installing js dependencies

added 34 packages, and audited 35 packages in 5s

6 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Adding files to .gitignore

Vite ⚡️ Ruby successfully installed! 🎉

Success!

 # Build settings
 theme: minima
 plugins:
+  - jekyll/vite
   - jekyll-feed

(...)
 # Exclude from processing.
@@ -43,6 +44,10 @@ plugins:
 # their entries' file path in the `include:` list.
 #
+ exclude:
+  - bin
+  - config
+  - vite.config.ts
+  - tmp
 #   - .sass-cache/
 #   - .jekyll-cache/
 #   - gemfiles/
?? Procfile.dev
?? Rakefile
?? _frontend/entrypoints/application.js
?? bin/vite
?? config/vite.json
?? package-lock.json
?? package.json
?? vite.config.ts

We can ignore foreman's Profile.dev and Rakefile for now.

Files of interest are:

In other words, jekyll-vite plugin configures Jekyll how and when to run Vite. It will use config/vite.json as its reference point for configuration as to which files should trigger the rebuild.

If we were to run jekyll s now, we'd get:

Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
  Dependency Error: Yikes! It looks like you don't have bin or one of its dependencies installed. In order to use Jekyll as currently configured, you'll need to install this gem. If you've run Jekyll with `bundle exec`, ensure that you have included the bin gem in your Gemfile as well. The full error message from Ruby is: 'cannot load such file -- bin' If you run into trouble, you can find helpful resources at https://jekyllrb.com/help/!
                    ------------------------------------------------
      Jekyll 4.2.0   Please append `--trace` to the `serve` command
                     for any additional information or backtrace.
                    ------------------------------------------------

This confusing error occurs because of a tiny bug in our _config.yml: We want to make sure that _config.yml excludes the files we want excluded.

Currently we have:

#exclude:
  - bin
  - config
  - vite.config.ts
  - tmp

Have you caught the error? We want to exclude block to be active, and we want it uncommented, like so:

-#exclude:
+exclude
  - bin
  - config
  - vite.config.ts
  - tmp

After modification of _config.yml we can see that re-running the Jekyll server we have first sign of Vite running:

Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
            Source: /Users/pablo/Dev/Blog/jekyll-vite-blog
       Destination: /Users/pablo/Dev/Blog/jekyll-vite-blog/_site
 Incremental build: disabled. Enable with --incremental
      Generating...
       Jekyll Feed: Generating feed for posts
                    done in 0.279 seconds.
 Auto-regeneration: enabled for '/Users/pablo/Dev/Blog/jekyll-vite-blog'
    Server address: http://127.0.0.1:4000/
  Server running... press ctrl-c to stop.

Now, when we load up the main page, we should see Vite ⚡️ Ruby in the dev tool console, right?

sad trombone

No, we should not.

That's because we actually have not actually told Jekyll blog where to get the JS even though it processed/bundled application.js with Vite. To configure that we need to look closer how the theme template is put together:

Soooooo, in this never-ending Turkish soap opera, we need to do two things:

Let's create _includes and download the files:

# $BLOG_DIR
mkdir _includes
cd _includes
# $BLOG_DIR/_includes
wget https://raw.githubusercontent.com/jekyll/minima/master/_includes/custom-head.html
wget https://raw.githubusercontent.com/jekyll/minima/master/_includes/head.html

In our downloaded custom-head let's add:

{% raw %}
{% comment %}
  Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons:

  1. Head over to https://realfavicongenerator.net/ to add your own favicons.
  2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet.
{% endcomment %}
+
+{% vite_client_tag %}
+{% vite_javascript_tag application %}
{% endraw %}

Now, rerun the server:

# in $BLOG_DIR/
Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
Configuration file: /Users/pablo/Dev/Blog/jekyll-vite-blog/_config.yml
            Source: /Users/pablo/Dev/Blog/jekyll-vite-blog
       Destination: /Users/pablo/Dev/Blog/jekyll-vite-blog/_site
 Incremental build: disabled. Enable with --incremental
      Generating...
       Jekyll Feed: Generating feed for posts
I, [2021-09-11T17:27:37.131081 #43108]  INFO -- : Building with Vite ⚡️
I, [2021-09-11T17:27:37.338824 #43108]  INFO -- vite: vite v2.5.6 building for development...
I, [2021-09-11T17:27:37.370320 #43108]  INFO -- vite: transforming...
I, [2021-09-11T17:27:37.374091 #43108]  INFO -- vite: ✓ 1 modules transformed.
I, [2021-09-11T17:27:37.464859 #43108]  INFO -- vite: rendering chunks...
I, [2021-09-11T17:27:37.468836 #43108]  INFO -- vite: ../.jekyll-cache/vite-dev/manifest-assets.json             0.00 KiB
I, [2021-09-11T17:27:37.468866 #43108]  INFO -- vite: ../.jekyll-cache/vite-dev/manifest.json                    0.14 KiB
I, [2021-09-11T17:27:37.470749 #43108]  INFO -- vite: ../.jekyll-cache/vite-dev/assets/application.a54155ad.js   0.03 KiB / brotli: 0.04 KiB
I, [2021-09-11T17:27:37.478778 #43108]  INFO -- : Build with Vite complete: /Users/pablo/Dev/Blog/jekyll-vite-blog/.jekyll-cache/vite-dev
D, [2021-09-11T17:27:37.492287 #43108] DEBUG -- : Skipping vite build. Watched files have not changed since the last build at 2021-09-11 17:27:37
D, [2021-09-11T17:27:37.499576 #43108] DEBUG -- : Skipping vite build. Watched files have not changed since the last build at 2021-09-11 17:27:37
D, [2021-09-11T17:27:37.506038 #43108] DEBUG -- : Skipping vite build. Watched files have not changed since the last build at 2021-09-11 17:27:37
                    done in 0.651 seconds.
 Auto-regeneration: enabled for '/Users/pablo/Dev/Blog/jekyll-vite-blog'
    Server address: http://127.0.0.1:4000/
  Server running... press ctrl-c to stop. 

We see Vite building things for us. Opening the browser's dev tools, when loading the page we now see hat page now includes our entry point and calls it.

But what happened to our styling?

Ooops.

We have overriden where the assets are actually being served from. 'Cept we gon nutttin in terms of style.

Great, let's fix it.

Fixing the styles

Minima styles are in _sass/minima. We have this fancy tool called Vite, how about using it to compile our SCSS files?

To do that, we will download the CSS asset files.

# in $BLOG_DIR
mkdir -p _frontend/minima/skins
cd _frontend/minima
wget https://raw.githubusercontent.com/jekyll/minima/master/_sass/minima/_base.scss
wget https://raw.githubusercontent.com/jekyll/minima/master/_sass/minima/_layout.scss
wget https://raw.githubusercontent.com/jekyll/minima/master/_sass/minima/custom-styles.scss
wget https://raw.githubusercontent.com/jekyll/minima/master/_sass/minima/custom-variables.scss
wget https://raw.githubusercontent.com/jekyll/minima/master/_sass/minima/initialize.scss
cd skins
wget https://raw.githubusercontent.com/jekyll/minima/master/_sass/minima/skins/classic.scss
cd ..

To make things go, we need to remove Jekyll's SASS tag from the _includes/head.html:

{% raw %}
<meta name="viewport" content="width=device-width, initial-scale=1">
   {%- seo -%}
-  <link rel="stylesheet" href="{{ "/assets/css/style.css" | relative_url }}">
   {%- feed_meta -%}
   {%- if jekyll.environment == 'production' and site.google_analytics -%}
     {%- include google-analytics.html -%}
{% endraw %}

and point SASS to Vite pipe by modifying _includes/custom-head.html:

{% raw %}
{% comment %}
  Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons:

  1. Head over to https://realfavicongenerator.net/ to add your own favicons.
  2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet.
{% endcomment %}

{% vite_client_tag %}
+{% vite_stylesheet_tag styles.scss %}
{% vite_javascript_tag application %}
{% endraw %}

Now, let's create an SCSS entry point:

# in $BLOG_DIR
cat > _frontend/entrypoints/styles.scss<<EOF
@import "~/minima/skins/classic";
@import "~/minima/initialize";
EOF

Since we are using SCSS/SASS, we need to add it to the NPM packages, so that Vite can use it:

npm add -D sass

Finally, we fix the entry point in _frontend/minima/initialize.scss to point to the root of the directory:

$base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Segoe UI Symbol", "Segoe UI Emoji", "Apple Color Emoji", Roboto, Helvetica, Arial, sans-serif !default;
$code-font-family: "Menlo", "Inconsolata", "Consolas", "Roboto Mono", "Ubuntu Mono", "Liberation Mono", "Courier New", monospace;
$base-font-size:   16px !default;
$base-font-weight: 400 !default;
$small-font-size:  $base-font-size * 0.875 !default;
$base-line-height: 1.5 !default;

$spacing-unit:     30px !default;

$table-text-align: left !default;

// Width of the content area
$content-width:    800px !default;

$on-palm:          600px !default;
$on-laptop:        800px !default;

$on-medium:        $on-palm !default;
$on-large:         $on-laptop !default;

@mixin media-query($device) {
  @media screen and (max-width: $device) {
    @content;
  }
}

@mixin relative-font-size($ratio) {
  font-size: #{$ratio}rem;
}

@import
  "~/minima/custom-variables", // Hook to override predefined variables.
  "~/minima/base",             // Defines element resets.
  "~/minima/layout",           // Defines structure and style based on CSS selectors.
  "~/minima/custom-styles"     // Hook to override existing styles.
;

To complete the set up, we set up Foreman to start the our dev. Thanks to the latest fixes, Jekyll watch option handles reloading correctly with Vite. Now, you can use either Jekyll's watch or livereload. Let's define a Procfile.dev:

cat > Procfile.dev <<EOF
vite: bin/vite dev
jekyll: bundle exec jekyll s --watch
EOF  

foreman start -f Procfile.dev

Huzzah!

Summary

Vite works just fine with Jekyll. It requires a tiny bit of encouragement to function properly.


  1. Many thanks to FrontEndMaster’s Webpack class taught by Sean Larkin. If here is one course that helped me understand Webpack, this would be the one. ↩︎

Read next