By default, KosmoJS source folders render on the client using Vite's fast dev server and instant HMR.
When your application requires improved SEO, faster-perceived loading, or better performance on low-end devices, SSR becomes beneficial - especially for public-facing pages and marketing content.
The SSR generator provides the required server runtime, while keeping your development workflow unchanged.
π οΈ Enabling SSR β
Selecting SSR during source folder creation activates it automatically.
For folders created without SSR (or when adding SSR capabilities to existing setups), manual activation is available through generator registration in your source folder's vite.config.ts:
import vuePlugin from "@vitejs/plugin-vue";
import devPlugin from "@kosmojs/dev";
import {
// ...
vueGenerator,
ssrGenerator,
} from "@kosmojs/generators";
import defineConfig from "../vite.base";
export default defineConfig(import.meta.dirname, {
plugins: [
vuePlugin(),
devPlugin(apiurl, {
generators: [
// ...
vueGenerator(),
ssrGenerator(), // add SSR support
],
}),
],
});π Server Entry Point β
When SSR is activated, KosmoJS generates entry/server.ts with the default implementation.
The renderFactory function on the server side orchestrates SSR rendering.
It accepts a callback that returns an object with rendering methods:
renderToString(url, { criticalCss })- Default implementation that renders the complete page before transmissionrenderToStream(url, { criticalCss })- Optional advanced implementation for progressive streaming SSR
Important: Only renderToString is provided by default. Streaming SSR requires manual renderToStream implementation. When both methods exist, renderToStream takes precedence.
import { createSSRApp } from "vue";
import { renderToString } from "vue/server-renderer";
import { renderFactory, createRoutes } from "_/front/entry/server";
import App from "@/front/App.vue";
import createRouter from "@/front/router";
const routes = createRoutes();
export default renderFactory(() => {
return {
async renderToString(url, { criticalCss }) {
const app = createSSRApp(App);
await createRouter(app, routes, { url });
const head = criticalCss
.map(({ text }) => `<style>${text}</style>`)
.join("\n");
const html = await renderToString(app);
return { head, html };
},
// Optional: implement renderToStream for streaming SSR
// async renderToStream(url, { criticalCss }) {
// // Your streaming implementation here
// // If provided, this takes precedence over renderToString
// },
};
});Default implementation - renderToString - is taking two arguments - URL and SSROptions - and should return:
head- HTML to inject into<head>(typically critical CSS)html- The rendered application markup
This approach renders pages completely and synchronously, returning the full HTML string. For advanced scenarios requiring faster time-to-first-byte or handling large pages, implement renderToStream for progressive content delivery (more on that later).
ποΈ Render Factory Arguments β
The same two arguments provided to both renderToString and renderToStream:
URL- The requested URL for server renderingSSROptionsobject
type SSROptions = {
template: string;
manifest: Record<string, SSRManifestEntry>;
criticalCss: Array<{ text: string; path: string }>;
request: IncomingMessage;
response: ServerResponse;
};| Property | Description |
|---|---|
template | The client-side index.html produced by Vite, with <!--app-head--> and <!--app-html--> markers for SSR content |
manifest | Vite's manifest.json containing the full module graph - client entries, dynamic imports, and associated CSS |
criticalCss | Route-matched CSS chunks extracted by walking the manifest graph |
request | Node.js IncomingMessage for reading headers, cookies, locale, and other request data |
response | Node.js ServerResponse for writing headers, controlling caching, issuing redirects, or streaming HTML |
Critical CSS Usage β
Each item in criticalCss exposes two properties:
text- the stylesheet content, as plain textpath- a browser-ready path to the stylesheet
You can tailor style delivery to your performance needs:
| Strategy | Benefit |
|---|---|
<style>${text}</style> | Inlines styles for the quickest first paint |
<link rel="stylesheet" href="${path}"> | Leverages browser cache across page navigations |
<link rel="preload" as="style" href="${path}"> | Warms up styles for later application |
Request/Response Access β
Exposing request and response directly supports advanced SSR patterns:
- Examine request headers (User-Agent, cookies, locale)
- Configure response headers (caching rules, redirects)
- Write HTML incrementally for streaming responses
This flexibility lets you return complete HTML via renderToString or manage the response stream directly with renderToStream.
π Stream Rendering β
For advanced use cases - such as sending HTML to the client while rendering is still in progress - the SSR factory may export a renderToStream method. Vue's server renderer supports streaming via Node and Web streams.
Below is an example implementation:
import { createSSRApp } from "vue";
import { renderToNodeStream } from "vue/server-renderer";
import { renderFactory, createRoutes } from "_/front/entry/server";
import App from "@/front/App.vue";
import createRouter from "@/front/router";
const routes = createRoutes();
export default renderFactory(() => {
return {
async renderToStream(url, { criticalCss }) {
const app = createSSRApp(App);
await createRouter(app, routes, { url });
const head = criticalCss
.map(({ text }) => `<style>${text}</style>`)
.join("\n");
// Divide template at application insertion point
const [htmlStart, htmlEnd] = template.split("<!--app-html-->");
// Send initial HTML with head content
response.write(htmlStart.replace("<!--app-head-->", head));
// Create the stream
const stream = renderToNodeStream(app);
stream.on("data", (chunk) => response.write(chunk));
stream.on("end", () => {
response.write(htmlEnd);
response.end();
});
stream.on("error", (err) => {
console.error("SSR stream error:", err);
response.statusCode = 500;
response.end();
});
},
};
});π‘ The streaming pattern and where you inject styles, preload links, or other head content depends on your HTML template structure.
KosmoJSgives you the controls - you choose the right strategy for your environment.
Streaming is particularly useful when:
- pages load large amounts of async content
- first paint time matters for user experience
- reducing server memory pressure on large HTML payloads
π¦ Static Asset Handling β
Client assets are loaded into memory when the SSR server starts and served automatically for incoming requests.
To disable this behavior, set serveStaticAssets to false:
export default renderFactory(() => {
return {
serveStaticAssets: false,
// ...
};
});With this option disabled, the server skips asset loading entirely and responds with 404 Not Found for static file requests.
This configuration is ideal for deployments where a reverse proxy such as Nginx handles static file delivery.
ποΈ Production Builds β
Trigger a production SSR build with:
pnpm buildnpm run buildyarn buildThis produces two outputs:
dist/SOURCE_FOLDER/client/ β static browser assets
dist/SOURCE_FOLDER/ssr/ β server entry bundleThe server bundle can be executed on any Node.js environment.
π§ͺ Local Testing β
Start the SSR server locally:
node dist/front/ssr/server.js --port 4000Then open:
http://localhost:4000Verify that:
- HTML is rendered server-side
- Interactivity appears after hydration
π Deployment β
Deploy behind a reverse proxy such as Nginx, Caddy, Traefik, or a managed load balancer. Serve static assets from a CDN or your hosting provider for optimal latency and throughput.
π Development Experience β
Your workflow remains fully client-side during development:
pnpm devVitedev server handles requests + HMR- No SSR server running locally
SSR is a production-only concern.
Server runtime constraints
Avoid accessing browser-only globals (window, document) in SSR mode. Use guards or client-entry hooks instead.
SSR unlocks real performance and SEO gains for Vue apps - and KosmoJS makes the setup lightweight, predictable, and aligned with modern Vue best practices.