serve
Builds a Response from a page function, through renderToStream, with
optional content negotiation (HTMX).
Signature#
import { serve } from "@vincle/flow/http";
function serve( req: Request, page: (n: Negotiation) => JSX.Element, adapter: StreamingAdapter, opts?: FlowOptions & ResponseInit & { negotiate?: Negotiate; mode?: "full" | "fragment" },): Promise<Response>;Parameters#
| Param | Type | Description |
|---|---|---|
req |
Request |
Incoming HTTP request |
page |
(n: Negotiation) => JSX.Element |
Page component function |
adapter |
StreamingAdapter |
Adapter for encoding fragments |
opts |
FlowOptions & ResponseInit & { negotiate?: Negotiate; mode?: "full" | "fragment" } |
Optional flow options, headers, mode |
Options are validated at the call, before anything renders: see Error handling.
Usage#
import { serve } from "@vincle/flow/http";import { NativeAdapter } from "@vincle/flow/adapters";
Bun.serve({ port: 3000, fetch(req) { return serve(req, () => <HomePage />, NativeAdapter); },});With HTMX negotiation#
import { serve, negotiateHtmx } from "@vincle/flow/http";import { HtmxAdapter } from "@vincle/flow/adapters";
Bun.serve({ port: 3000, fetch(req) { return serve(req, () => <Page />, HtmxAdapter, { negotiate: negotiateHtmx, }); },});HTMX sends HX-Target to indicate which fragment a request expects. The negotiator extracts it and sets Vary: HX-Target so shared caches never serve a fragment response to a full-page navigation.
With custom negotiation#
Write your own negotiator for any client library that uses custom headers:
import type { Negotiate } from "@vincle/flow";
const negotiateMyLib: Negotiate = (req) => { const fragment = req.headers.get("X-Fragment-Id") ?? undefined; return { headers: { Vary: "X-Fragment-Id" }, target: fragment, };};The Vary header is unioned with existing Vary values (not overwritten), so multiple negotiators or manual headers never corrupt shared-cache keys.
With custom error handler#
serve(req, () => <Page />, NativeAdapter, { onError: (error, { id, kind }) => { console.error(`Fragment ${id} failed:`, error); return <p>Unavailable</p>; },});With headers#
serve(req, () => <Page />, NativeAdapter, { headers: { "X-Custom": "value" }, status: 200,});Options#
| Option | Type | Default | Description |
|---|---|---|---|
signal |
AbortSignal |
N/A | Abort all pending renders |
onError |
(error, info) => JSX.Element | void |
N/A | Per-fragment error handler |
defaultTimeout |
number |
N/A | Per-fragment timeout (ms) |
negotiate |
(req: Request) => Negotiation |
N/A | Extract per-request hints |
mode |
"full" | "fragment" |
"full" |
Render full page or fragment-only |
headers |
HeadersInit |
N/A | Additional response headers |
status |
number |
200 |
Response status code |
statusText |
string |
N/A | Response status text |
The response body is a stream: the shell is flushed immediately, the browser starts parsing HTML while fragments are still rendering. The content-type header is set to text/html; charset=utf-8 automatically.