# Add an "Open in framejs" export to an existing app

Use this when the user wants **their own** app, site, CLI, notebook or generated
HTML page to export what it is showing as a framejs frame: a shareable
`https://framejs.app/j/<id>` link that runs in any browser and embeds anywhere.
You are editing the user's codebase, not writing a one-off frame.

A working example in the wild:
[omsf/alchemy-viz](https://github.com/omsf/alchemy-viz) — its view menus carry a
**Share to the web** button (`ts/src/shared/framejs.ts`) that turns the page, a
self-contained bundle plus its data, into a frame.

## The shape of every export

1. **Build the frame** — a JSON object `{ js, inputs, og }`:
   - `js`: an ES module that draws the app into the runtime's `root` element.
   - `inputs` (optional): the data, keyed by name. Small data inline as plain
     JSON; large or already-public data as a URL DataRef
     `{ "type": "url", "value": "https://…" }`.
   - `og`: `{ title, description }` for the link preview.
2. **Send it** to framejs (one POST, no account, no API key) and get a URL back.
3. **Open or show the URL** to the person who asked for it.

Only `js` is required. The full frame format (`modules`, `definition`, …) is in
[short-url-api.md](short-url-api.md).

## 1. The frame module (`js`)

The frame is NOT the app's HTML page. It is one ES module that the framejs
runtime runs inside its own page, with a global `root` element to draw into
(rules: [coding-guide.md](coding-guide.md)). It loads the app's rendering code
one of two ways:

- **From a CDN (preferred when the code is published)** — import the app's built
  ES module from npm or GitHub via a CDN (jsDelivr, unpkg, esm.sh). The frame
  stays small. **Pin a version or commit sha**: a frame that imports `@main`
  changes under everyone who has the link.
- **Inlined** — for a page that already carries its whole bundle (a standalone
  HTML file a CLI generated, a notebook output, an unpublished build): put the
  bundle's text into `js` verbatim, before the mounting code. The bundle must be
  self-contained (no relative imports; CDN imports are fine). Large is fine —
  framejs.app stores multi-megabyte frames.

Then mount the view and feed it the data through `onInputs`, so the data is a
separate, swappable input rather than baked into the code:

```js
// Generated by <app>'s "Open in framejs" export.
import { mountView } from "https://cdn.jsdelivr.net/npm/my-viz@1.4.2/dist/my-viz.js";

root.innerHTML = "";
const host = document.createElement("div");
host.style.cssText = "width:100%;height:100%;";
root.appendChild(host);
const view = mountView(host);

export function onInputs(inputs) {
  if (inputs["payload.json"]) view.setPayload(inputs["payload.json"]);
}

export function onResize(width, height) {
  view.resize?.(width, height);
}
```

Rules that matter here (all from the coding guide):

- Never set `root.style.width/height/position`; size a child to 100%.
- Keep the top-right `140 × 64` px free of controls — the runtime's Edit button
  sits there.
- The app's UI state (camera, zoom, selection, theme) should be passed as an
  input next to the payload, or read with the runtime globals `getJson(key)` /
  `setJson(key, value)` — never `localStorage` (every frame shares one origin)
  and never hand-written `location.hash` parsing.
- Embed strings that you splice into `js` with `JSON.stringify` so quotes,
  backslashes and newlines in the data cannot break the module.

## 2. Send it

### Recommended: `POST https://framejs.io/api/frame`

Works from **anywhere**: a browser page on any origin (CORS is open), a
`file://` HTML page, a CLI, a server, Python. Creates an editable frame on
framejs.app and returns its URL.

Request body:

```json
{
  "frame": {
    "js": "…module source…",
    "inputs": { "payload.json": { "any": "json" } },
    "og": { "title": "Ligand network", "description": "Shared from my-viz" }
  }
}
```

Response (`200`): `{ "id": "<32 hex>", "url": "https://framejs.app/j/<id>" }`.
Errors are `4xx/5xx` with `{ "error": "…" }`.

To **update the same frame** later (re-export after the user changes the view),
send the same body with the `id` you got back, unchanged:
`{ "id": "<id>", "frame": … }`. An `id` that is not a uuid is rejected with a
`400`. This works while the frame is anonymous; once its owner claims it, use the API
token flow in [short-url-api.md](short-url-api.md).

Browser (a button in a web app or a standalone HTML page):

```js
export async function exportToFramejs(frame) {
  // Open the tab NOW, while still inside the click: popup blockers reject a
  // window opened after an await.
  const tab = window.open("", "_blank");
  try {
    const res = await fetch("https://framejs.io/api/frame", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ frame }),
    });
    const body = await res.json();
    if (!res.ok) throw new Error(body.error || `HTTP ${res.status}`);
    if (tab) tab.location.href = body.url;
    return body.url;
  } catch (e) {
    tab?.close();
    throw e;
  }
}
```

CLI / server (Node 18+, Deno, Bun — or the same request from Python
`requests`/`urllib`):

```js
export async function exportToFramejs(frame) {
  const res = await fetch("https://framejs.io/api/frame", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ frame }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(body.error || `HTTP ${res.status}`);
  return body.url; // print it; optionally open it in the browser
}
```

### Alternatives

- **Immutable snapshot** — `POST https://framejs.io/api/shorten/json` with the
  frame object itself as the body (`{ js, inputs, og }`, no `frame` wrapper).
  Returns `{ shortUrl: "https://framejs.io/j/<sha256>" }`. Content-addressed:
  the same frame always gives the same URL, and it never changes. It expires ~30
  days after it was last opened. CORS is open.
- **Directly to framejs.app** — `POST https://framejs.app/j/<uuid>.json` with
  the frame object as the body and a UUIDv7 you mint (dashes stripped). Fine
  from a CLI or server, where you also get the API-token flow (see
  [short-url-api.md](short-url-api.md)). From a browser it is a blind write:
  framejs.app serves no CORS, so it only goes through as `mode: "no-cors"` with
  `Content-Type: text/plain` and you cannot read the result. Prefer `/api/frame`
  in browsers.
- **No network at all** — a hash URL `https://framejs.io/#?js=<base64>` (see
  [URL encoding format](https://framejs.io/docs/ai/agent-skill#url-encoding-format)).
  Only for small code: a bundle or sizeable data makes the URL too long for
  browsers and chat apps.

## 3. Behaviour the export must have

- **Explicit and on demand.** Upload only when the user clicks the button / runs
  the command. Say what it does in the label or tooltip, e.g. "Open in framejs —
  uploads this view to framejs.app". Data leaves the machine at that moment.
- **Tell the user the link expires unless claimed.** An anonymous frame is
  temporary; opening it and claiming it with a free framejs account keeps it.
  Show this next to the link (or print it in a CLI).
- **Show the URL**, not just a new tab — the user usually wants to paste it into
  an issue, a chat or a slide.
- **Set `og.title` and `og.description`** so the link unfurls with a meaningful
  preview.
- **Fail visibly.** Offline, or a `4xx/5xx` from the API, is an error message,
  not a blank tab.

## 4. Verify before you finish

- Run the export (click the button, or run the CLI command) and **open the
  printed URL in a real browser**: the view renders, with the same data and
  state the app was showing.
- Try the largest realistic payload the app produces.
- For a standalone HTML export, open the file from disk (`file://`) and export
  from there too.
- Add a unit test for the function that builds the frame (`js`, `inputs`, `og`),
  so the export does not silently break when the app's bundle or payload format
  changes.
