Documentation menu

SvelteKit, Nuxt, Astro, React Router and Hono

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

Last updated

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. 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()))

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.

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