This tutorial assumes that you are familiar with Jekyll, Nodejs and the Yarn package manager for npm. It also assumes that you’re familiar with diff syntax. With those in mind lets get Jekyll up and running with some of the latest and greatest frontend goodness.

The completed sourcecode for this tutorial is available in the footnotes.

jekyll new allstar
cd allstar
bundle config set path ".bundle"
bundle

Webpack

yarn add webpack webpack-cli --dev

Add the basic webpack config file webpack.config.js in the app root folder and populate it with the basic config (from the webpack homepage):-

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
};

Now you can run yarn run webpack to compile packs! Don’t do this yet though.

A quick break down of the contents of this file will inform us what to do next:-

Entrypoint

Line 4 - entry: './src/index.js', is our entry point. So we need to define the file at src/index.js which will be the “root” js file for our “bundle”. So on the command line:-

mkdir src && touch src/index.js

Bundle

Lines 5-8 - output: { ... } defines where webpack will compile our bundle to. In this case it will be in dist/bundle.js

If you now run yarn run webpack, you will see that it generated the folder dist and populated it with the file bundle.js which contains the basic webpack functionality.

Jekyll Integration

HTML & JS

Firstly, the HTML pages that Jekyll renders need to be able to consume our webpacked JS bundle, so we need to create a layout that we can use in our frontmatter declarations:-

mkdir _layouts && cp .bundle/ruby/2.7.0/gems/jekyll-4.0.1/lib/blank_template/_layouts/default.html _layouts/

NOTE: My ruby version is 2.7.0 and jekyll version is 4.0.1, yours may differ so substitute accordingly.

Now edit the file _layouts/default.html as per the diff below.

--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -4,7 +4,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <meta charset="utf-8">
     <title>{{ page.title }} - {{ site.title }}</title>
-    <link rel="stylesheet" href="{{ "/assets/css/main.css" | relative_url }}">
+    <script type="text/javascript" src="{{ "/dist/bundle.js" | relative_url }}"></script>
   </head>
   <body>
     {{ content}}

Notice that we removed the CSS link. This is because we’ll be bundling it all in with webpack later on.

CLI

Webpack is a cli compiler tool for Javascript (and CSS), as Jekyll is for HTML and here their paths meet. Since we’d prefer not to have to run two seperate commands every time we recompile assets or HTML, can we find a better way by combining them?

This guy has a nice technique of piping webpack to Jekyll, which I’ve modified to enable better error logging to the console (iTerm2 mac) by wrapping the script and piping webpack’s output through tee so that failed webpack compilation warnings don’t get swallowed:-

Update package.json accordingly

--- a/package.json
+++ b/package.json
@@ -1,4 +1,9 @@
 {
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start": "bash -c \"./node_modules/.bin/webpack --watch --progress --colors | tee -i >(bundle exec jekyll serve --livereload --incremental)\"",
+    "build": "./node_modules/.bin/webpack --progress --colors --config webpack.config.js && bundle exec jekyll build"
+  },
   "devDependencies": {
     "webpack": "^4.43.0",
     "webpack-cli": "^3.3.11"

Now run yarn run start to get everything up and running.

JS

ES6

Webpacks documentation specifies the babel loader as a way to “transpile” ES6 Javascript in your JS files.

First, add the required dependencies.

yarn add babel-loader @babel/core @babel/preset-env --dev

Now add the babel config to webpack.config.js as outlined in the diff below.

--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,5 +5,19 @@ module.exports = {
   output: {
     path: path.resolve(__dirname, 'dist'),
     filename: 'bundle.js'
+  },
+  module: {
+    rules: [
+      {
+        test: /\.m?js$/,
+        exclude: /(node_modules|bower_components)/,
+        use: {
+          loader: 'babel-loader',
+          options: {
+            presets: ['@babel/preset-env']
+          }
+        }
+      }
+    ]
   }
 };

Stimulus

yarn add stimulus && mkdir src/controllers

Modify the entrypoint file such that:-

--- a/src/index.js
+++ b/src/index.js
@@ -0,0 +1,9 @@
+const logMessage = "ES6 & Stimulus with Jekyll on Webpack";
+console.log(logMessage)
+
+import { Application } from "stimulus"
+import { definitionsFromContext } from "stimulus/webpack-helpers"
+
+const application = Application.start()
+const context = require.context("./controllers", true, /\.js$/)
+application.load(definitionsFromContext(context))

Modify index.markdown such that:-

--- a/index.markdown
+++ b/index.markdown
@@ -4,3 +4,7 @@

 layout: home
 ---
+<div data-controller="hello">
+  <input data-target="hello.name" type="text">
+  <button data-action="click->hello#greet">Greet</button>
+</div>

And create the file src/controllers/hello_controller.js with the content:-

new file mode 100644
index 0000000..f067db7
--- /dev/null
+++ b/src/controllers/hello_controller.js
@@ -0,0 +1,11 @@
+import { Controller } from "stimulus"
+
+export default class extends Controller {
+  greet() {
+    console.log(`Hello, ${this.name}!`)
+  }
+
+  get name() {
+    return this.targets.find("name").value
+  }
+}

Now run yarn run start, open the page and you should see the Stimulus hello world greeter form in your Jekyll page.

yarn add turbolinks

And src/index.js:-

--- a/src/index.js
+++ b/src/index.js
@@ -7,3 +7,6 @@ import { definitionsFromContext } from "stimulus/webpack-helpers"
 const application = Application.start()
 const context = require.context("./controllers", true, /\.js$/)
 application.load(definitionsFromContext(context))
+
+import Turbolinks from "turbolinks"
+Turbolinks.start();

CSS

yarn add style-loader css-loader --dev

These loaders will enable webpack to parse raw CSS into your target bundle when you import a CSS file into the JS entrypoint file ./src/index.js.

Now modify your webpack.config.js such that:-

--- a/webpack.config.js
+++ b/webpack.config.js
@@ -17,6 +17,13 @@ module.exports = {
             presets: ['@babel/preset-env']
           }
         }
