Render modes
Every route declares how it renders, explicitly, with a renderMode export. A route that exports nothing inherits its app's defaultRenderMode from app.config.ts (which itself defaults to ssr). Nothing is chosen for you based on file location or data-fetching patterns.
ssr — server-rendered per request
The default. The route's loader runs on every request, and the page is rendered fresh each time.
export const renderMode = "ssr";
export async function loader() {
return { renderedAt: new Date().toISOString() };
}
export default function Page({ data }) {
return <p>Rendered at: {data.renderedAt}</p>;
}ssg — pre-rendered at build time
The route is rendered once during devora build and served as static HTML from then on. Good for content that doesn't change per request — this docs site itself uses ssg for every page, since there's no login and content only needs to render once to be cached.
export const renderMode = "ssg";
export async function loader() {
return {};
}
export default function AboutPage() {
return <p>This page was rendered once, at build time.</p>;
}In dev, ssg (and isr) routes still render live per request, for a faster edit loop — the build-time pre-render only applies to devora build output.
csr — client-only
The server sends a minimal shell with no content; the route's actual component only mounts once a small client bootstrap script imports and renders it in the browser. No loader ever runs for a csr route.
import { PageShell } from "@devorajs/core/client";
// note the /client subpath — a csr route is bundled for the browser too,
// and the main @devorajs/core entry pulls in server-only code (node:crypto,
// node:fs) that can't bundle for a browser target.
export const renderMode = "csr";
export default function CsrDemo() {
return <p>Mounted client-side at: {new Date().toISOString()}</p>;
}isr — static with scheduled revalidation
Like ssg, but the pre-rendered page is regenerated once a revalidate window passes (or on a manual revalidatePath() call). The revalidation trigger is explicit and declared in the route file, not a hidden multi-layer cache.
export const renderMode = "isr";
export const revalidate = { seconds: 3600 };
export async function loader() {
return { renderedAt: new Date().toISOString() };
}
export default function IsrPage({ data }) {
return <p>Rendered at: {data.renderedAt}</p>;
}Ongoing revalidation is fully reliable under a long-lived Node process (self-hosted or Docker). On a serverless platform (Vercel/Netlify), a function's filesystem isn't guaranteed to persist or be shared across invocations, so the initial build's static content still serves correctly but ongoing regeneration there is comparatively less battle-tested — worth confirming for your own workload if you lean heavily on isr in a serverless deployment.
streaming — deferred to v2
A streaming mode (chunked SSR that doesn't block on slow data) is part of the design but not implemented in v1 — routes that declare it don't currently render. It's deferred rather than dropped: the island hydration mechanism v1 does ship assumes a synchronous render pass, which is incompatible with React's streaming APIs, and needs a Suspense-boundary-based rewrite first.
Islands and client-only components
Independent of render mode, an individual component can opt into island hydration — island(() => import("./ThreeScene")) — so only that component hydrates on the client while the rest of an SSR/SSG/ISR page stays static HTML. A component that must never run on the server at all (touches window at module scope, say) uses clientOnly(() => import("./Widget")) instead, which renders nothing server-side and only resolves in a real browser.