Server-first
Every route renders to a complete HTML string. A page with no islands ships zero bytes of JavaScript — not a small runtime, not a hydration shim, nothing.
Bun-native · server-first · v0.2.0
HTML by default. JavaScript by choice.
Build content-heavy sites without shipping a client runtime to pages that do not need one. Interactivity is opt-in per directory, and the safe path is the default one.
This site is the documentation for stoneware-dev/stoneware-core — the framework's own source, issues, and releases live there.
The problem
Stoneware inverts the default: HTML first, JavaScript only where you ask for it. Four specific problems that follow from it.
The separation
Nothing scans your files for a directive and decides. The boundary is a directory, and the build enforces it — a route is fired once and is finished, an island is fired again for the surface that ships.
routes/blog/[slug].tsx
one firing
<article>…</article>0 B ships
islands/Counter.tsx
the same firing
<button>…</button>0 B ships
then a glaze firingCounter-a1b2c3.js0.2 KB ships
Both shelves come out of the first firing as finished HTML, which is why an island is never an empty box waiting for its script. Only the glaze is compiled for the browser, and only the island has one. The whole pipeline, step by step →
Install
Scaffolding runs on plain Node, so it works before Bun is installed. Everything after that runs on Bun.
bunx create-stoneware my-site
Both runners scaffold the project. Stoneware itself runs on Bun: the generated app needs it for stoneware dev and stoneware build.
Principles
Each of these is a constraint the framework enforces rather than a convention it suggests.
Every route renders to a complete HTML string. A page with no islands ships zero bytes of JavaScript — not a small runtime, not a hydration shim, nothing.
Templates are plain functions: props in, markup out. No classes, no hooks, no lifecycle. Logic lives in ordinary functions, not inside UI definitions.
Islands use Preact Signals directly. Stoneware does not implement a reactive graph — that is a deliberate scope boundary, not an oversight.
A file under islands/ hydrates. A file under routes/ never does. No per-file directive to remember, and no way to make a page interactive by accident.
Auto-escaping, automatic CSRF verification, and a restrictive CSP need no configuration to be on. The unsafe path requires typing more.
Bun.serve, Bun.build, Bun.escapeHTML, Bun.CSRF, Bun.FileSystemRouter. No npm package reimplementing something the runtime already ships.
Islands
// islands/Counter.tsx — the only file here that ships JS
import { signal } from "stoneware/signals";
const count = signal(0);
export default function Counter() {
return (
<button onClick={() => count.value++}>
Clicked {count} times
</button>
);
}Live, on this page
Server-rendered as static HTML, then hydrated. Clicking updates one text node — the tree is never re-run and nothing is diffed.
Content sites
None of this is an SEO feature bolted on. It follows from rendering the whole document on the server and shipping no runtime alongside it.
Measured
Twenty articles and an index — the same content, the same markup, built three ways and served by each framework's own production server.
JavaScript on an article page
0 B
Astro 0 B · Next.js 576 KB
Pages shipping no JavaScript
20 of 21
Astro 20 of 21 · Next.js 0 of 21
JavaScript on the one interactive page
4.8 KB
Astro 0.3 KB · Next.js 173.7 KB
Peak memory during the build
88 MB
Astro 356 MB · Next.js 1143 MB
Median time to first byte
1.13 ms
Astro 1.74 ms · Next.js 1.84 ms
Requests per second, 100 connections
2236
Astro 1984 · Next.js 920
The third row is a loss and it stays on the page: a plain script tag beats a hydrated island for one text box, and 0.3 KB against 4.8 KB is not close. The full study, and what varies between runs →
Documentation
Get started
The specific problems Stoneware exists for — and the ones it does not.
Scaffold a project, run it, and understand what the two directories mean.
Every file create-stoneware writes, what it is for, and what a build adds.
The path a request takes, and what hydration does to the DOM.
Core
File-based, Next.js-style conventions resolved by Bun's own router.
Plain functions that compose, take children and nest — and the one rule about async that follows from rendering to a string.
How a component earns its JavaScript, and what hydration actually does.
client:visible, client:idle and client:media — and what a page stops downloading.
Per-page metadata, and an <Image> that fixes layout shift without a build pipeline.
What a Stoneware site does for search before you configure anything, and the one call that writes the rest of the head.
Co-located CSS, collected by the build, with no import and no link tag to maintain.
Custom 404 and 500 pages, and the three properties that hold whether or not you write them.
Lose one subtree instead of the whole page, without losing the error.
Server
Form handling where CSRF verification is structural, not a decorator.
One file that runs on every request, and what changed for API routes.
Where db.ts goes, where queries run, and the one place they must never.
Request in, HTML out — with no port opened, no server started and nothing to tear down.
Security
Build & deploy
Dev server, production build, and what each command actually emits.
A checklist, one decision, and a walkthrough per platform — server or static, Vercel or Cloudflare.
What every response tells a browser and a CDN, why a page is never cached on the server, and the header that keeps a shared cache from handing one visitor another's page.
Prerender the whole site to files any host can serve, and know exactly which pages cannot go.
Reference
Every option in stoneware.config.ts, its default, and the environment variable that overrides it.
Everything the package exports, what it is for, and which of them you are unlikely to need.
Two studies: what the server does on 0.2.0, and what a visitor's browser waits for. One named run each, with what varies between runs called out.