Skip to content

By default, source folders use client-side rendering with Vite's stellar dev server and HMR. When you need SSR for production deployments, the SSR generator adds the necessary infrastructure while keeping your development workflow unchanged.

πŸ› οΈ Adding SSR Support ​

Selecting SSR during source folder creation enables it automatically. For folders created without SSR, enable it manually in your vite.config.ts:

vite.config.ts
ts
import solidPlugin from "vite-plugin-solid";
import devPlugin from "@kosmojs/dev";
import {
  // ...
  solidGenerator,
  ssrGenerator, 
} from "@kosmojs/generators";

import defineConfig from "../vite.base";

export default defineConfig(import.meta.dirname, {
  plugins: [
    solidPlugin({
     ssr: true // Enable SSR in Solid plugin
    }),
    devPlugin(apiurl, {
      generators: [
        // ...
        solidGenerator(),
        ssrGenerator(), 
      ],
    }),
  ],
});

πŸ“„ Server Entry Point ​

Enabling the SSR generator produces an entry/server.ts file containing the baseline server rendering setup.

Server-side renderFactory accepts a callback returning an object with rendering methods:

  • renderToString(url, { criticalCss }) - Baseline implementation rendering complete pages before transmission
  • renderToStream(url, { criticalCss }) - Advanced optional implementation enabling progressive streaming SSR
entry/server.ts
ts
import { renderToString, generateHydrationScript } from "solid-js/web";

import { renderFactory, createRoutes } from "_/front/entry/server";
import App from "@/front/App";
import createRouter from "@/front/router";

const routes = createRoutes({ withPreload: false });

export default renderFactory(() => {
  const hydrationScript = generateHydrationScript();
  return {
    async renderToString(url, { criticalCss }) {
      const router = await createRouter(App, routes, { url });
      const head = criticalCss.reduce(
        (head, { text }) => `${head}\n<style>${text}</style>`,
        hydrationScript,
      );
      const html = renderToString(() => router);
      return { head, html };
    },
  };
});

Important: The default setup provides only renderToString. Streaming SSR requires manual renderToStream implementation. When both exist, renderToStream takes priority.

How it works:

The renderToString function receives:

  • url - Requested URL requiring server rendering
  • criticalCss - Extracted critical CSS array from your components

Return requirements:

  • head - HTML for <head> injection (typically critical CSS)
  • html - Rendered application markup

The baseline renderToString approach renders pages synchronously in full, returning complete HTML strings.

Advanced scenarios - faster time-to-first-byte, large page handling - benefit from implementing renderToStream for progressive content delivery (detailed coverage follows later).

πŸŽ›οΈ Render Factory Arguments ​

Both renderToString and renderToStream takes same arguments - the current request URL and SSROptions:

ts
type SSROptions = {
  template: string;
  manifest: Record<string, SSRManifestEntry>;
  criticalCss: Array<{ text: string; path: string }>;
  request: IncomingMessage;
  response: ServerResponse;
};
PropertyDescription
templateVite's client index.html output with <!--app-head--> and <!--app-html--> placeholders ready for SSR injection
manifestThe manifest.json generated by Vite - a complete dependency graph covering client modules, dynamic imports, and CSS
criticalCssCSS chunks specific to the current route, resolved by traversing the manifest graph
requestNode.js IncomingMessage providing access to headers, cookies, locale, and more
responseNode.js ServerResponse for managing headers, caching, redirects, or progressive HTML streaming

Critical CSS Usage ​

Every criticalCss entry contains two properties:

  • text - the raw CSS content, already decoded
  • path - the asset path browsers can load directly

Choose your delivery strategy based on your performance goals:

StrategyBenefit
<style>${text}</style>Eliminates extra requests for fastest first paint
<link rel="stylesheet" href="${path}">Enables cache reuse when navigating between pages
<link rel="preload" as="style" href="${path}">Preloads styles for deferred application

Request/Response Access ​

Direct access to request and response unlocks advanced SSR capabilities:

  • Read incoming headers (User-Agent, cookies, locale)
  • Define custom response headers (caching policies, redirects)
  • Stream HTML chunks progressively

This design lets you opt for simple HTML generation (renderToString) or take full control over the response stream (renderToStream).

πŸ”€ String Rendering ​

The renderToString method is the simpler approach, suitable for most SSR use cases:

ts
renderToString(url, SSROptions): SSRStringReturn

It takes URL and SSROptions arguments and returns an SSRStringReturn object:

ts
type SSRStringReturn = {
  head?: string;  // Content for <head> section (scripts, meta tags, etc.)
  html: string;   // The rendered application HTML
};

The default implementation uses SolidJS's renderToString to generate the complete HTML in one pass, along with the hydration script for the <head> section.

Also critical CSS appended to the head to avoid render-blocking stylesheets and improve first paint performance.

🌊 Stream Rendering ​

For more advanced scenarios where you want to stream HTML to the client as it's generated, implement the renderToStream method.

It also takes URL and SSROptions as arguments and should implement app-specific rendering strategy.

