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. "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 / iframe: a full page load

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. Nothing extra to handle.

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:

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 loadIn-place swapIn-place swap
Re-connect on flex.page.changeNot needed (script re-runs)RequiredRequired
connect() argumentDefault, or a containerDefault, or a containerThe container (shadow root handled for you)