> ## Documentation Index
> Fetch the complete documentation index at: https://knitkit.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Edge Fragment Composition with @knitkit/edge

> Stream-stitch independent HTML fragments from multiple remotes at the edge using @knitkit/edge, with automatic import-map injection and graceful fallbacks.

`@knitkit/edge` assembles a complete page at the edge from independent HTML fragments — each owned by a different team, built with a different framework (or no framework at all). It fetches all fragments in parallel, streams them into the host template in document order, and injects a negotiated import map before `</head>` so any client-side modules hydrate correctly. Because every fragment renders its own HTML, there is no shared React instance to coordinate and no "invalid hook call" risk. The package runs wherever the Web Fetch and Streams APIs are available: Cloudflare Workers, Deno Deploy, Vercel Edge Functions, and Node.js 18+.

## Install

```bash theme={null}
npm i @knitkit/edge
```

## Concepts

### Fragment placeholders

You mark insertion points in your host HTML template with `<knitkit-fragment>` custom elements:

```html theme={null}
<knitkit-fragment name="checkout">
  <!-- This inner content is the fallback rendered when the fragment fetch fails -->
  <p>Checkout is temporarily unavailable.</p>
</knitkit-fragment>
```

Use the self-closing form when you have no meaningful fallback:

```html theme={null}
<knitkit-fragment name="reviews" />
```

Each placeholder's `name` attribute maps to a `Fragment` object in the `fragments` array you pass to `composeStream`.

### The `Fragment` type

```ts theme={null}
interface Fragment {
  /** Matches the `name` of a `<knitkit-fragment>` placeholder in the template. */
  name: string;
  /** URL to fetch the fragment's HTML from. */
  src: string;
  /** Optional fetch init (headers, signal, etc.). */
  init?: RequestInit;
}
```

## Step-by-step

<Steps>
  ### Write the host template

  Create a shell HTML document that owns the page `<head>`, global navigation, and any chrome that is not owned by a remote team. Place `<knitkit-fragment>` elements where each team's content belongs. Include meaningful fallback content inside each tag — that content renders whenever the corresponding fragment server is unreachable.

  ```html theme={null}
  <!doctype html>
  <html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>My Shop</title>
  </head>
  <body>
    <header><a href="/">My Shop</a></header>

    <main>
      <knitkit-fragment name="product">
        <p>Product details are loading…</p>
      </knitkit-fragment>

      <knitkit-fragment name="reviews">
        <p>Reviews are temporarily unavailable.</p>
      </knitkit-fragment>
    </main>

    <footer>Powered by knitkit</footer>
  </body>
  </html>
  ```

  ### Call `composeStream`

  Import `composeStream` and call it with your template, the array of fragments to fetch, and an optional import map. All fragment fetches are kicked off in parallel immediately; the stream flushes each resolved fragment in document order.

  ```js theme={null}
  import { composeStream } from "@knitkit/edge";

  const stream = composeStream({
    template,
    fragments: [
      { name: "product", src: "https://product.example.com/fragment" },
      { name: "reviews", src: "https://reviews.example.com/fragment" },
    ],
    // importMap is injected before </head> automatically.
    importMap: {
      imports: {
        "@shop/analytics": "https://cdn.example.com/analytics.js",
      },
    },
  });
  ```

  If your `template` contains a `</head>` tag, `composeStream` inserts a `<script type="importmap">` tag directly before it. If no `</head>` is found, the tag is prepended to the document.

  ### Return the response

  On a real edge runtime, return a `Response` wrapping the stream. Use `composeResponse` as a shorthand — it sets `content-type: text/html; charset=utf-8` automatically.

  <CodeGroup>
    ```js Cloudflare Workers / Deno Deploy theme={null}
    import { composeResponse } from "@knitkit/edge";

    export default {
      async fetch(request) {
        return composeResponse({
          template,
          fragments: [
            { name: "product", src: "https://product.example.com/fragment" },
            { name: "reviews", src: "https://reviews.example.com/fragment" },
          ],
          importMap: { imports: { "@shop/analytics": "https://cdn.example.com/analytics.js" } },
        });
      },
    };
    ```

    ```js Node.js http (adapter) theme={null}
    import { createServer } from "node:http";
    import { Readable } from "node:stream";
    import { composeStream } from "@knitkit/edge";

    createServer((req, res) => {
      const stream = composeStream({ template, fragments, importMap });
      res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
      Readable.fromWeb(stream).pipe(res);
    }).listen(3000);
    ```
  </CodeGroup>
