One app graph.
Two kinds of caller.

pracht resolves your routes, loaders, API routes, and capabilities into one explicit graph — then projects it to browsers and to agents: HTTP endpoints, WebMCP page tools, remote MCP, and llms.txt. Most frameworks render your app for humans and leave agents to scrape it.

src/routes.ts

routes.ts
import { defineApp, group, route, timeRevalidate } from "@pracht/core";

export const app = defineApp({
  shells: {
    public: "./shells/public.tsx",
    app:    "./shells/app.tsx",
  },
  middleware: { auth: "./middleware/auth.ts" },
  // The same graph an agent sees: typed operations, not a scraped DOM.
  capabilities: { "notes.search": "./capabilities/notes-search.ts" },
  routes: [
    group({ shell: "public" }, [
      route("/",        "./routes/home.tsx",    { render: "ssg" }),
      route("/pricing", "./routes/pricing.tsx", {
        render: "isg", revalidate: timeRevalidate(3600),
      }),
    ]),
    group({ shell: "app", middleware: ["auth"] }, [
      route("/dashboard", "./routes/dashboard.tsx", { render: "ssr" }),
      route("/settings",  "./routes/settings.tsx",  { render: "spa" }),
    ]),
  ],
});

Measured, not claimed

What a page costs

Hydration is a per-route setting, so the framework's cost is something you choose rather than something you inherit. Each rung below is the same page, rendering the same markup, with one thing changed.

hydration: "none"
0 KB

Static HTML. No client runtime is injected at all.

hydration: "islands"
7.5 KB

Preact plus the island bootstrap. Only components in src/islands/ hydrate — the router never loads.

hydration: "full"
17.4 KB

The page hydrates and the client router takes over navigation, prefetching, and loader fetches.

full + preact/compat
18.2 KB

The same page with the React compatibility layer, so React-authored dependencies resolve.

Gzipped client JavaScript a cold load fetches, including the chunks the router imports after hydration. Your application code sits on top of this. Switching prefetching off with client: { prefetch: false } takes full hydration to 15.9 KB. Re-measure any of it with pnpm benchhow these numbers are produced.

Why pracht

Write the app down once

Everything below follows from the graph being explicit. A manifest a machine can read is a manifest a machine can serve, review, and test.

One Explicit App Graph

Routes, loaders, API routes, and capabilities are declared in one typed manifest and resolved into a single graph. Nothing is inferred from folder names, so what runs where is readable by you, by a reviewer, and by a machine.

Projected to Agents, Not Scraped

The same graph becomes typed HTTP endpoints, WebMCP page tools, remote MCP tools, and an llms.txt index. Agents call declared operations with validated input instead of guessing at your DOM.

One Set of Rules per Operation

Validation, middleware, effect class, and confirmation run on the server for every caller — your loader, the browser, an in-page agent, a remote MCP host. The human UI and the agent surface cannot drift, because they are the same function.

Preact-First

Full hooks, JSX, and the Preact ecosystem on a runtime you can size: 0 KB on a static route, 17.4 KB gzip fully hydrated. Both measured by pnpm bench, both gated in CI.

Per-Route Render and Hydration Modes

SSG, SSR, ISG, or SPA, and full, islands, or no hydration — chosen per route. Mix static marketing pages with dynamic dashboards in one app and one build.

Vite-Native, End-to-End Typed

Full Vite pipeline for client and SSR builds, plus loader return types that flow into components and capability contracts that flow into every call site.

And it deploys anywhere: Node, Cloudflare Workers, Netlify, Vercel, or a pure static export, from one codebase and one build. A thin adapter, not a rewrite.

Rendering

One app, four rendering strategies

Configure render mode per route. Mix and match in the same app without extra wiring or separate deployments.

SSG

Static Generation

HTML at build time. Serve from CDN with zero server cost. Perfect for marketing pages, blogs, and docs.

SSR

Server Rendering

Fresh HTML on every request. Full access to cookies, headers, and auth state. Ideal for personalized pages.

ISG

Incremental Static

Static HTML that regenerates on a schedule. Serve instantly, update in the background. Great for catalogs and pricing.

SPA

Client-Only

No SSR — render entirely in the browser. Best for auth-gated dashboards where SEO doesn't matter.

Data Loading

Loaders stay on the server

Loader functions run server-side only — during the build for SSG, on each request for SSR. Secrets, database connections, and API keys never reach the client bundle.

After hydration, client navigation fetches only the loader data as JSON — the component tree updates without a full page reload.

Data loading guide
routes/dashboard.tsx
import type { LoaderArgs, RouteComponentProps } from "@pracht/core";

export async function loader({ request, context }: LoaderArgs) {
  const user = await getUser(request);
  return { user, projects: await context.db.projects.all() };
}

export function head({ data }) {
  return { title: `${data.user.name} — Dashboard` };
}

export function Component({ data }: RouteComponentProps<typeof loader>) {
  // data is typed: { user: User; projects: Project[] }
  return <h1>Welcome, {data.user.name}</h1>;
}

Get Started

Ready to build?

Install pracht and the Vite plugin, wire up your adapter, and ship to Cloudflare Workers or Vercel in minutes.

$npm create pracht@latest my-app