Nitro Renderer
The renderer is a special handler in Nitro that catches all routes that don't match any specific API or route handler. It's commonly used for server-side rendering (SSR), serving single-page applications (SPAs), or creating custom HTML responses.
HTML template
Auto-detected index.html
By default, Nitro automatically looks for an index.html file in your project src dir.
If found, Nitro will use it as the renderer template and serve it for all unmatched routes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Vite + Nitro App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
import { defineHandler } from "nitro/h3";
export default defineHandler((event) => {
return { hello: "API" };
});
index.html is detected, Nitro will automatically log in the terminal: Using index.html as renderer template.With this setup:
/api/hello→ Handled by your API routes/about,/contact, etc. → Served withindex.html
Custom HTML file
You can specify a custom HTML template file using the renderer.template option in your Nitro configuration.
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
renderer: {
template: './app.html'
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Custom Template</title>
</head>
<body>
<div id="root">Loading...</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Hypertext Preprocessor (experimental)
Nitro uses rendu Hypertext Preprocessor, which provides a simple and powerful way to create dynamic HTML templates with JavaScript expressions.
You can use special delimiters to inject dynamic content:
{{ content }}to output HTML-escaped content{{{ content }}}or<?= expression ?>to output raw (unescaped) content<? ... ?>for JavaScript control flow
It also exposes global variables:
$REQUEST: The incoming Request object$METHOD: HTTP method (GET, POST, etc.)$URL: Request URL object$HEADERS: Request headers$RESPONSE: Response configuration object$COOKIES: Read-only object containing request cookies
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dynamic template</title>
</head>
<body>
<h1>Hello {{ $REQUEST.url }}</h1>
</body>
</html>
Custom renderer handler
For more complex scenarios, you can create a custom renderer handler that programmatically generates responses.
Create a renderer file to define your custom rendering logic:
export default function renderer({ req, url }: { req: Request; url: URL }) {
return new Response(
/* html */ `<!DOCTYPE html>
<html>
<head>
<title>Custom Renderer</title>
</head>
<body>
<h1>Hello from custom renderer!</h1>
<p>Current path: ${url.pathname}</p>
</body>
</html>`,
{ headers: { "content-type": "text/html; charset=utf-8" } }
);
}
Then, specify the renderer entry in the Nitro config:
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
renderer: {
handler: './renderer.ts'
}
})
Renderer priority
The renderer always acts as a catch-all route (/**) and has the lowest priority. This means:
Specific API routes are matched first (e.g., /api/users)
Specific server routes are matched next (e.g., /about)
The renderer catches everything else
api/
users.ts → /api/users (matched first)
routes/
about.ts → /about (matched second)
renderer.ts → /** (catches all other routes)
[...].ts) in your routes, Nitro will warn you that the renderer will override it. Use more specific routes or different HTTP methods to avoid conflicts.Server-Side Rendering (SSR)
Nitro supports full server-side rendering through Vite's environment API. With SSR, Nitro renders your application to HTML on the server, sends it to the browser, and the client hydrates the page to make it interactive.
This requires two entry files:
- Server entry (
entry-server) — renders the app to HTML on the server. - Client entry (
entry-client) — hydrates the server-rendered HTML in the browser.
Auto-detected entry points
Nitro automatically detects entry-server and entry-client files in your project's app/, src/, or root directory. No Vite configuration is needed when you follow this convention.
app/
entry-server.ts ← auto-detected as SSR entry
entry-client.ts ← auto-detected as client entry
routes/
api/hello.ts
ℹ Using app/entry-server.ts as vite ssr entry.
ℹ Using app/entry-client.ts as vite client entry.
Supported file extensions: .ts, .js, .mts, .mjs, .tsx, .jsx.
Server entry
The server entry must export an object with a fetch method that receives a Request and returns a Response:
export default {
async fetch(req: Request): Promise<Response> {
const html = renderAppToHTML(req);
return new Response(html, {
headers: { "Content-Type": "text/html;charset=utf-8" },
});
},
};
Client entry
The client entry runs in the browser and hydrates the server-rendered HTML:
import { hydrate } from "my-framework";
import { App } from "./app";
hydrate(document.querySelector("#app"), App);
Asset management with ?assets imports
In production, Nitro collects CSS and JS assets from each entry point. Use the ?assets query to import asset manifests in the server entry:
import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";
export default {
async fetch(req: Request) {
const assets = clientAssets.merge(serverAssets);
const html = `<!DOCTYPE html>
<html>
<head>
${assets.css.map((attr) => `<link rel="stylesheet" href="${attr.href}" />`).join("\n")}
${assets.js.map((attr) => `<link rel="modulepreload" href="${attr.href}" />`).join("\n")}
<script type="module" src="${assets.entry}"></script>
</head>
<body>
<div id="app">${await renderToString(req)}</div>
</body>
</html>`;
return new Response(html, {
headers: { "Content-Type": "text/html;charset=utf-8" },
});
},
};
The ?assets=client suffix tells Nitro to collect assets from the client environment, while ?assets=ssr collects assets from the SSR environment (such as CSS imported only in server components). The merge() method combines them into a single manifest with:
assets.entry— the client entry script URLassets.css— an array of stylesheet attributes ({ href })assets.js— an array of module preload attributes ({ href })
Framework examples
See working SSR examples for popular frameworks:
| Framework | Example |
|---|---|
| React | vite-ssr-react |
| Vue + Vue Router | vite-ssr-vue-router |
| Solid | vite-ssr-solid |
| Preact | vite-ssr-preact |
Use Cases
Single-Page Application (SPA)
Serve your SPA's index.html for all routes to enable client-side routing:
Server-Side Rendering (SSR)
See Server-Side Rendering for more details.