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.