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 istrueonly inside an isolated editor preview;- the installation’s public configuration;
url.get(),url.has(), and one-timeurl.consume()accessors;- an in-memory
navigationobject; site.url()andsite.navigate()for root-relative Typeroll page paths;- installation-scoped
storage.sessionandstorage.localJSON storage; api.fetch()for direct calls to declared provider routes;forms.has(),forms.list(), andforms.submit()for approved form bindings;handoff.read()for the values a visitor typed into a native navigation form on the previous page (runtime 0.43.0+).
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.
In previews (context.preview is true) forms.submit() sends nothing and
resolves { v: 1, ok: true, done: true, preview: true }, so the whole flow can
be reviewed without creating submissions; unknown bindings and non-object data
are still rejected. Test real writes on a deployed test page, where they appear
in the normal Forms submission inbox.
Navigate without changing the page path
Section titled “Navigate without changing the page path”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.
Navigate between Typeroll pages
Section titled “Navigate between Typeroll pages”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.
Receive values from a navigation form
Section titled “Receive values from a navigation form”A native Navigation inputs block (core/navigation_form) in navigate mode
lets a visitor start on one page, for example by typing two addresses and a
floor area, and continue to the page that holds your component. The values stay
in the browser tab. Your component receives them with context.handoff:
const handoff = context.handoff ? await context.handoff.read() : null;
render({ from: handoff?.values.address_from ?? "", fromPostalCode: handoff?.parts.address_from?.postal_code ?? "", to: handoff?.values.address_to ?? "", area: handoff?.values.m2 ?? "",});read() resolves null or an object like this:
{ "key": "moving", "source": "navigation_form", "values": { "address_from": "Storgatan 1, 111 22 Stockholm", "address_from_street": "Storgatan", "address_from_street_number": "1", "address_from_postal_code": "111 22", "address_from_locality": "Stockholm", "address_to": "Kungsgatan 2, Uppsala", "m2": "75" }, "parts": { "address_from": { "street": "Storgatan", "street_number": "1", "postal_code": "111 22", "locality": "Stockholm" } }}values holds every field as the visitor typed it, as strings of at most 512
characters, keyed by the navigation form’s field names. When the visitor picked
an address suggestion and did not edit it afterwards, the structured parts
street, street_number, postal_code, locality, country and
formatted follow as <field>_<part>. parts groups the same parts by field.
Map the names your form needs; do not assume a field or part is present.
Access needs all three of:
- the component declares
"page_handoff": trueand the manifest requestspage_handoff:read; - the site administrator grants
page_handoff:readwhen installing or approving the release; - the page author binds the block instance to the navigation form. Such
components get a Prefill from navigation inputs field (block data
page_handoff_key) that takes the navigation form’s Defaults key. In an HTML-mode page use<x-extension block="…" props='{"page_handoff_key":"moving"}' />.
The handoff must also be addressed to the current page, which is the
navigation form’s destination, and be less than 15 minutes old. Otherwise, and
whenever nothing was handed off, read() resolves null without an error;
render the normal empty form. context.handoff.key is the bound key, or null
for an unbound instance.
Like a native receiving form, the runtime takes the handoff out of the tab the
first time a bound component reads it, so a reload starts empty. Every bound
component on that page receives the same values, and repeated read() calls in
a mount return them again. Keep them in context.storage.session if your flow
needs them after a reload. A native receiving form with the same key on the
same page takes the values first.
It works the same in preview links, where the preview shell keeps the handoff
for the tab instead of the browser’s session storage. In the editor canvas
read() resolves null.
The stored format is private to Typeroll and may change in any Core release.
Read handoffs only through context.handoff; never read Web Storage or call
page globals for them. The values are never added to a URL, referrer,
generated HTML, analytics event or Typeroll request. Treat them as personal
data: do not log them or send them anywhere before the visitor submits.
context.handoff exists from runtime 0.43.0. Declare
"runtime_compatibility": ">=0.43.0 <1.0.0" in a release that uses
page_handoff, because earlier runtimes reject the scope. Embedded
applications post typeroll.extension.handoff.read with a unique request_id
and receive typeroll.extension.handoff.result with the same handoff value.
Receive an opaque recipient token
Section titled “Receive an opaque recipient token”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:
- reads only declared query, fragment, path, or raw-query inputs;
- rejects values that exceed the declared length or pattern;
- captures values in the component’s private runtime closure;
- removes inputs marked
consumewithhistory.replaceState; and - exposes the captured value through
context.url.
From Core 0.2.18, cleanup preserves ordinary URL fragments exactly when no
fragment parameter is consumed. Removing ?quote=opaque-value therefore leaves
#details unchanged. Consuming a declared fragment parameter removes that
parameter and retains the remaining fragment parameters. The exported cleanup
helper and browser runtime behave the same way. Republish existing sites after
upgrading Core to receive the corrected runtime.
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.
Call the provider directly
Section titled “Call the provider directly”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.
Embedded applications
Section titled “Embedded applications”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.
First-party Analytics context (runtime 0.40.0)
Section titled “First-party Analytics context (runtime 0.40.0)”Bundled components receive context.analytics, either null or an object with
endpoint, token, and requires_consent. This contains a public site event
token, never a service credential. Respect the site’s optional-consent state
before sending. The CMS accepts only the site’s configured event rules; see
Events and conversions.
Public configuration and site versions
Section titled “Public configuration and site versions”Schema properties declared public: true can be edited independently on a site
version. The portal app’s configuration editor follows the selected version.
For automation, pass the same version to list_extension_installations,
read_extension_installation, and update_extension_installation_config.
REST clients use ?version=redesign on the installation GET or PATCH endpoint:
{ "config": { "heading": "Try the new introduction" } }Only schema-declared public fields can change on a branch. Credentials, private
settings, permissions, installation status and app releases remain site-wide;
manage those on main. version in an installation PATCH body still means an
app release; the query parameter selects the site version. An unknown site
version is rejected rather than falling back to main.
The public configuration follows the existing Settings copy-on-write, diff,
reset and promotion rules. Preview and static publication resolve the selected
version. Frozen publications retain their saved configuration. A branch edit
never changes the main installation or publishes production: the MCP config
tool’s automatic deploy targets staging for a branch, production for main.
Use deploy: false to save several edits before an explicit publication.
Native portal apps access this via sdk.configuration.read() and .save();
sdk.context.site_version identifies the selected site version, while
sdk.context.version is the app release. This does not branch provider-owned
records, forms, credentials, external sessions or arbitrary app API calls.