Integrating ESBuild and Tailwind in Middleman

This document explains how to configure ESBuild and Tailwind CSS in a Middleman project. Assets are compiled into source/assets/build/ and linked from the layout.

Prerequisites

  • Ruby and Middleman installed
  • Node.js 20+ and npm installed

Target Folder Structure

coumets-dev/
├── source/
│   ├── assets/
│   │   ├── src/
│   │   │   ├── application.js
│   │   │   └── application.css
│   │   └── build/          ← ESBuild output
│   ├── index.html.erb
│   └── layouts/
│       └── layout.erb
├── config.rb
├── esbuild.config.mjs
└── package.json

Install Dependencies

1
2
npm init -y
npm install --save-dev esbuild tailwindcss @tailwindcss/postcss postcss @tailwindcss/typography concurrently mermaid

ESBuild Configuration

esbuild.config.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import * as esbuild from 'esbuild'
import { readFile, writeFile, mkdir } from 'fs/promises'
import postcss from 'postcss'
import tailwindcss from '@tailwindcss/postcss'

const isWatch = process.argv.includes('--watch')
const isProd = process.env.NODE_ENV === 'production'

async function buildCss() {
  const input = await readFile('source/assets/src/application.css', 'utf8')
  const result = await postcss([tailwindcss()]).process(input, {
    from: 'source/assets/src/application.css',
    to: 'source/assets/build/bundle.css',
  })
  await mkdir('source/assets/build', { recursive: true })
  await writeFile('source/assets/build/bundle.css', result.css)
}

async function buildJs() {
  return esbuild.build({
    entryPoints: ['source/assets/src/application.js'],
    bundle: true,
    outfile: 'source/assets/build/bundle.js',
    minify: isProd,
    define: {
      'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'),
    },
  })
}

async function buildAll() {
  await buildCss()
  await buildJs()
}

if (isWatch) {
  await buildAll()
  // Optional: chokidar watches source/assets/src, ERB, and Markdown for Tailwind class changes
} else {
  await buildAll()
}

Set NODE_ENV=production for CI and production builds so JavaScript is minified.

Tailwind Entry CSS

source/assets/src/application.css

1
2
@import "tailwindcss";
@plugin "@tailwindcss/typography";

package.json Scripts

1
2
3
4
5
6
7
{
  "scripts": {
    "build:assets": "node esbuild.config.mjs",
    "watch:assets": "node esbuild.config.mjs --watch",
    "dev": "concurrently \"npm run watch:assets\" \"bundle exec middleman server\""
  }
}

Middleman Configuration

config.rb

Do not set css_dir or js_dir to assets/build Middleman Sass will try to process Tailwind output and fail. Link pre-built bundles directly from the layout instead.

1
2
3
4
5
6
7
8
9
10
11
set :markdown_engine, :kramdown
set :markdown,
    input: 'GFM',
    syntax_highlighter: 'rouge'

set :images_dir, 'assets/img'
set :trailing_slash, true

ignore %r{^assets/src/}
ignore %r{^assets/build/.*\.map$}
ignore 'partials/*'

Add gem 'kramdown-parser-gfm' to the Gemfile for GitHub-flavoured Markdown tables and fenced code blocks.

Update Layout

source/layouts/layout.erb

1
2
<link rel="stylesheet" href="/assets/build/bundle.css">
<script src="/assets/build/bundle.js" defer></script>

Compile Assets

1
2
npm run build:assets
bundle exec middleman build

For development with live reload:

1
npm run dev

Result

Your Middleman project now uses ESBuild for fast JavaScript bundling and Tailwind CSS v4 for utility-first styling, with typography support for Markdown content.