</Steps>

## API overview

### `composeStream(options)`

```ts theme={null}
function composeStream(options: ComposeOptions): ReadableStream<Uint8Array>
```

Returns a `ReadableStream<Uint8Array>` that flushes template text immediately and streams each fragment in document order as its fetch resolves. Use this when you need direct control over the stream — for example, to pipe it into a Node `http.ServerResponse`.

### `composeResponse(options, init?)`

```ts theme={null}
function composeResponse(options: ComposeOptions, init?: ResponseInit): Response
```

Wraps `composeStream` in a `Response` with `content-type: text/html; charset=utf-8`. The typical return value from an edge handler. You can pass a second `ResponseInit` argument to add extra headers or override the status code.

### `compose(options)`

```ts theme={null}
function compose(options: ComposeOptions): Promise<string>
```

Buffers the entire composed page into a single string. Useful for testing, pre-rendering to a CDN, or any context where you need the full HTML before proceeding.

### `parseTemplate(template)`

```ts theme={null}
function parseTemplate(template: string): Segment[]
```

Splits the template into an ordered array of `{ type: "text", value }` and `{ type: "fragment", name, fallback }` segments. Exported for unit testing your templates in isolation — you can assert on the parse tree without needing a network.

### `serializeImportMap(map)`

```ts theme={null}
function serializeImportMap(map: ImportMap): string
```

Re-exported from `@knitkit/edge` for cases where you manage the import-map tag yourself instead of passing `importMap` to `composeStream`.

## Graceful degradation

When a fragment fetch fails — network error, non-2xx status, or a timeout you set via `AbortSignal` in `init` — `composeStream` falls back to the placeholder's inner HTML. The page still renders completely; only the affected section shows the fallback content. Provide a custom error handler to override the default fallback:

```js theme={null}
composeStream({
  template,
  fragments: [
    { name: "product", src: "https://product.example.com/fragment" },
  ],
  onError(fragment, error) {
    // Return any HTML string to replace the failed fragment.
    return `<section class="error-state">
      <p>${fragment.name} is unavailable: ${error.message}</p>
    </section>`;
  },
});
```

If you do not supply `onError`, the fallback is the placeholder's inner HTML. If the placeholder is self-closing (no inner HTML), knitkit emits an HTML comment so you can see the failure in the page source without a visible UI error.

## Real gateway example

The following is drawn from `examples/edge-composition/gateway.mjs` in the knitkit repo. It adapts the Web `ReadableStream` to Node's `http` module for local development — on a real edge runtime you would return `composeResponse` directly.

```js theme={null}
// gateway.mjs
import { createServer } from "node:http";
import { Readable } from "node:stream";
import { composeStream } from "@knitkit/edge";

const port = Number(process.env.PORT ?? 5203);
const PRODUCT = process.env.PRODUCT_URL ?? "http://localhost:5204/";
const REVIEWS = process.env.REVIEWS_URL ?? "http://localhost:5205/";

const template = `<!doctype html>
<html lang="en">
<head><meta charset="utf-8" /><title>knitkit — edge-composed shop</title></head>
<body>
<h1>knitkit — independent apps composed at the edge</h1>
<knitkit-fragment name="product">loading product…</knitkit-fragment>
<knitkit-fragment name="reviews">loading reviews…</knitkit-fragment>
<footer>Each fragment is its own app (React, or no framework). No shared runtime required.</footer>
</body></html>`;

const server = createServer((req, res) => {
  if ((req.url ?? "/") !== "/") {
    res.writeHead(404);
    res.end("not found");
    return;
  }

  const stream = composeStream({
    template,
    fragments: [
      { name: "product", src: PRODUCT },
      { name: "reviews", src: REVIEWS },
    ],
    importMap: {
      imports: { "@shop/analytics": "https://cdn.example.com/analytics.js" },
    },
  });

  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
  Readable.fromWeb(stream).pipe(res);
});

server.listen(port, () =>
  console.log(`edge gateway: http://localhost:${port}`),
);
```

<Tip>
  Fragment servers must respond with CORS headers (`Access-Control-Allow-Origin`) when the gateway and fragment origins differ, or when you are running the gateway on a real edge platform that fetches from a different origin. Add `{ "Access-Control-Allow-Origin": "*" }` (or a specific origin) to every fragment server's response headers. In production, scope the header to your gateway's hostname rather than using the wildcard.
</Tip>
