Skip to main content

Delivery modes

The SDK's API is the same wherever your experience runs. What changes is how the experience gets onto the page — and three of those differences affect the code you write. Read this page before shipping an experience embedded in a site you own.

DeliveryWhose page is itWhere the experience livesHow you import the SDK
Standalone / iframe playerCeros' (the published URL, or an iframe to it)The Ceros documentBare specifier @ceros/flex-experience-sdk
Flex SSRYoursYour page's markup, server-rendered and hydratedThe absolute CDN URL
Flex InlineYoursA shadow root on the <div data-flex-inline> containerThe absolute CDN URL

Throughout this page, "standalone" covers both the published URL and an iframe pointing at it — your script runs inside the Ceros document either way. The one place the two part company is page navigation, called out where it matters below. "Inline / SSR" means the experience is a guest in a page you control.


Loading the SDK​

Standalone / iframe. The bare specifier resolves via the import map Ceros renders on every published page:

<script type="module">
import { connect } from '@ceros/flex-experience-sdk'
const experience = await connect()
</script>

Flex Inline / SSR. Your page has no Ceros import map, so import the bundle directly:

<script type="module">
import { connect } from 'https://assets.ceros.site/js/flex-experience-sdk.js'
const experience = await connect(/* a root — see below */)
</script>

The URL is stable and always serves the current SDK, and the assets host sends the CORS headers a cross-origin module import needs. Because a URL import is self-contained, it doesn't matter whether it runs before or after Ceros' own embed script.


Your custom head and body HTML is not injected​

In Flex you can attach custom HTML to your experience's <head> and <body> — analytics snippets, meta and OpenGraph tags, fonts, third-party embeds, consent scripts.

  • Standalone / iframe: Ceros owns the document and renders that custom HTML for you.
  • Flex Inline / Flex SSR: Ceros never touches your page's <head> or <body>, so that custom HTML is not emitted. Anything the experience depends on has to be on your host page instead.

In practice, on an Inline or SSR page:

  • Analytics and consent scripts you configured in Flex won't run unless your page includes them. (Ceros still forwards experience analytics events to the window.flexAnalytics host SDK — it's the arbitrary <script> tags that aren't carried over.)
  • Fonts and stylesheets referenced only from your custom head won't load. Add them to your page.
  • SEO, meta, and OpenGraph tags are your page's responsibility.

Rule of thumb: on Inline / SSR, everything outside the experience itself is yours to provide.


Multi-page experiences navigate differently depending on whose page it is.

Standalone: a full page load, unless the experience opts in​

By default, moving between pages loads a new document. experience.pages.goTo(slug) behaves like the presentation controls: the browser navigates, and your script runs again from scratch on the new page.

An experience can turn on Page navigation in its Flex settings to swap pages in place instead, so the document is never unloaded — which is also what lets fullscreen survive a page change. On such an experience, standalone behaves like Inline / SSR below: your script does not run a second time, and the handle it captured is stale. The event that tells you a page changed is the same one.

Two things follow. The iframe embed always does a full page load, even on an experience with the setting on. And nothing in your script can read the setting — so don't try to branch on it. Write the bindPage() shape below and you are correct either way: on a full load the whole script re-runs and the listener never fires; on an in-place swap the listener is what rebinds.

Flex Inline / SSR: an in-place swap​

Ceros' embed script runs a client-side router that swaps pages without reloading. Both experience.pages.goTo(slug) and links authored in the experience route through it. Two consequences to design for:

1. Your handle goes stale on every swap. The experience element is replaced, so a handle captured once points at the old, detached one — after which findByLocator, pages.*, and on(...) all quietly stop working. Re-connect whenever the page changes. Ceros fires a flex.page.change event on the window for every in-place swap, including back/forward and programmatic goTo — and this is the shape to write whatever the delivery, per the note above:

let experience = await connect(root)

function bindPage() {
// re-query locators and (re)attach listeners for the page now on screen
}

bindPage() // the first page — flex.page.change only fires on swaps

window.addEventListener('flex.page.change', async () => {
experience = await connect(root) // pick up the new experience element
bindPage()
})

2. Locators only see the current page. findByLocator('x') queries the page on screen at the moment you call it. A component on another page returns an empty set, and calls on it do nothing. Bind per page — inside bindPage() above — rather than once at startup.


What to pass to connect()​

connect(root?) finds the experience inside root, defaulting to the experience on the page.

  • Standalone / iframe / SSR: the default connect() works, as does any container element that holds the experience.
  • Flex Inline: the experience lives inside the container's shadow root, and connect reaches through it for you — pass the container itself: connect(document.querySelector('[data-flex-inline]')).

Multiple experiences on one page. Scope each call to that embed's container. Every embed carries a data-flex-manifest-url attribute, and the same call shape works for Inline and SSR:

const experience = await connect(
document.querySelector(
'[data-flex-manifest-url="https://…/pricing/manifest.v1.json"]',
),
)

Pass { timeout } when an embed may be lazy-loaded and you'd rather get an error than a promise that never settles.


Summary​

Standalone / iframeFlex SSRFlex Inline
SDK importBare specifier (import map)Absolute CDN URLAbsolute CDN URL
Custom head/body HTMLRendered by CerosYour page's jobYour page's job
Page navigationFull page load — in place with Page navigation on (standalone only)In-place swapIn-place swap
Re-connect on flex.page.changeRequired with Page navigation on, harmless without — always write itRequiredRequired
connect() argumentDefault, or a containerDefault, or a containerThe container (shadow root handled for you)