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
index.html
Auto-detected
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 and use defineRenderHandler
to define your custom rendering logic:
import { defineRenderHandler } from "nitro/runtime";
export default defineRenderHandler((event) => {
return {
body: `<!DOCTYPE html>
<html>
<head>
<title>Custom Renderer</title>
</head>
<body>
<h1>Hello from custom renderer!</h1>
<p>Current path: ${event.path}</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: {
entry: './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.Use Cases
Single-Page Application (SPA)
Serve your SPA's index.html
for all routes to enable client-side routing: