npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

htmx-ui-engine

v0.3.0

Published

Build htmx sites from Nunjucks pages: file-based routing, Tailwind, dev server with HMR and static builds, on Bun or Node.

Readme

htmx-ui-engine

Build htmx sites from plain HTML pages: Nunjucks templates, file-based routing, Tailwind CSS, a dev server with hot reload and mock htmx endpoints, and static production builds. One engine, two runtimes: Bun (Bun.serve + Bun.build) and Node (Vite). Made for htmx-ui components; works without them.

bun create htmx-ui@latest my-site     # or: npm create, pnpm create, yarn create

Install into an existing project

# Bun
bun add -d htmx-ui-engine tailwindcss bun-plugin-tailwind

# Node (npm, pnpm or yarn)
npm install -D htmx-ui-engine tailwindcss vite @tailwindcss/vite
{ "scripts": { "dev": "htmx-ui dev", "build": "htmx-ui build", "preview": "htmx-ui preview" } }

The runtime follows whoever started the CLI: bun run dev and bunx htmx-ui use Bun; npm run, pnpm, yarn and npx use Node. Force one with --bun or --node (or HTMX_UI_RUNTIME=bun|node).

Project layout

pages/index.html          -> /
pages/about.html          -> /about
pages/docs/index.html     -> /docs
layouts/ partials/        shared templates: {% extends "layouts/base.html" %}, {% include "partials/nav.html" %}
data/*.json               readable from any template: {{ json("data/site.json").name }}
public/                   served at / and copied to dist/ as-is
htmx-ui.config.ts         optional

Pages are Nunjucks templates rendered before bundling, so scripts and styles they reference are bundled, hashed and minified, and Tailwind sees every class. The build keeps page paths (dist/docs/index.html) and puts scripts, styles and images in dist/assets/, shared by every page that uses them: a site whose layout loads one app.ts ships one JS and one CSS file, on either runtime. Template helpers:

| Helper | | | :--- | :--- | | asset("app.ts") | path to a project file, relative to the page (use it for <script src> / <link href> in layouts) | | assetVer("app.css") | the same, plus ?ver=<package.json version> (and a random suffix in dev), so a cached copy is never reused | | url | the page's route, e.g. /docs/button (mark the active nav link) | | json(path), svg(path, attrs), glob(pattern) | read JSON, inline an SVG, list files | | origin | the public site URL (url in the config, or $SITE_URL) | | dedent, highlight(lang) filters | strip indentation; syntax-highlight code at build time (Shiki) |

Add your own with globals and filters in the config \u2014 plain JavaScript, registered before every render, so they reach layouts and imported macros too. markup(html) wraps a string so a helper can return HTML.

With htmx-ui installed, its macros and icons resolve too: {% from "components/icon/icon.html" import icon %}.

Config

// htmx-ui.config.ts
import { defineConfig } from "htmx-ui-engine";

export default defineConfig({
  pages: "pages",        // routes directory
  roots: ["."],          // where templates/data/icons are found, in order; roots[0] is the
                         // project root. An entry may be { name, dir }: the name is the prefix
                         // templates reach that directory by, so it can move without the
                         // templates changing. htmx-ui's src/ is added last
  outDir: "dist",
  publicDir: "public",
  url: "https://example.com",
  port: 3000,            // $PORT overrides
  // Mock endpoints for hx-get / hx-post (Bun.serve route syntax, web Request -> Response),
  // served by the dev servers and by createSite() / the server adapters
  routes: {
    "/api/users/:id": (req) => new Response(`<p>User ${req.params.id}</p>`, { headers: { "Content-Type": "text/html" } }),
  },
  transform: (html, page) => html,             // post-process every rendered page
  build: { minify: true, sourcemap: "linked", done: async ({ outDir, pages }) => {} },
  vite: {},                                    // Node only: extra Vite config
  plugins: [],                                 // optional features, see Plugins below
});

Plugins

Features only some sites need are separate packages, listed under plugins. The official ones: htmx-ui-plugin-docs (Markdown for every docs page, llms.txt, sitemaps, heading anchors, navigation), htmx-ui-plugin-versions (archived docs versions and a switcher) and htmx-ui-plugin-search (a Ctrl/⌘K palette).

import { defineConfig } from "htmx-ui-engine";
import docs from "htmx-ui-plugin-docs";
import search from "htmx-ui-plugin-search";

export default defineConfig({ plugins: [docs(), search()] });

A plugin is an object with a name and any of: roots (templates, searched after yours and before htmx-ui's), globals, filters, transform, routes, fetch, build.done, commands (CLI commands such as htmx-ui versions:archive, listed by htmx-ui --help), configResolved(config) and an api for other plugins. Your own config always wins: its routes and globals override a plugin's, its fetch runs first, its transform and build.done last. Plugins work in dev, builds, on Bun and Node and behind every server adapter.

import { definePlugin, editHtml } from "htmx-ui-engine";

export const externalLinks = () =>
  definePlugin({
    name: "external-links",
    transform: (html) =>
      editHtml(html, {
        element(el) {
          if (el.matches("a[href^='http']")) el.setAttribute("rel", "noopener");
        },
      }),
  });

Plugin code runs on Node too (Vite loads the config there), so use node: APIs and editHtml(), a streaming HTML editor that runs on both runtimes, rather than Bun.* or HTMLRewriter. The site's docs (/docs/plugins/writing) cover templates, routes, build output, commands, browser code, testing and publishing.

Serve it from a backend

The static build is just files, so any server can host it — and one function does it for you: createSite() finds dist/, answers with the config's routes and fetch, and answers anything else with a 404 page: your pages/404.html when you have one, htmx-ui's default otherwise.

import { createSite } from "htmx-ui-engine";

const site = await createSite();                    // dist/, config, pages; no framework required
const server = Bun.serve({ fetch: (req) => site.handle(req) });

It also renders, so a backend can serve pages and htmx fragments from the same templates, macros and data as the site:

site.render("/docs/index.html", { user });          // a full page, per request
site.fragment("partials/invoice-row.html", { invoice });   // an HTML fragment for hx-get / hx-post

Ready-made adapters mount it as middleware or a plugin (Elysia first — it's the one that needs no ordering):

import { Elysia } from "elysia";
import { htmxUi } from "htmx-ui-engine/elysia";   // or /express, /hono, /fastify, /koa

new Elysia()
  .get("/api/hello", () => "<p>Hi</p>")
  .use(htmxUi({ site }))                             // everything else is the built site
  .listen(3000);

htmxUi(options?) takes the options of createSite(): root, config, context, asset(file, page), cache and site. Elysia, Fastify and Koa answer only what your own routes don't, in any order; Express and Hono middleware goes last. In production they serve the output of htmx-ui build, so run it before starting your server. The site's Server frameworks docs (/docs/servers) cover each framework, rendering and deploying.

Use the bundler plugins directly

// Bun: Bun.build / Bun.serve
import { htmxUi } from "htmx-ui-engine/bun";
import tailwind from "bun-plugin-tailwind";
await Bun.build({ entrypoints: ["./pages/index.html"], outdir: "dist", plugins: [await htmxUi(), tailwind] });
// Node: vite.config.ts
import { htmxUi } from "htmx-ui-engine/vite";
import tailwindcss from "@tailwindcss/vite";
export default { plugins: [htmxUi(), tailwindcss()] };

htmx-ui-engine itself exports the runtime-agnostic core: defineConfig, loadConfig, render, renderPage, routeFor, findPages, pagesOf, highlight, createSite, and definePlugin, editHtml, contentType for plugins.

Requires Bun 1.2.3+ or Node 22.12+ (Node runtime).

License

MIT