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.
| Delivery | Whose page is it | Where the experience lives | How you import the SDK |
|---|---|---|---|
| Standalone / iframe player | Ceros' (the published URL, or an iframe to it) | The Ceros document | Bare specifier @ceros/flex-experience-sdk |
| Flex SSR | Yours | Your page's markup, server-rendered and hydrated | The absolute CDN URL |
| Flex Inline | Yours | A shadow root on the <div data-flex-inline> container | The 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.flexAnalyticshost 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.
Page navigation differs
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
connectreaches 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 / iframe | Flex SSR | Flex Inline | |
|---|---|---|---|
| SDK import | Bare specifier (import map) | Absolute CDN URL | Absolute CDN URL |
| Custom head/body HTML | Rendered by Ceros | Your page's job | Your page's job |
| Page navigation | Full page load | In-place swap | In-place swap |
Re-connect on flex.page.change | Not needed (script re-runs) | Required | Required |
connect() argument | Default, or a container | Default, or a container | The container (shadow root handled for you) |