Skip to content

Frontend & recipient links

A bundled component is an ES module with one exported function:

export async function mount(element, props, context) {
// Render into element. Return after initial setup is complete.
}

Typeroll calls mount once per block instance. The runtime context is scoped to that mount and contains:

  • protocol, runtime, Extension, installation, and component identifiers;
  • preview, which is true only inside an isolated editor preview;
  • the installation’s public configuration;
  • url.get(), url.has(), and one-time url.consume() accessors;
  • an in-memory navigation object;
  • site.url() and site.navigate() for root-relative Typeroll page paths;
  • installation-scoped storage.session and storage.local JSON storage;
  • api.fetch() for direct calls to declared provider routes;
  • forms.has(), forms.list(), and forms.submit() for approved form bindings.

Build a native lead tool on Typeroll Forms

Section titled “Build a native lead tool on Typeroll Forms”

Create the receiving form in the site’s ordinary Forms module, then declare its ID in the component manifest and request forms:submit. The bespoke frontend can calculate, branch, animate, or collect data however it wants and submit only the final fields:

const result = await context.forms.submit("lead", {
name: "Ada Lovelace",
email: "ada@example.com",
team_size: 12,
estimated_monthly_price: 4800,
});
if (result.ok === false) {
// Map result.errors to the custom interface.
} else if (result.done) {
// Show the custom confirmation state.
}

Typeroll adds its signed token, honeypot protocol fields, and proof of work. The request goes directly to the absolute Forms endpoint embedded by the CMS. Typeroll Cloud sites use the hosted Forms service and self-hosted sites use their own Forms endpoint. No Function or proxy is installed in the customer’s static hosting project. Submissions, notifications, and configured webhooks behave exactly like submissions from a standard core/form block.

The editor preview exposes binding names but intentionally rejects real submissions. Test writes on a deployed test page, where they appear in the normal Forms submission inbox.

An Extension can implement several screens inside one block:

context.navigation.subscribe((view) => render(view));
context.navigation.navigate("confirmation");

Navigation belongs to the mounted component. It does not create a new Typeroll page or URL path, and the component’s captured runtime context remains available until that mount is destroyed. A full reload starts a new mount and follows the provider’s declared session strategy.

Use the site-navigation API instead of assigning a root-relative path directly:

context.storage.session.set("quote-draft", {
from: "Storgatan 1",
to: "Kungsgatan 2",
});
context.site.navigate("/flyttfirmeoffert/");

On a published site, site.url(path) resolves against the site’s current origin. In a navigable preview it resolves to the matching preview route and preserves any signed preview ticket. Only root-relative paths are accepted.

Both storage areas accept JSON-compatible values through get, set, and remove. They are namespaced by installation. On a published site they use the browser’s corresponding Web Storage area. Because an isolated preview has an opaque origin, preview storage is tab-scoped and survives navigation inside that preview; preview local intentionally has the same lifetime as preview session. Values are not added to the URL, referrer, generated HTML, or Typeroll request body.

Preview changes the page that initiates browser requests: bundled code runs from the portal preview rather than the published site’s domain. A third-party browser key restricted by HTTP referrer or page origin must allow the portal’s preview origin to work there. Prefer a separate, narrowly restricted preview credential; do not loosen the production key just to make preview requests pass.

Declare only the URL inputs that the component needs:

{
"url_context": {
"query": [
{
"name": "quote",
"expose_as": "quote_token",
"sensitive": true,
"consume": true,
"max_length": 256,
"pattern": "^[A-Za-z0-9_-]+$"
}
]
}
}

The provider can email several recipients links to the same static Typeroll page, for example /offer/?quote=opaque-value. At mount time, Typeroll:

  1. reads only declared query, fragment, path, or raw-query inputs;
  2. rejects values that exceed the declared length or pattern;
  3. captures values in the component’s private runtime closure;
  4. removes inputs marked consume with history.replaceState; and
  5. exposes the captured value through context.url.

The token is not initial block state and is never inserted into generated HTML, saved page data, Typeroll’s datastore, or diagnostics.

const quoteToken = context.url.consume("quote_token");
await context.api.fetch("/quotes/approve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: quoteToken }),
});

The provider remains responsible for generating a cryptographically random token and enforcing expiry, revocation, intended recipient, allowed actions, and replay behavior. A difficult-to-guess URL is a bearer credential, not an authorization model by itself.

Manifest v3 declares api.base_url, routes and an authentication mode. context.api.fetch() resolves only declared relative paths, sends the browser request directly to that provider URL with credentials: omit, and does not pass through Typeroll or Cloudflare Pages Functions.

With authentication: "signed_installation", the runtime first obtains a five-minute JWT from the CMS issuer and adds it as X-Typeroll-Extension-Token. The token confirms the enabled installation and requesting site origin. It is not a visitor login and does not authorize a quote, booking or wedding plan; the provider must still validate its own recipient token and requested action. The provider must allow the installed site origin in CORS and accept X-Typeroll-Extension-Token in its preflight policy.

An embedded_app runs in a sandboxed provider-origin iframe. It receives a versioned typeroll.extension.init message and can request resize or internal navigation with the matching protocol messages. Validate message origin, source window, protocol version, installation ID, and component ID on every message.

For the first init message, require event.source === window.parent and an HTTPS event.origin, then lock that exact origin for the frame session. Do not derive it from document.referrer; the Typeroll host intentionally applies a no-referrer policy to Extension iframes.

An embedded app receives binding names in typeroll.extension.init but never receives the signed form tokens. It can request a submission with typeroll.extension.form.submit; the host responds with typeroll.extension.form.result after validating the frame, origin, installation, component, binding, and request IDs.

For declared provider routes, the frame sends typeroll.extension.api.request with a unique request_id, relative path, and optional method, headers, and body. The site host performs the same direct, route-constrained request as context.api.fetch() and returns a serializable typeroll.extension.api.result. This browser-side message bridge does not relay traffic through Typeroll infrastructure or a hosting Function; the network request still goes straight from the published site to the provider API. Responses above 1 MiB fail closed.

Next, implement the provider backend and admin SSO.