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.1.6
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.
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.
Documentation
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.
File-based, Next.js-style conventions resolved by Bun's own router.
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.
One seo() call for search engines, every social network, and rich results.
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.
Co-located CSS, collected by the build, with no import and no link tag to maintain.
Form handling where CSRF verification is structural, not a decorator.
What is on before you configure anything, and why it cannot be off by accident.
Dev server, production build, and what each command actually emits.
What a host has to provide, which platforms qualify, and the one file you add.
0.1.6 — error boundaries, a request hook, a request path about three times faster, and a dev server that stops breaking itself.
The two deploy releases — why a build would not run where it was not built.
What shipped in 0.1.3 and 0.1.2, and what each change replaced.
Where db.ts goes, where queries run, and the one place they must never.
One file that runs on every request, and what changed for API routes.
The same 16-page site built in Stoneware, Astro and Next.js, measured.