Getting Started
@nano_kit/ssr is the base package for server-side rendering in Nano Kit. It provides the core rendering infrastructure — Vite manifest parsing, virtual navigation, store dehydration, and a Vite plugin — that adapter packages build on top of.
Installation
Section titled “Installation”pnpm add @nano_kit/store @nano_kit/router @nano_kit/ssryarn add @nano_kit/store @nano_kit/router @nano_kit/ssrnpm install @nano_kit/store @nano_kit/router @nano_kit/ssrApp Index File
Section titled “App Index File”Every SSR app needs a shared index file that exports routes and pages. This file is the single source of truth consumed by both the server renderer and the Vite plugin. It is also the right place to declare global TypeScript types for route names.
// src/index.tsimport { page, layout, loadable } from '@nano_kit/router'import * as Layout from './Layout.js'
export const routes = { home: '/', about: '/about', user: '/users/:id'} as const
export const pages = [ layout(Layout, [ page('home', loadable(() => import('./pages/Home.js'))), page('about', loadable(() => import('./pages/About.js'))), page('user', loadable(() => import('./pages/User.js'))) ])]
declare module '@nano_kit/router' { interface AppContext { routes: typeof routes }}Creating an Adapter
Section titled “Creating an Adapter”An adapter consists of three parts: a renderer, a client entry, and a Vite plugin.
-
Extend the Renderer
Subclass
Rendererfrom@nano_kit/ssr/rendererand implement the abstractrenderToStringmethod. Inject the framework-specificcomposefunction from your router integration by passing it tosuper().// src/renderer/index.jsimport { Renderer, headDescriptorToHtml, ROOT_ID } from '@nano_kit/ssr/renderer'import { compose } from 'your-framework-router'import { get } from '@nano_kit/store'export class FrameworkRenderer extends Renderer {constructor(options) {super({ ...options, compose })}renderToString(data) {let lang, dir, title, head = ''data.head.forEach((descriptor) => {if (descriptor.tag === 'lang') lang = get(descriptor.value) || undefinedelse if (descriptor.tag === 'dir') dir = get(descriptor.value) || undefinedelse if (descriptor.tag === 'title') title = get(descriptor.value) || undefinedelse head += headDescriptorToHtml(descriptor)})if (title) head = `<title>${title}</title>${head}`const body = yourFrameworkRenderToString(data.context)return `<html lang="${lang}" dir="${dir}"><head>${head}</head><body><div id="${ROOT_ID}">${body}</div><script>${this.dehydratedScript(data.dehydrated)}</script></body></html>`}}Also provide a default renderer template that the Vite plugin uses when the user does not supply a custom one:
// src/renderer.jsimport { FrameworkRenderer } from 'your-framework-ssr/renderer'import { routes, pages } from 'virtual:app-index'export const renderer = new FrameworkRenderer({base: import.meta.env.BASE_URL,manifestPath: import.meta.env.MANIFEST,routes,pages}) -
Create the Client Entry
Wrap the base
readyfunction from@nano_kit/ssr/client, injecting the framework-specificrouter:// src/client/index.jsimport { router } from 'your-framework-router'import { ready as baseReady } from '@nano_kit/ssr/client'export * from '@nano_kit/ssr/client'export function ready(options) {return baseReady({ ...options, router })}Also provide a default client template that the Vite plugin uses when the user does not supply a custom one:
// src/client.jsimport { ROOT_ID, ready } from 'your-framework-ssr/client'import { routes, pages } from 'virtual:app-index'ready({ routes, pages }).then((context) => {// hydrate the app — framework-specificyourFrameworkHydrate(document.getElementById(ROOT_ID), context)}) -
Create the Vite Plugin
Pass a
SsrPluginAdapterto the baseSsrPluginfrom@nano_kit/ssr/vite-plugin. The adapter tells the plugin how to load the default client and renderer templates:// src/vite-plugin/index.jsimport path from 'node:path'import fs from 'node:fs/promises'import SsrPlugin from '@nano_kit/ssr/vite-plugin'const adapter = {clientPath: 'virtual-client.js',rendererPath: 'virtual-renderer.js',loadClient() {return fs.readFile(path.join(import.meta.dirname, '..', 'client.js'), 'utf-8')},loadRenderer() {return fs.readFile(path.join(import.meta.dirname, '..', 'renderer.js'), 'utf-8')}}export default function FrameworkSsrPlugin(options) {return SsrPlugin(options, adapter)}The plugin automatically handles building both the client bundle and the SSR renderer bundle.
Usage in an App
Section titled “Usage in an App”Once an adapter package is published, end users configure their project like this:
// vite.config.jsimport { defineConfig } from 'vite'import ssr from 'your-framework-ssr/vite-plugin'
export default defineConfig({ plugins: [ssr({ index: 'src/index.js' })]})vite dev— starts the development server with SSR rendering handled in-process via Vite’s dev middleware.vite build— produces two output directories:dist/client/(browser assets) anddist/renderer/(SSR renderer bundle).
For production, you need to write your own HTTP server that imports the renderer bundle and serves the HTML:
// server.jsimport { renderer } from './dist/renderer/index.js'
app.get('*', async (req, res) => { const result = await renderer.render(req.url)
if (result.redirect) { return res.redirect(result.statusCode, result.redirect) }
if (result.html !== null) { return res.status(result.statusCode).send(result.html) }
res.status(result.statusCode).send('Not Found')})Next Steps
Section titled “Next Steps”@nano_kit/react-ssr— The React adapter built with this package.- Router SSR —
Stores$,Head$, and other page-level SSR hooks.