SSR + HTMX + Alpine + Bun = HTML over the wire. No SPA. No illusions.
A proof-of-concept website demonstrating server-driven HTML architecture with modern tooling. This is what web development looks like when you stop pretending the browser is a JVM.
- S – SSR (Server-Side Rendering): HTML is generated on the server. Period. No hydration ceremony, no JavaScript rehydration tax.
- H – HTMX: Intercept HTTP requests at the JavaScript level and swap DOM fragments. Navigation feels instant. Forms submit cleanly. No JSON API complexity.
- A – Alpine.js: Tiny reactive library (16KB) for local interactivity only. Menu toggles, loading spinners, inline edit modes. Not for rendering, routing, or state management.
- B – Bun: Modern JavaScript runtime. Fast startup, native JSX support, built-in TypeScript. No Node.js. No package manager bloat.
server.tsx # Hono server (single entry point)
views/
layout.tsx # HTML layout with header, nav, footer
pages/
home.tsx # Routes return JSX components
manifesto.tsx # Rendered to HTML strings
about.tsx # Sent as Content-Type: text/html
demo.tsx # HTMX and Alpine interactions
views/public/
styles.css # Simple CSS (no PostCSS, no SCSS)
htmx.js # CDN script (https://unpkg.com/htmx.org)
alpine.js # CDN script (https://unpkg.com/alpinejs)
# Install dependencies
bun install
# Development (auto-reload)
bun run dev
# Production
bun startVisit: http://localhost:3000
app.get("/:lang/manifesto", (c) => {
const lang = c.get("lang");
return c.html(
<Layout lang={lang} page="manifesto">
<Manifesto lang={lang} />
</Layout>
);
});The server receives a request, runs JSX through the component tree, and sends HTML. The browser receives a valid HTML document and renders it immediately. No JavaScript needed for initial paint.
<nav hx-boost="true">
<a href="/en/demo">Demo</a> <!-- hx-boost makes this AJAX -->
</nav>When a user clicks a link, HTMX intercepts the HTTP request (instead of a full page reload), sends it via XHR, and swaps the response into the DOM using hx-swap="innerHTML". The browser's back/forward buttons still work. No client-side router.
<form hx-post="/en/demo/form" hx-target="#message">
<input type="text" name="email" required />
<button type="submit">Submit</button>
</form>When a form is posted, HTMX sends the data. The server validates, processes, and sends back HTML (not JSON). HTMX swaps it in. If validation fails, the server sends the form with error messages. The browser just displays it.
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">Hidden content</div>
</div>Alpine is loaded from a CDN and instantly provides reactivity for UI state that doesn't need server coordination. Menu toggles, visibility states, loading spinners. No build step. Just add x- attributes to HTML.
- No client-side routing: Every page transition is an HTTP request.
- No hydration: HTML is ready to display immediately. Zero JavaScript execution needed to show the initial page.
- No JSON API: The server responds with HTML, not data structures.
- No JavaScript build pipeline: No bundler, no tree-shaking, no code splitting.
bun runand you're done. - No state management library: The server is the source of truth.
- HTTP is well-designed: GET, POST, status codes, caching headers. We've been trying to hide it. This doesn't.
- The browser is fast at rendering HTML: It's literally its job. Faster than parsing JSON and running JavaScript to render DOM nodes.
- HTMX is tiny and boring: 14KB. No magic. Just AJAX with HTML instead of JSON.
- Alpine is boring: 16KB. Local interactivity only. No global state. No lifecycle hooks you have to memorize.
- Bun is fast: Startup and execution measured in milliseconds, not seconds.
| Aspect | SHAB | Next.js / SPA |
|---|---|---|
| Build | None. bun run |
webpack/turbopack + config files |
| JS sent to browser | ~30KB (HTMX + Alpine CDN) | 100KB–500KB+ (after tree-shaking) |
| First Paint | ~50ms | 2–5 seconds (JS parsing + hydration) |
| Routing | Server-side. HTTP requests. | Client-side. JavaScript router. |
| Forms | HTTP POST → HTML → DOM swap | Prevent default + state update + re-render |
| APIs | Server renders HTML directly | JSON endpoint + client-side template rendering |
| Complexity | Server-side templating | State management + hooks + hydration strategy |
Next.js excels at:
- Complex interactive UIs (animations, drag-and-drop)
- Offline-first apps
- Highly dynamic content (real-time collaboration)
- Serving multiple clients (web, mobile API)
SHAB is better for:
- Content sites (blogs, docs, dashboards)
- Traditional web applications
- Server-driven architectures
- Teams that want simplicity
- server.tsx: Hono app with middleware, routes, and HTMX endpoints.
- views/layout.tsx: HTML layout with nav, header, footer.
- views/pages/*.tsx: Page components rendered to HTML strings.
- public/styles.css: Minimal CSS (no build step).
- Navigation: HTMX
hx-booston links. No page reload. Smooth transitions. - Form validation: Server-side. Validation errors returned as HTML.
- Inline editing: Alpine toggles edit mode. HTMX submits to server.
- Fragment swapping: HTMX targets specific divs and swaps partial content.
- i18n: Only English content (though URL prefix
/en,/plexist for routing). - Database: This is a PoC. No persistence layer.
- Authentication: Not implemented, but compatible with session cookies + server-side checks.
- Real-time: Not a goal. HTMX + Server Request/Response is synchronous by design.
- Use
bun build server.tsx --outfile dist/server.jsto pre-compile if desired (not required). - Set a process manager (pm2, systemd, Docker).
- Set environment variables (PORT, NODE_ENV).
- Serve static files with a CDN or nginx in front.
Add a database:
const db = await connectDB(process.env.DATABASE_URL);
app.post("/submit", async (c) => {
const data = await c.req.formData();
await db.insert(data);
return c.html(<div>✓ Saved</div>);
});Add a service:
const result = await emailService.send({
to: email,
subject: "Welcome",
});
return c.html(<SuccessMessage />);Add middleware:
app.use(authMiddleware); // Check session cookies
app.use(rateLimitMiddleware); // Rate limit per IPAll of it stays on the server. The browser still just renders HTML.
The web was solved in 1999. We've been complicating it ever since.
SHAB is not innovation. It's archaeology. Digging up REST principles, HTML-over-the-wire patterns, and classical web architecture that worked for decades. We wrapped it in modern tooling (Bun, HTMX, Alpine) so it works in 2025.
The reason this feels radical is that we spent 15 years building the exact opposite. But the reason it feels good is that HTTP and HTML are not mistakes. We were just convinced they were.
- Install Vercel CLI:
npm i -g vercel- Deploy:
vercel- Follow prompts. Vercel will:
- Detect the project
- Install Bun runtime
- Build and deploy
For any Node.js-compatible platform:
- Install Bun on the server
- Clone the repository
- Run
bun install - Run
bun run server.tsx - Set
PORTenvironment variable if needed
PORT- Server port (default: 3000)
No SPA. No hydration. No JavaScript build pipeline. Just HTTP and HTML.