Pracht is a full-stack Preact framework built on Vite. It draws routing and rendering
- Preact-first — lightweight by default; ship less JavaScript to the client.
- Vite-native — leverage Vite's dev server, HMR, and multi-environment builds.
- Explicit over magic — hybrid routing (file modules + manifest config) so developers always know what runs where.
- Deploy anywhere — platform adapters isolate runtime differences; one codebase targets Cloudflare Workers, Vercel, Node, etc.
- AI-assisted — Claude Code skills for scaffolding, debugging, and operating the framework.
- Proven by tests — thorough E2E testing (Playwright) to prove SSR, SSG, ISG, SPA, and client navigation actually work in production scenarios.
- Instant local DX — one command to start, instant HMR, zero config. Hit a button and you're rolling.
- Hybrid file-based routing: route modules live in
src/routes/, wired via an explicitsrc/routes.tsmanifest usingdefineApp(),route(),group(). - Dynamic segments:
:paramsyntax, catch-all segments. - Typed route links:
pracht typegenemits route id/param types and href helpers for<Link>,useNavigate(), and code outside components. - Shells: named layout wrappers (e.g.
public,app) decoupled from URL structure; assigned per route or group. - Middleware: named middleware defined in
src/middleware/, applied per route or group; runs server-side before loaders. - Route groups: inherit shell, middleware, render mode, and path prefix.
- Navigation UX: automatic scroll restoration, link prefetching
(
intent/viewport/render), and opt-in View Transitions.
Per-route render mode via { render: "spa" | "ssr" | "ssg" | "isg" }:
| Mode | When HTML is generated | Use case |
|---|---|---|
| SPA | Client only | Dashboards, auth-gated UI |
| SSR | Every request | Personalized / dynamic pages |
| SSG | Build time | Marketing, docs, blog |
| ISG | Build + revalidation | Pricing, catalog, semi-static |
ISG supports time-based and webhook-based revalidation policies.
Orthogonal to the render mode, every route has a hydration mode via
{ hydration: "full" | "islands" | "none" } (default "full", zero change
for existing apps):
"islands"— partial hydration inspired by Deno Fresh and Astro: only components fromsrc/islands/hydrate, each as its own code-split chunk loaded by a tiny bootstrap instead of the full client runtime. Per-usage strategies via theclientprop:load(default),idle,visible. Island props are JSON-serialized into the HTML with clear dev errors for non-serializable values. Navigation is MPA-style full-document in v1."none"— fully static output, zero JavaScript shipped.
Works in both the manifest router and the pages router
(export const HYDRATION = "islands"). See docs/ISLANDS.md.
Two styles, both fully supported — pick whichever fits your mental model:
Inline (co-located in the route file):
- Loaders:
export function loader(args)— runs at build (SSG), request (SSR), or client navigation time. Returns typed, serializable data. - Form: A component that allows posting to one of our API routes
Separate files (manifest-wired):
- Loader logic can live in a dedicated server file (e.g.
src/server/). - Wired via the route config object in
routes.ts:route("/dashboard", { component: "./routes/dashboard.tsx", loader: "./server/dashboard-loader.ts", render: "ssr", });
- Route files become pure components — no server code mixed in.
Both styles can coexist in the same app. When a separate loader file is
specified in the config, it takes precedence over inline exports.
- Head:
export function head(args)— per-route<head>metadata merged with shell-level head. - Client hooks:
useRouteData(),useRevalidate(),useNavigation()(pending navigation/submission state for progress bars and optimistic UI),useNavigate(),useLocation(),useParams(),<Form>component,<Link>(withprefetch,preserveScroll,viewTransitionprops), and imperativeprefetch().
Standalone server endpoints independent of the page rendering pipeline:
- Defined in
src/api/with file-based path mapping (e.g.src/api/health.ts→/api/health). - Export named HTTP method handlers (
export function GET(args),POST(args), etc.) or one default handler that branches onargs.request.method. - Receive the same
LoaderArgs-style context (request, params, context, signal). - Return
Responseobjects directly — full control over status, headers, body. - API routes are independent of page-route middleware by default. Shared API
policy can be attached explicitly via
defineApp({ api: { middleware: [...] } }). - Opt-in request validation via
defineApi()with any Standard Schema validator (body, query, params), standardized 422 issue responses, and JSON-value handler returns. pracht typegenregisters each API route's request/response types so theapiFetch()client checks paths, methods, bodies, queries, and params at compile time and returns typed responses. See docs/API_VALIDATION.md.
Platform adapters export a request handler shaped for their runtime:
| Adapter | Runtime | Notes |
|---|---|---|
adapter-node |
Node.js http |
Static file serving, ISG mtime check |
adapter-cloudflare |
Workers fetch |
env.ASSETS, Cache API-backed ISG, opt-in Workers Caching |
adapter-vercel |
Serverless / Edge | Build Output API v3 + native ISR |
Each adapter:
- Converts platform request → Web
Request - Loads Vite manifests for asset injection
- Implements ISG revalidation only when the platform has appropriate persistent storage/cache semantics, and documents/warns otherwise
- Generates a platform-specific entry module via the Vite plugin
- Constraints:
defineApp({ constraints })declares invariants over the route graph (requireMiddleware,requireShell,requireRenderMode,forbidRenderMode,requireHead);pracht verifyenforces them deterministically. - App-graph snapshot + plan:
.pracht/app-graph.jsonis a committed route-graph lockfile;pracht plandiffs the live graph against a base git ref for an intent-level changelog (--markdownfor PR comments); verify fails when the snapshot is stale. - Report:
pracht reportassembles a PR-ready markdown body from the graph diff, verify results, and bundle budgets. - Smoke tests:
pracht generate routeemits a Playwright smoke test when the app has an e2e setup. - Authoring guide:
pracht llms [--write]and the MCPget_docstool expose the framework's conventions to any coding agent.
Agent skills for framework authors and app builders live in skills/
(one SKILL.md per skill; .claude/skills symlinks there so Claude Code picks them up
in this repo). They are published with a discovery manifest at
https://pracht.resynapse.dev/.well-known/agent-skills/index.json and seeded into new
apps by create-pracht — see the skills catalog:
- Scaffold: generate routes, shells, middleware, API routes with correct wiring
- Debug: framework-aware debugging (route matching, loader errors, hydration)
- Deploy: guided adapter setup and deployment
- Audit: loaders, auth, CSRF, headers, secrets, bundles, SEO, a11y, and more
Phase 1 delivers a working framework that can build and serve a Preact app with SSR and SSG, deployed to Node. Thoroughly tested with Playwright E2E tests.
-
packages/framework— core exportsdefineApp(),route(),group()— route manifest APIRouteModuletype — loader, head, default/Component, errorBoundaryShellModuletype — layout wrapper with head contributionMiddlewareModuletype — server-side request interceptor- Router:
matchAppRoute()segment-based matching - Server renderer:
handlePrachtRequest()→ full HTML with hydration state - Client runtime:
startApp(), hydration, client-side navigation - Hooks:
useRouteData(),useRevalidate(),<Form>
-
packages/vite-plugin— Vite integration- Virtual modules:
virtual:pracht/client,virtual:pracht/server - Multi-environment build (client + ssr)
import.meta.glob()module registry generation- SSG prerendering at build time (concurrent, configurable)
- Dev server with HMR
- Virtual modules:
-
packages/adapter-node— Node.js adaptercreateNodeRequestHandler()— Web Request/Response overhttp- Static file serving from
dist/client/ - Vite manifest loading for asset injection
- ISG time-window and webhook revalidation
-
packages/cli— developer tooling (instant local DX)pracht dev— one command, instant Vite dev server with HMRpracht build— production build with clear output- Node targets run the production build with
node dist/server/server.js; Cloudflare and Vercel use their platform CLIs - Zero config to start — sensible defaults, override when needed
- Fast feedback loop: save a file → see the change instantly
-
Example app — demonstrates SSR + SSG routes, shells, loaders, and forms
-
E2E tests — Playwright tests proving:
- SSR renders correct HTML on the server
- SSG generates static files at build time
- Client-side navigation works without full page reload
- Loaders return correct data
- Shells wrap routes correctly
- Hydration completes without errors
pracht/
packages/
framework/ # Core: routing, rendering, runtime, types
vite-plugin/ # Vite integration, virtual modules, build
adapter-node/ # Node.js server adapter
adapter-cloudflare/ # Cloudflare Workers adapter
adapter-vercel/ # Vercel Edge adapter
cli/ # Dev/build/generate/inspect/verify/plan/report/doctor commands
create-pracht/ # (Phase 2) Starter scaffolding
example/ # Working example app
docs/ # Architecture and design docs
e2e/ # Playwright end-to-end tests
The live scaffold for this layout is documented in docs/WORKSPACE.md.
See docs/ARCHITECTURE.md for detailed rationale.
- Hybrid routing over pure file-based — explicit manifest avoids hidden conventions and enables shell/middleware grouping without directory nesting.
- Per-route render modes — no global default; each route declares its strategy.
- Server-owned navigation — client fetches route state JSON from server rather than running loaders in the browser; keeps secrets server-side.
- Virtual module generation — Vite plugin generates entry points from
import.meta.glob(), avoiding manual registration. - Adapter pattern — platform logic isolated from core; adapters are thin request/response translators.