+      },
+      {
+        test: /\.css$/,
+        use: [
+          'style-loader',
+          'css-loader'
+        ]
       }
     ]
   }

Now we can create the first CSS file for inclusion, this file will also act as the CSS “entrypoint” file for any additional CSS or SCSS/SASS files we want to bring in via @import, which we will do shortly.

touch src/main.css and add the following content:-

body {
  color: white;
  background-color: black;
}

Now modify src/index.js such that:-

--- a/src/index.js
+++ b/src/index.js
@@ -10,3 +10,5 @@ application.load(definitionsFromContext(context))

 import Turbolinks from "turbolinks"
 Turbolinks.start();
+
+import './main.css';

SASS / SCSS

Next we want to supercharge our CSS syntax with some awesomeness, so lets get the sass-loader for Webpack running and start importing some Sass files.

yarn add sass sass-loader --dev

Now modify as per the diff (note file name in the diff):-

--- a/webpack.config.js
+++ b/webpack.config.js
@@ -22,7 +22,12 @@ module.exports = {
         test: /\.css$/,
         use: [
           'style-loader',
-          'css-loader'
+          'css-loader',
+          {
+            loader: 'sass-loader',
+            ident: 'sass',
+            options: { sourceMap: true }
+          }
         ]
       }
     ]

Now touch src/sass/main.scss and add the following to the file:-

body {
  [data-controller="hello"] {
    border: solid 1px green;
  }
}

Now we can import this into our main CSS entrypoint file:-

