SSR & SSG
EmberKit ships four rendering modes controlled by emberkit.config.ts. The Vite plugin handles SSR during development; the CLI builds client and server bundles and can pre-render static routes at build time.
Rendering Modes
| Mode | Build output | Runtime | Best for |
|---|---|---|---|
hybrid default | Client + server bundle + ssr-manifest.json; static routes pre-rendered to HTML files | Pre-rendered HTML for static paths; SSR for dynamic routes ([param]) | Most apps (docs, marketing + dynamic pages) |
static | Same as hybrid, but all non-dynamic routes pre-rendered at build time | Static files only (no server required) | Blogs, marketing sites, documentation |
ssr | Client + server bundle + manifest; no pre-render step | Every HTML request rendered on the server | Personalized or frequently changing content |
spa | Client bundle only | Client-side routing; dev server skips SSR middleware | Dashboards, admin panels, fully client-driven apps |
How routes are classified
During emberkit build, each file under src/routes/ becomes a manifest entry:
- Static route β path has no dynamic segments (e.g.
/,/about,/docs/installation) - Dynamic route β path contains a segment parameter (e.g.
/blog/:slugfrom[slug].tsx)
In hybrid mode, static routes are written to dist/.../index.html at build time. Dynamic routes are rendered at request time via dist/server/entry-server.js.
Configuration
Create emberkit.config.ts in your project root (or run emberkit generate config):
import { defineConfig } from '@emberkit/core';
export default defineConfig({
mode: 'hybrid', // 'static' | 'ssr' | 'spa' | 'hybrid'
server: {
port: 3000,
host: 'localhost',
},
build: {
outDir: 'dist',
target: 'esnext',
},
// Optional: merge extra Vite options
vite: {
plugins: [/* emberkitVitePlugin(), tailwindcss(), β¦ */],
},
});
Register the Vite plugin in vite.config.ts (included in CLI templates):
import { defineConfig } from 'vite';
import { emberkitVitePlugin } from '@emberkit/core/vite-plugin';
export default defineConfig({
plugins: [emberkitVitePlugin()],
esbuild: {
jsxImportSource: '@emberkit/core',
},
});
The plugin reads emberkit.config.ts for mode, routeDir, markdown options, and compression. If no config file exists, hybrid is the default.
Development
pnpm dev # emberkit dev β Vite + HMR
For every mode except spa, the dev server runs SSR middleware on HTML navigations:
- Skips assets, HMR, and non-HTML requests
- Loads the virtual SSR entry and calls
render(url, server) - Injects rendered markup into
index.htmlat#app(or<body id="app">)
View the page source in the browser to confirm server-rendered HTML before hydration.
Production Build
pnpm build # emberkit build
| Mode | Steps |
|---|---|
spa | Client bundle β dist/ |
ssr | Client bundle β SSR bundle β ssr-manifest.json |
hybrid | Client β SSR β manifest β pre-render static routes |
static | Client β SSR β manifest β pre-render all non-dynamic routes |
Build artifacts:
dist/
βββ index.html # Client shell (and / for pre-rendered home)
βββ assets/ # JS, CSS, hashed chunks
βββ ssr-manifest.json # mode, routes[], isStatic flags
βββ server/
β βββ entry-server.js # SSR renderer (unless you provide src/entry-server.tsx)
βββ about/
βββ index.html # Pre-rendered static route (hybrid/static)
Preview and production servers
pnpm preview # emberkit preview β quick local check (default port 4173)
pnpm serve # emberkit serve β production server using ssr-manifest.json
Preview serves static files and calls entry-server.js for SSR modes.
Serve resolves requests in order: static asset β pre-rendered path/index.html β SSR (for ssr mode, or dynamic routes in hybrid) β SPA fallback to root index.html.
Custom server entry
Add src/entry-server.ts or src/entry-server.tsx to replace the generated SSR shim. Export render(url) returning { html, status } or an HTML string.
Route metadata (built-in SSR head)
The default server entry injects optional route exports into <head>:
// src/routes/about.tsx
export const metadata = {
title: 'About β My App',
description: 'Learn more about us',
};
export default function AboutPage() {
return <main><h1>About</h1></main>;
}
For declarative head tags from components, use the <Head> component β it updates the DOM on the client and registers tags during render. See Head for details.
Custom error pages
Place these in src/routes/ (not nested under a segment):
| File | When used |
|---|---|
404.tsx | No route matches |
500.tsx | Uncaught error while rendering a route |
If neither file exists, EmberKit renders built-in default pages (same components used for client navigation). Custom pages receive pathname on 404 and error on 500.
// src/routes/404.tsx
export default function NotFound() {
return (
<main>
<h1>404</h1>
<p>Page not found</p>
</main>
);
}
Dynamic route props
SSR passes params to the matched route component:
// src/routes/blog/[slug].tsx
import type { RouteParams } from '@emberkit/core';
export default function Post({ params }: RouteParams<{ slug: string }>) {
return <article><h1>{params.slug}</h1></article>;
}
On the client, the runtime also provides params, query, and request via RouteParams. See Routing.
Low-level HTML APIs
These utilities live in @emberkit/core for custom servers and tests:
renderToHTMLString
import { renderToHTMLString } from '@emberkit/core';
const fragment = renderToHTMLString(<Page />);
// HTML fragment (body content only)
renderToHTMLString escapes text and string attributes for safe SSR output.
Head registry
import { drainHeadContent } from '@emberkit/core';
// After rendering components that use <Head>:
const headTags = drainHeadContent();
Use this in a custom entry-server when you need full <Head> support in SSR.
Internal helpers
renderSSR, createHtmlDocument, and createStreamingRenderer exist in the core package for advanced integrations but are not part of the public @emberkit/core export surface today. Prefer the CLI pipeline or renderToHTMLString + drainHeadContent for custom servers.
Hydration after SSR
Server HTML includes data-ek-bind indices and data-ekclick handler ids for interactive regions. The client entry reconnects signals and events without re-rendering the full tree. Details: Hydration.
Performance (why speed is the goal)
EmberKitβs rendering modes exist to minimize time-to-content and client work:
| Technique | Speed benefit |
|---|---|
| SSR / pre-render | Users see HTML immediately; no empty shell waiting for JS |
static / hybrid pre-render | Static paths served from disk or CDN β very low TTFB |
| Selective hydration | Smaller JS parse/execute; faster Time to Interactive on content-heavy pages |
| Signal DOM binding | Updates skip virtual DOM diffing β only bound nodes change |
Speed is the frameworkβs first principle; weight and zero-JS defaults support it. See Introduction.
Edge deployment
Deploy pre-rendered dist/ to any static host, or run entry-server.js on Node, Bun, or edge adapters. See Edge Deployment.
Next Steps
- Hydration β Selective client interactivity
- Routing β File-based routes, layouts, loaders
- Head β Declarative
<head>management - Edge Deployment β Cloudflare Workers and Deno