Documentation menu

Next.js adapter

Mount the gateway as Next middleware (@rebilder/gateway/next) — plus an optional always-markdown route for previews and agent permalinks.

How it mounts

The adapter imports nothing from next — Next middleware/proxy handlers and app-router route handlers speak web-standard Request/Response, so standard types are the whole contract. This repo’s apps use the Next 16 proxy.ts convention; the identical code works in a Next ≤15 middleware.ts.

terminal
npm install @rebilder/gateway        # pnpm add / yarn add

The 2-line integration

lib/gateway-config.ts
// lib/gateway-config.ts — wire the gateway to your source of truth
import type { GatewayConfig } from '@rebilder/gateway'
import { getProduct, getPolicies, getCollection } from './catalog' // your code

export const gatewayConfig: GatewayConfig = {
  storeId: 'store_123',
  sources: {
    product:  (url) => getProduct(url.pathname),      // null when not a PDP
    policies: (url) => getPolicies(url.pathname),
    catalog:  (url) => getCollection(url.pathname),
  },
  onEvent: (event) => { /* queue to your analytics sink; fire-and-forget */ },
}
proxy.ts
// proxy.ts (Next 16) — middleware.ts on Next ≤15 is identical
import { NextResponse } from 'next/server'
import { createGatewayProxy } from '@rebilder/gateway/next'
import { gatewayConfig } from './lib/gateway-config'

const gateway = createGatewayProxy(gatewayConfig)

export default async function proxy(req: Request) {
  return (await gateway(req)) ?? NextResponse.next()
}

export const config = { matcher: ['/products/:path*', '/policies/:path*', '/collections/:path*'] }

A Response short-circuits with markdown; null continues to your HTML pipeline unchanged — humans, crawlers, protocol routes, URLs no source matched, and sources that threw all pass through. A thrown source never breaks your site: errors are contained, the event still fires, the request falls through to HTML.

First live integration (dogfood): trymumm.com wires exactly this in its proxy.ts — gateway first, ?? updateSession(request) fallthrough — proving the compose-with-existing-middleware pattern in production.

Matcher scope

Scope config.matcher to the paths your sources can answer (/products/:path*, /policies/:path*, /collections/:path* in the example). Requests outside the matcher never reach the gateway at all — no classification, no event. If you widen sources later, widen the matcher in the same change.

Optional: a dedicated markdown route

A stable always-markdown URL — an agent permalink, or the "what agents see" preview. It renders markdown for any requester and returns 404 JSON ({ "error": "not_found" }) when no source matches:

app/md/[[...path]]/route.ts
// app/md/[[...path]]/route.ts
import { createGatewayRouteHandler } from '@rebilder/gateway/next'
import { gatewayConfig } from '../../../lib/gateway-config'

export const GET = createGatewayRouteHandler(gatewayConfig, { stripPrefix: '/md' })

stripPrefix removes the route prefix (whole path segments only) before consulting your sources, so /md/products/x resolves against sources keyed by canonical paths (/products/x). This route is its own URL, so it doesn’t conflict with the cloaking guardrail — the guardrail is about serving different substance on the *same* URL.

llms.txt route

The same subpath export ships createLlmsTxtRouteHandler for serving a deterministic llms.txt from your gateway config — usage, options, and the honest effectiveness caveat live on the llms.txt page.