--- a/src/main.css
+++ b/src/main.css
@@ -1,3 +1,5 @@
+@import './sass/main.scss';
+
 body {
   color: white;
   background: black;

When you run yarn run build or re-run yarn run start you should now see a green border around the Greeter form.

Tailwind CSS

There are two main ways you can use Tailwind. Either directly importing the library or its parts and adding the exposed classes in your HTML markup. Or by @applying modules in your actual CSS. We’ll tackle both cases seperately in terms of our bundle, since importing the whole of Tailwind is likely to greatly increase our bundle size.

yarn add tailwindcss --dev

Notice if you add the line @import 'tailwindcss' into your src/main.css file, webpack will start spitting warnings like:-

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  bundle.js (1.89 MiB)

So for now, we’re not going to import Tailwind, instead we’re going to focus on enabling the use of @apply, which will selectively apply the parts of Tailwind we want, via plugins via PostCSS via Webpack.

Postcss

yarn add postcss postcss-loader --dev

First we configure the postcss-loader for Webpack:-

--- a/webpack.config.js
+++ b/webpack.config.js
@@ -23,6 +23,17 @@ module.exports = {
         use: [
           'style-loader',
           'css-loader',
+          {
+            loader: 'postcss-loader',
+            options: {
+              ident: 'postcss',
+              sourceMap: true,
+              plugins: [
+                require('tailwindcss'),
+                require('autoprefixer'),
+              ],
+            },
+          },
           {
             loader: 'sass-loader',
             ident: 'sass',

Then we use it in the SASS to apply some Tailwind CSS:-

--- a/src/sass/main.scss
+++ b/src/sass/main.scss
@@ -1,5 +1,8 @@
 body {
   [data-controller="hello"] {
     border: solid 1px green;
+    @apply bg-gray-500;
   }
+
+  @apply bg-black;
 }
--- a/src/main.css
+++ b/src/main.css
@@ -2,5 +2,4 @@

 body {
   color: white;
-  background: black;
 }

Purgecss

This item in the pipeline needs to analyse not only our CSS code but also the HTML markup that Jekyll has generated after compilation, in order to optimise the CSS output. Therefore we can’t do this step in Webpack as it runs before the Jekyll compilation. So we’re going to use MinCssExtractPlugin for Webpack and the ‘jekyll-purgecss’ plugin for Jekyll in order to achieve this.

First add the css extract plugin:- yarn add mini-css-extract-plugin --dev

Mow modify the webpack build:-

--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,4 +1,5 @@
 const path = require('path');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');

 module.exports = {
   entry: './src/index.js',
@@ -6,6 +7,7 @@ module.exports = {
     path: path.resolve(__dirname, 'dist'),
     filename: 'bundle.js'
   },
+  plugins: [new MiniCssExtractPlugin()],
   module: {
     rules: [
       {
@@ -22,6 +24,7 @@ module.exports = {
         test: /\.css$/,
         use: [
           'style-loader',
+          MiniCssExtractPlugin.loader,
           'css-loader',
           {
             loader: 'postcss-loader',

Now add a reference to the seperated CSS dist file into the page layout:-

--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -5,6 +5,7 @@
     <meta charset="utf-8">
     <title>{{ page.title }} - {{ site.title }}</title>
     <script type="text/javascript" src="{{ "/dist/bundle.js" | relative_url }}"></script>
+    <link rel="stylesheet" href="{{ "/dist/main.css" | relative_url }}">
   </head>
   <body>
     {{ content}}

Add PurgeCSS yarn add purgecss so that the purgecss binary is available at ./node_modules/.bin/purgecss

Add the jekyll-purgecss gem in the Gemfile and _config.yml:-

--- a/Gemfile
+++ b/Gemfile
@@ -16,6 +16,7 @@ gem "minima", "~> 2.5"
 # If you have any plugins, put them here!
 group :jekyll_plugins do
   gem "jekyll-feed", "~> 0.12"
+  gem "jekyll-purgecss"
 end

 # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
--- a/_config.yml
+++ b/_config.yml
@@ -33,6 +33,10 @@ github_username:  jekyll
 theme: minima
 plugins:
   - jekyll-feed
+  - jekyll-purgecss
+
+## Purged CSS output folder
+css_dir: dist

 # Exclude from processing.
 # The following items will not be processed, by default.

Note the css_dir line in _config.yml. This is essential to ensure purgecss targets the right output location.

Now add a purgecss.config.js file at the root of the project with the contents:-

// purgecss.config.js

module.exports = {
  // These are the files that Purgecss will search through
  content: ["./_site/**/*.html"],

  // These are the stylesheets that will be subjected to the purge
  css: ["./_site/dist/main.css"],
  defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
};

You can now add tailwind into your source css or sass with an @import:-

--- a/src/main.css
+++ b/src/main.css
@@ -1,3 +1,4 @@
+@import 'tailwindcss';
 @import './sass/main.scss';

 body {

And it won’t bulk up the final CSS file in _site/dist/main.css anywhere near the 1.7~mb that the raw tailwindcss would have done, once you build the site.

You can run JEKYLL_ENV=production yarn run build or the equivalent start script to generate your optimized site for deployment. Just bear in mind that you need to set the JEKYLL_ENV to production in order for purgecss to do it’s thing.

Now add dist to .gitignore and then git rm -f dist/bundle.js as you don’t need to keep the root dist files, as they’re just intermediate and will also become very bulky as they’re unprocessed.

To see the source code for this, its here