A common pattern is to split the template and stream in chunks:

entry/server.ts
ts
import { renderToStream, generateHydrationScript } from "solid-js/web";

import { renderFactory, createRoutes } from "_/front/entry/server";
import App from "@/front/App";
import createRouter from "@/front/router";

const routes = createRoutes({ withPreload: false });

export default renderFactory(() => {
  const hydrationScript = generateHydrationScript();
  return {
    async renderToString(url, { criticalCss }) {
      const router = await createRouter(App, routes, { url });

      const head = criticalCss.reduce(
        (head, { text }) => `${head}\n<style>${text}</style>`,
        hydrationScript,
      );

      // Split template at the app HTML insertion point
      const [htmlStart, htmlEnd] = template.split("<!--app-html-->");

      // Send the start of HTML with head content
      response.write(htmlStart.replace("<!--app-head-->", head));

      // Create the Solid stream from the router
      const { pipe } = renderToStream(() => router);

      // Pipe the stream to the response
      pipe(response, {
        onCompleteShell() {
          // Shell is readyβ€”streaming begins
        },
        onCompleteAll() {
          // All Suspense boundaries resolved - finalize response
          response.write(htmlEnd);
          response.end();
        },
      });

      // Essential: Always call response.end() after all chunks added
      response.end();
    },
  };
});

Essential: You must call response.end() when streaming is complete. Without it, the client will wait indefinitely for more data.

Modern frameworks like SolidJS provide pipeable streams that make streaming straightforward, but the implementation details are yours to choose based on your application's needs.

πŸ“¦ 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:

entry/server.ts
ts
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.

πŸ—οΈ Building for Production ​

Build your SSR application with the standard build command:

sh
pnpm build
sh
npm run build
sh
yarn build

This creates an SSR bundle in dist/SOURCE_FOLDER/ssr/ containing an server.js file ready to run in production.

πŸ§ͺ Testing the SSR Build Locally ​

Before deploying to production, test your SSR build locally. The SSR server accepts either a port or socket argument:

Using a port:

sh
node dist/front/ssr -p 4000
# or
node dist/front/ssr --port 4000

Using a Unix socket:

sh
node dist/front/ssr -s /tmp/app.sock
# or
node dist/front/ssr --sock /tmp/app.sock

Visit http://localhost:4000 to verify your application renders correctly on the server.

πŸš€ Production Deployment ​

The SSR bundle is designed to work behind a reverse proxy like nginx or Caddy. A typical nginx configuration:

nginx
upstream ssr_backend {
  server http://127.0.0.1:4000;
  # or use a socket:
  # server unix:/tmp/app.sock;
}

server {
  listen 80;
  server_name example.com;

  location / {
    proxy_pass http://ssr_backend;
  }
}

This configuration forwards requests to your SSR server while properly handling headers and connection upgrades.

πŸ”„ Development Workflow ​

The SSR generator doesn't change your development workflow. During development:

  • Run pnpm dev as usual
  • Vite dev server handles requests with HMR
  • Client-side rendering provides instant feedback
  • Full developer experience remains unchanged

SSR only activates in production builds, giving you the best of both worlds: fast development iteration and production-ready server rendering.

πŸ’‘ Production Guidelines ​

Test SSR locally before deployment. Always run your built SSR bundle locally and verify it renders correctly before deploying to production servers.

Use streaming for large pages. If your application generates significant HTML or has long data-fetching chains, implement renderToStream for better perceived performance. Users see content faster as it streams in.

Monitor memory usage. SSR keeps Node.js processes running continuously. Monitor memory consumption and implement proper error handling to prevent memory leaks.

Leverage caching. Place a CDN or caching layer in front of your SSR server for routes that don't change frequently. This reduces server load and improves response times.

Handle errors gracefully. Implement error boundaries in your application and proper error handling in your server entry point. Server errors shouldn't crash the entire process.

Consider source folder separation over hybrid rendering. Rather than implementing complex route-level SSR/CSR switching within a single source folder, leverage KosmoJS's separation of concerns principle. Create one source folder for marketing content with SSR enabled, and another for your customer application using CSR. This architectural approach is cleaner, more maintainable, and aligns with KosmoJS's organizational philosophy - each concern gets its own space with appropriate rendering strategy.

⚠️ Limitations and Considerations ​

Browser APIs aren't available. Code that runs during SSR can't access window, document, or other browser-specific APIs. Use isServer checks or lifecycle methods that only run on the client.

Async data fetching needs coordination. SolidJS's resources and suspense work in SSR, but ensure your data fetching completes before rendering. The framework handles this, but complex async patterns require attention.

Bundle size matters differently. In SSR, initial bundle size affects server memory and startup time rather than user download time. However, the hydration bundle still downloads to clients, so optimization remains important.

State serialization requires planning. If your application has complex state, ensure it serializes correctly for hydration. SolidJS handles most cases automatically, but custom stores or non-serializable data need special attention.

Released under the MIT License.