Rendering Modes
pracht supports four rendering modes configured per route. Each route declares how and when its HTML is generated โ giving you the right performance and freshness trade-off for every page in one app.
Overview
| Mode | HTML generated | Loader runs | Best for |
|---|---|---|---|
| SSG | Build time | Build time | Marketing pages, docs, blogs |
| SSR | Every request | Every request | Personalized, dynamic pages |
| ISG | Build + revalidation | Build + on stale | Pricing, catalogs, semi-static |
| SPA | Client only | Client navigation | Auth-gated dashboards, admin UI |
SSG โ Static Site Generation
route("/about", "./routes/about.tsx", { render: "ssg" });HTML is generated at build time. The loader runs once during the build, and the output is written to dist/client/about/index.html. No server required for this route โ it's served as a static file from your CDN.
Dynamic SSG paths
For routes with dynamic segments, export a getStaticPaths function that returns the params for each page:
export function getStaticPaths(): RouteParams[] {
const posts = getAllPosts();
return posts.map(p => ({ slug: p.slug }));
}
export async function loader({ params }: LoaderArgs) {
return { post: await getPost(params.slug) };
}
export function Component({ data }) {
return <article>{data.post.title}</article>;
}The build calls getStaticPaths() to enumerate params, constructs full paths from the route pattern, then runs the loader and renderer for each. Prerendering runs concurrently (default: 6 parallel renders).
SSR โ Server-Side Rendering
route("/dashboard", "./routes/dashboard.tsx", { render: "ssr" });HTML is generated fresh on every request. The loader runs server-side, the component renders to a string, and the full HTML response includes the serialized hydration state.
After the initial load, client-side navigation takes over โ subsequent navigations fetch only the loader data as JSON, not full HTML.
When to use SSR
- Pages that depend on the request (cookies, auth, personalization)
- Data that changes on every request
- Pages where SEO matters and data is dynamic
ISG โ Incremental Static Generation
import { timeRevalidate } from "@pracht/core";
route("/pricing", "./routes/pricing.tsx", {
render: "isg",
revalidate: timeRevalidate(3600), // revalidate every hour
});ISG generates HTML at build time (like SSG) but regenerates it after a configurable time window. On the first request after the window expires, the stale page is served immediately while a new version regenerates in the background โ stale-while-revalidate.
[!INFO] ISG revalidation is implemented at the adapter level. The Node adapter uses file `mtime`; Cloudflare uses a cache timestamp in KV.
Webhook revalidation (Phase 2)
import { webhookRevalidate } from "@pracht/core";
{
revalidate: webhookRevalidate({ key: "pricing-update" });
}
// POST to the revalidation endpoint to trigger regenerationSPA โ Single Page Application
route("/settings", "./routes/settings.tsx", { render: "spa" });No server-side rendering. The server returns a minimal HTML shell, and the component renders entirely in the browser. The loader runs during client-side navigation only.
When to use SPA
- Auth-gated pages where SEO doesn't matter
- Complex interactive UIs (editors, rich dashboards)
- Pages where server rendering adds no value
Mixing Modes
The real power is mixing modes in a single app without separate deployments or frameworks:
export const app = defineApp({
routes: [
group({ shell: "public" }, [
route("/", "...", { render: "ssg" }), // Static
route("/pricing", "...", {
render: "isg", // Revalidating
revalidate: timeRevalidate(3600),
}),
route("/login", "...", { render: "ssr" }), // Dynamic
]),
group({ shell: "app", middleware: ["auth"] }, [
route("/dashboard", "...", { render: "ssr" }), // Personalized
route("/settings", "...", { render: "spa" }), // Client-only
]),
],
});Client Navigation
After the initial page load โ regardless of render mode โ the client router handles all navigation. Route transitions use the same flow:
- Client matches the new route
- Fetches loader data as JSON via
x-pracht-route-state-requestheader - Updates the component tree with new data
- Pushes to browser history
This means even SSG routes get fresh loader data during client navigation. The static HTML is only for the initial load and crawlers.