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.
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
nextis never called. - Everything else calls
next()once and gets its response back withVary: Acceptadded, plus aLink: rel="alternate"header when yourmatchrouter 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
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 (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
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
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 7.9 or later; version 8 needs no flag)
import type { Config } from '@react-router/dev/config'
export default {
future: { v8_middleware: true },
} satisfies ConfigThe .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: 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
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 routesRegister 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.
curl -H "Accept: text/markdown" https://your-site.example/services/bike-fitting
curl -I https://your-site.example/services/bike-fitting