# [SvelteKit, Nuxt, Astro, React Router and Hono](https://rebilder.com/docs/adapters/frameworks)

> Mount the gateway in SvelteKit, Nuxt, Astro, React Router, Remix or Hono with one fetch middleware. Agents get markdown; everyone else gets your page.

- **Updated:** 2026-09-28
- **Author:** Rebilder
- **Section:** Adapters
- **Description:** Mount the gateway in SvelteKit, Nuxt, Astro, React Router, Remix or Hono with one fetch middleware. Agents get markdown; everyone else gets your page.
- **Publisher:** Rebilder

## One middleware for fetch-based frameworks

Most modern frameworks hand their middleware a web-standard `Request` and a `next()` that produces the page. `@rebilder/gateway/fetch` fits that shape, so one function covers all of them. It adds no dependency and imports nothing from any framework.

gateway.ts

```
import { createFetchMiddleware } from '@rebilder/gateway/fetch'
import { gatewayConfig } from './gateway-config'

// Build once, at module scope.
const gateway = createFetchMiddleware(gatewayConfig)

// In any middleware that receives a Request and a next():
//   return gateway(request, () => next())
```

- An agent asking for markdown on a URL your sources answer gets the markdown response, and `next` is never called.
- Everything else calls `next()` once and gets its response back with `Vary: Accept` added, plus a `Link: rel="alternate"` header when your `match` router confirms the URL has a source.
- A gateway failure falls through to `next()`. An error thrown by your own page propagates unchanged.

Use the same `gatewayConfig` as every other adapter; see [Sources](/docs/sources). Run `npx rebilder init` in your project to scaffold the right snippet for your framework.

## SvelteKit

src/hooks.server.ts

```
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit'
import { createFetchMiddleware } from '@rebilder/gateway/fetch'
import { gatewayConfig } from '$lib/gateway-config'

const gateway = createFetchMiddleware(gatewayConfig)

export const handle: Handle = ({ event, resolve }) => gateway(event.request, () => resolve(event))
```

Put the config in `src/lib/gateway-config.ts`. If you already export a `handle`, combine the two with `sequence` from `@sveltejs/kit/hooks`, gateway first.

## Nuxt and Nitro

server/middleware/rebilder.ts

```
// server/middleware/rebilder.ts (Nuxt and Nitro; h3 helpers are auto-imported)
import { handleRequest, negotiationHeaders } from '@rebilder/gateway/fetch'
import { gatewayConfig } from '../utils/gateway-config'

export default defineEventHandler(async (event) => {
  if (event.method !== 'GET' && event.method !== 'HEAD') return
  const served = await handleRequest(toWebRequest(event), gatewayConfig)
  if (served) return served
  for (const [name, value] of negotiationHeaders(gatewayConfig, getRequestURL(event))) {
    appendResponseHeader(event, name, value)
  }
})
```

A Nitro middleware cannot wait for the page it runs before, so this recipe uses `handleRequest` directly. It returns markdown to agents. For everyone else it appends the negotiation headers from `negotiationHeaders` and returns nothing, and Nuxt renders the page as usual. It handles `GET` and `HEAD` only, so request bodies stay untouched for your own routes.

## Astro

src/middleware.ts

```
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
import { createFetchMiddleware } from '@rebilder/gateway/fetch'
import { gatewayConfig } from './gateway-config'

const gateway = createFetchMiddleware(gatewayConfig)

export const onRequest = defineMiddleware((context, next) => gateway(context.request, () => next()))
```

> **On-demand pages only** Astro runs middleware per request only on pages rendered on demand: an adapter, plus `output: "server"` or `export const prerender = false` on the page. Prerendered pages are static files, so serve their markdown with an edge recipe such as [Cloudflare](/docs/adapters/cloudflare) or [Netlify](/docs/adapters/runtimes#netlify) instead.

## React Router

app/root.tsx

```
// app/root.tsx
import type { Route } from './+types/root'
import { createFetchMiddleware } from '@rebilder/gateway/fetch'
import { gatewayConfig } from './gateway-config.server'

const gateway = createFetchMiddleware(gatewayConfig)

export const middleware: Route.MiddlewareFunction[] = [({ request }, next) => gateway(request, next)]
```

Route middleware is always on in React Router 8. On React Router 7.9 or later, enable it first:

react-router.config.ts

```
// react-router.config.ts (React Router 7.9 or later; version 8 needs no flag)
import type { Config } from '@react-router/dev/config'

export default {
  future: { v8_middleware: true },
} satisfies Config
```

The `.server` suffix on the config file keeps your resolvers out of the browser bundle.

## Remix v2

Remix v2 has no middleware, so run the gateway as Express middleware in front of Remix with a custom server. It uses the [Node adapter](/docs/adapters/node).

server.ts

```
// server.ts: Remix v2 with a custom Express server
import express from 'express'
import { createRequestHandler } from '@remix-run/express'
import { createGatewayMiddleware } from '@rebilder/gateway/node'
import { gatewayConfig } from './app/gateway-config.server'

const app = express()
app.use(createGatewayMiddleware(gatewayConfig)) // before Remix
app.all('*', createRequestHandler({ build: await import('./build/server/index.js') }))
app.listen(3000)
```

## Hono

src/index.ts

```
import { Hono } from 'hono'
import { createFetchMiddleware } from '@rebilder/gateway/fetch'
import { gatewayConfig } from './gateway-config'

const gateway = createFetchMiddleware(gatewayConfig)
const app = new Hono()

app.use(async (c, next) => {
  let passed = false
  const res = await gateway(c.req.raw, async () => {
    passed = true
    await next()
    return c.res
  })
  if (!passed) return res // markdown, answered before any route ran
  c.res = undefined // clear first, so Hono does not copy the old headers over ours
  c.res = res
})

// ...your routes
```

Register it before your routes. Hono ignores a response returned after `next()` has run, so the recipe assigns `c.res` instead, clearing it first so the new `Vary` header is kept. The same code runs on Workers, Bun, Deno and Node.

## Check it

Request a page your sources answer twice, once as an agent and once as a browser. The first returns markdown; the second returns your page with `Vary: Accept`. [Troubleshooting](/docs/troubleshooting) covers the common misses.

terminal

```
curl -H "Accept: text/markdown" https://your-site.example/services/bike-fitting
curl -I https://your-site.example/services/bike-fitting
```