Getting started
Engine
htmx-ui-engine builds a site from Nunjucks pages: file-based routing, Tailwind, a dev server with hot reload and mock htmx endpoints, and static builds, on Bun or Node.
htmx-ui-engine is the build tool behind npm create htmx-ui and this website. It turns a
folder of HTML pages into a static site: each page is rendered with Nunjucks, then bundled (scripts and styles
hashed and minified, Tailwind compiled from every class the page uses). The same engine runs on two runtimes:
| Runtime | Dev server | Build | Tailwind |
|---|---|---|---|
| Bun | Bun.serve with HMR | Bun.build | bun-plugin-tailwind |
| Node | Vite with HMR | Vite | @tailwindcss/vite |
Both render pages with the same code, so a site builds the same way on either.
Commands#
{
"scripts": {
"dev": "htmx-ui dev",
"build": "htmx-ui build",
"preview": "htmx-ui preview"
}
}
| Command / option | Description |
|---|---|
htmx-ui dev | Dev server at http://localhost:3000 with hot reload: scripts and styles update in place; pages reload when a layout, partial, macro or data file changes. |
htmx-ui build | Builds every page into dist/ (same paths as pages/), with scripts, styles and images in dist/assets/, content-hashed and shared by every page that uses them. Copies public/ next to it. |
htmx-ui preview | Serves dist/ with the same clean URLs (/about for about.html). |
--port <n> | Port for dev and preview. $PORT works too. |
--root <dir> | Project root, if not the current directory. |
--bun, --node | Pick the runtime (also HTMX_UI_RUNTIME=bun|node). |
The runtime follows whatever started the command: bun run dev and bunx htmx-ui run on Bun;
npm run, pnpm, yarn and npx run on Node. Install the Tailwind
integration for the one you use:
bun add -d htmx-ui-engine tailwindcss bun-plugin-tailwind
npm install -D htmx-ui-engine tailwindcss vite @tailwindcss/vite
Project layout#
pages/index.html -> /
pages/about.html -> /about
pages/docs/index.html -> /docs
pages/docs/setup.html -> /docs/setup
layouts/ partials/ shared templates
data/*.json read from templates with json()
public/ served at / and copied to dist/ as-is
app.ts styles.css client entry and stylesheet
htmx-ui.config.ts optional
Pages, layouts and partials are covered in Templates & data. With
htmx-ui installed, its macros and icons resolve by name, after your own files:
{% from "components/icon/icon.html" import icon %}.
Link files in public/ with a root-absolute URL (href="/favicon.svg"); they keep their names.
Link everything else with asset(), so the bundler can hash it.
Configuration#
Every option is optional, and a project without htmx-ui.config.ts uses the defaults.
import { defineConfig } from "htmx-ui-engine";
export default defineConfig({
pages: "pages",
templates: ["."],
outDir: "dist",
publicDir: "public",
url: "https://example.com",
port: 3000,
});
| Option | Description |
|---|---|
pages | Routes directory. Default "pages". |
templates | Template roots, searched in order. Default ["."]; htmx-ui's src/ is added last (ui: false turns that off). |
outDir | Build output. Default "dist". |
publicDir | Copied as-is. Default "public"; false to disable. |
url | Public site URL, the origin template global. $SITE_URL overrides it. |
port | Dev and preview port. Default 3000; $PORT overrides it. |
globals, filters | Extra Nunjucks globals and filters. |
transform(html, page) | Post-process every rendered page, in dev and build. |
routes | Dev-only request handlers (see below). |
fetch(req) | Dev-only fallback for anything else; return null for a 404. |
build.minify, build.sourcemap, build.define | Production bundling options. |
build.done(ctx) | Runs after a build with { outDir, pages }, e.g. to write a sitemap. |
vite | Node only: extra Vite config, merged over the engine's. |
Mock htmx endpoints#
While you build the front end, routes answers hx-get and hx-post with HTML
fragments, so no backend is needed. Handlers take a web Request and return a Response, with
Bun.serve's route syntax, on either runtime:
import { defineConfig } from "htmx-ui-engine";
const html = (body: string) => new Response(body, { headers: { "Content-Type": "text/html" } });
export default defineConfig({
routes: {
"/api/users/:id": (req) => html(`<p>User ${req.params.id}</p>`),
"/api/search": async (req) => {
const q = new URL(req.url).searchParams.get("q") ?? "";
return html(`<li>Results for ${q}</li>`);
},
},
});
Use the plugins directly#
Already have a build script or a Vite config? Use the engine's plugin and skip the CLI:
import { htmxUi } from "htmx-ui-engine/bun";
import tailwind from "bun-plugin-tailwind";
await Bun.build({
entrypoints: ["./pages/index.html", "./pages/about.html"],
root: "./pages",
outdir: "./dist",
plugins: [await htmxUi(), tailwind], // htmx-ui first, so Tailwind sees rendered markup
});
import { htmxUi } from "htmx-ui-engine/vite";
import tailwindcss from "@tailwindcss/vite";
export default {
plugins: [htmxUi(), tailwindcss()],
};
The package root, htmx-ui-engine, exports the runtime-independent core for your own tooling:
defineConfig, loadConfig, render, renderPage,
findPages, routeFor and highlight.