Skip to content
Vincle

Loading…

    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.