# Nitro Renderer

> Use a renderer to handle all unmatched routes with custom HTML or a templating system.

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.

## Configuration

The renderer is configured using the `renderer` option in your Nitro config:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  renderer: {
    template: './index.html',  // Path to HTML template file
    handler: './renderer.ts',  // Path to custom renderer handler
    static: false,             // Treat template as static HTML (no rendu processing)
  }
})
```

| Option | Type | Description |
| --- | --- | --- |
| `template` | `string` | Path to an HTML file used as the renderer template. |
| `handler` | `string` | Path to a custom renderer handler module. |
| `static` | `boolean` | When `true`, skips rendu template processing and serves the HTML as-is. Auto-detected based on template syntax when not set. |

Set `renderer: false` in the config to explicitly disable the renderer entirely (including auto-detection of `index.html`).

## 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.

<code-group>

```html [index.html]
<!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>
```

```ts [routes/api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  return { hello: "API" };
});
```
</code-group>

<tip>

When `index.html` is detected, Nitro will automatically log in the terminal: `Using index.html as renderer template.`
</tip>

With this setup:

- `/api/hello` → Handled by your API routes
- `/about`, `/contact`, etc. → Served with `index.html`

### Custom HTML file
You can specify a custom HTML template file using the `renderer.template` option in your Nitro configuration.

<code-group>

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  renderer: {
    template: './app.html'
  }
})
```

```html [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>
```
</code-group>

### Static templates

By default, Nitro auto-detects whether your HTML template contains [rendu](#hypertext-preprocessor-experimental) syntax. If it does, the template is processed dynamically on each request. If it doesn't, it's served as static HTML.

You can override this behavior with the `static` option:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  renderer: {
    template: './index.html',
    static: true // Force static serving, skip template processing
  }
})
```

In production, static templates are inlined into the server bundle and served directly for optimal performance.

### Hypertext Preprocessor (experimental)

Nitro uses [rendu](https://github.com/h3js/rendu) Hypertext Preprocessor, which provides a simple and powerful way to create dynamic HTML templates with JavaScript expressions.

#### Output expressions

- `{{ expression }}` — HTML-escaped output
- `{{{ expression }}}` or `<?= expression ?>` — raw (unescaped) output

```html
<h1>Hello {{ $URL.pathname }}</h1>
<div>{{{ '<strong>raw html</strong>' }}}</div>
```

#### Control flow
Use `<? ... ?>` for JavaScript control flow:

```html
<? if ($METHOD === 'POST') { ?>
  <p>Form submitted!</p>
<? } else { ?>
  <form method="POST">
    <button type="submit">Submit</button>
  </form>
<? } ?>

<ul>
<? for (const item of ['a', 'b', 'c']) { ?>
  <li>{{ item }}</li>
<? } ?>
</ul>
```

#### Server scripts

Use `<script server>` to execute JavaScript on the server:

```html
<script server>
  const data = await fetch('https://api.example.com/data').then(r => r.json());
</script>
<pre>{{ JSON.stringify(data) }}</pre>
```

#### Streaming content

Use the `echo()` function for streaming content. It accepts strings, functions, Promises, Response objects, or ReadableStreams:

```html
<script server>
  echo("Loading...");
  echo(async () => fetch("https://api.example.com/data"));
</script>
```

#### Global variables

Access request context within templates:

| Variable | Description |
| --- | --- |
| `$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 |

#### Built-in functions

| Function | Description |
| --- | --- |
| `htmlspecialchars(str)` | Escape HTML characters (automatically applied in `{{ }}` syntax) |
| `setCookie(name, value, options?)` | Set a cookie in the response |
| `redirect(url)` | Redirect the user to another URL |
| `echo(content)` | Stream content to the response |

```html [index.html]
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Dynamic template</title>
  </head>
  <body>
    <h1>Hello {{ $REQUEST.url }}</h1>
    <p>Welcome, <?= $COOKIES["user"] || "Guest" ?>!</p>
    <script server>
      setCookie("visited", "true", { maxAge: 3600 });
    </script>
  </body>
</html>
```

<read-more></read-more>

## Custom renderer handler

For more complex scenarios, you can create a custom renderer handler that programmatically generates responses.

The handler is a default export function that receives an H3 event object. You can access the incoming `Request` via `event.req`:

```ts [renderer.ts]
export default function renderer({ req }: { req: Request }) {
  const url = new URL(req.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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  renderer: {
    handler: './renderer.ts'
  }
})
```

<note>

When `renderer.handler` is set, it takes full control of rendering. The `renderer.template` option is ignored.
</note>

## Renderer priority

The renderer always acts as a catch-all route (`/**`) and has the **lowest priority**. This means:

1. Specific API routes are matched first (e.g., `/api/users`)
2. Specific server routes are matched next (e.g., `/about`)
3. The renderer catches everything else

```md
api/
  users.ts        → /api/users (matched first)
routes/
  about.ts        → /about (matched second)
renderer.ts         → /** (catches all other routes)
```

<warning>

If you define a catch-all route (`[...].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.
</warning>

<read-more></read-more>

## Vite integration

When using Nitro with Vite, the renderer integrates with Vite's build pipeline and dev server.

### Development mode

In development, the renderer template is read from disk on each request, so changes to `index.html` are reflected immediately without restarting the server. Vite's `transformIndexHtml` hook is applied to inject HMR client scripts and other dev-time transforms.

### SSR with `<!--ssr-outlet-->`

When using Vite environments with an `ssr` service, you can add an `<!--ssr-outlet-->` comment to your `index.html`. Nitro will replace it with the output from your SSR entry during rendering:

```html [index.html]
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>SSR App</title>
  </head>
  <body>
    <div id="app"><!--ssr-outlet--></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```

### Production build

During production builds, Vite processes the `index.html` through its build pipeline (resolving scripts, CSS, and other assets), then Nitro inlines the transformed HTML into the server bundle.

## Use Cases

### Single-Page Application (SPA)

Serve your SPA's `index.html` for all routes to enable client-side routing:

> [!TIP]
> This is the default behavior of Nitro when used with Vite.
