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'

// Pass your fallthrough. The proxy then returns a Response for every request,
// and the HTML half of each negotiated URL gets `Vary: Accept` — see below for
// why that matters more than it looks.
const gateway = createGatewayProxy(gatewayConfig, () => NextResponse.next())

export default gateway

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 with a ?? 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, so there is no classification and no event. If you widen sources later, widen the matcher in the same change.

Optional: a dedicated markdown route

A stable always-markdown URL, such as 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, which 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.