Reference
SDKs
Official SDKs for JavaScript, PHP, Swift and Kotlin, and everything in the JavaScript SDK, from setup and types to pages, preview, cache refresh, campaigns, errors and tests.
Reference12 min read
CMS Orbit has official SDKs for four languages. Each one is a typed client for the same delivery API, with the same calls for pages, menus, global content, lists, campaigns, products, regions, sitemaps and preview. npx cms-orbit generate writes types for each of them from your space's content types and components.
@bifrost-orbit-cms/sdk on npmCMSOrbit with Swift Package Managercom.bifrostorbit:cms-sdk with Gradle or Maven| Language | Package | For | Guide |
|---|---|---|---|
| JavaScript and TypeScript | @bifrost-orbit-cms/sdk on npm | Next.js, React and any Node.js website | This page |
| PHP | bifrost-orbit/cms-sdk with Composer | Laravel, Symfony and plain PHP | PHP SDK |
| Swift | CMSOrbit with Swift Package Manager | iOS and macOS apps, and server-side Swift | Swift SDK |
| Kotlin | com.bifrostorbit:cms-sdk with Gradle or Maven | Android apps and JVM servers, Java too | Kotlin SDK |
Using another language? Read content over the delivery API or GraphQL; both are plain HTTP.
The JavaScript SDK
The JavaScript SDK, @bifrost-orbit-cms/sdk, connects a website to CMS Orbit: a typed client for the delivery API, a React renderer, Next.js helpers for routing, preview, cache refresh and sitemaps, SEO output, a mock client for tests, and a type generator. It is open source under the MIT licence and published on npm.
@bifrost-orbit-cms/sdk@bifrost-orbit-cms/sdk/reactCmsPage, CmsLivePage, useLivePage, CmsBlocks, defineRegistry, BlockProps, CmsImage, RichText, CmsInsights, and hooks for banners, popups and countdowns@bifrost-orbit-cms/sdk/nextgone, CmsBridge, and insightsMetadata, insightsRewrites and createInsightsProxy for Insights Orbit@bifrost-orbit-cms/sdk/seotoMetadata, hreflang, JsonLd and sitemap XML@bifrost-orbit-cms/sdk/testingcreateMockClient and fixturesnpx cms-orbit generate--lang php, --lang swift or --lang kotlin for the other SDKs| Import | For |
|---|---|
@bifrost-orbit-cms/sdk | The client, its errors, cache tags and image addresses |
@bifrost-orbit-cms/sdk/react | CmsPage, CmsLivePage, useLivePage, CmsBlocks, defineRegistry, BlockProps, CmsImage, RichText, CmsInsights, and hooks for banners, popups and countdowns |
@bifrost-orbit-cms/sdk/next | Handlers for preview, cache refresh, sitemaps and robots, gone, CmsBridge, and insightsMetadata, insightsRewrites and createInsightsProxy for Insights Orbit |
@bifrost-orbit-cms/sdk/seo | toMetadata, hreflang, JsonLd and sitemap XML |
@bifrost-orbit-cms/sdk/testing | createMockClient and fixtures |
npx cms-orbit generate | TypeScript types from your space's content types and components. Add --lang php, --lang swift or --lang kotlin for the other SDKs |
This page is the reference. For step-by-step guides, start with Connect a website, which builds a complete website file by file, then:
- How your website and CMS Orbit work together
- Render components
- Menus, site-wide content and lists
- Caching and refreshing pages
- Regions and languages
- Metadata, sitemaps and robots with the SDK
- Test your website
- Deploy your website
- Troubleshooting your website
Install
npm install @bifrost-orbit-cms/sdkIt needs Node.js 20 or later. React 19 and Next.js 16 are optional: the client works in any JavaScript runtime, /react needs React and /next needs Next.js. Not using Next.js? See Connect a website without Next.js.
Set up
Create the keys in Configuration › API keys: a public key for published content and, for previews, a secret key.
CMS_API_URL=https://api.bifrostorbit.com/cmsCMS_PUBLIC_KEY=cms_pk_... # published contentCMS_PREVIEW_KEY=cms_sk_... # drafts; server only, staging onlyCMS_WEBHOOK_SECRET=... # verifies publish webhooksCMS_DEFAULT_REGION=germany # the region's key, from Configuration › Environmentsimport { createCmsClient } from '@bifrost-orbit-cms/sdk'export const cms = createCmsClient({ baseUrl: process.env.CMS_API_URL!, apiKey: process.env.CMS_PUBLIC_KEY!, previewKey: process.env.CMS_PREVIEW_KEY, defaultRegion: process.env.CMS_DEFAULT_REGION ?? 'germany', cache: { revalidate: 60, tags: true },})baseUrlhttps://api.bifrostorbit.com/cms. Required.apiKeycms_pk_…) for published content. Required.previewKeycms_sk_…) for drafts. Server only: refused in browser code, so it cannot leak into a public bundle.defaultRegioncache{ revalidate, tags }. Previews are never cached.timeoutretriesfetchfetch, for tests or tracing.| Option | What it does |
|---|---|
baseUrl | The API address, https://api.bifrostorbit.com/cms. Required. |
apiKey | A public key (cms_pk_…) for published content. Required. |
previewKey | A secret key (cms_sk_…) for drafts. Server only: refused in browser code, so it cannot leak into a public bundle. |
defaultRegion | Used when a call names no region. |
cache | Next.js cache settings for published reads, { revalidate, tags }. Previews are never cached. |
timeout | Per attempt, in milliseconds. 5 seconds by default. |
retries | Extra attempts after a network error or a server error. 2 by default. |
fetch | Your own fetch, for tests or tracing. |
The language defaults to the region's own; pass locale to ask for another. With only a public key, the client also works in browser code, since the API allows calls from any website.
Types
npx cms-orbit generate --out src/cms/types.tsReads CMS_API_URL and CMS_PUBLIC_KEY, and writes one type per component and content type. Run it in CI so a schema change that would break the website fails the build.
Reference and References fields are typed as ids. If the website reads content with include: ['references'], add --include references and they are typed as the entries instead, such as CmsEntry<FaqEntryFields>[].
Components
import '@/cms/types'import { defineRegistry } from '@bifrost-orbit-cms/sdk/react'export const registry = defineRegistry({ hero: Hero, 'card-grid': CardGrid, faq: Faq })import { CmsImage, RichText, type BlockProps } from '@bifrost-orbit-cms/sdk/react'export function Hero({ block }: BlockProps<'hero'>) { return ( <section data-cms-id={block.id}> <h1>{block.heading}</h1> <RichText value={block.description} /> <CmsImage image={block.image} sizes="100vw" priority /> </section> )}Put data-cms-id={block.id} on each component's outer element. In the live preview, editors click an element with it to select that block, so a component without it cannot be picked from the preview.
Blocks nested in a slot, such as the button in a hero, are on the slot's field, so block.button is the button's node ({ id, type, fields, children, variant }) or null, and a Components slot is a list. The same blocks are rendered for you as slots.button if you would rather place them as they are. See Components and slots in Field types.
Once types are generated, leaving a component out of the registry is a type error. At run time an unknown component renders a note in development and nothing in production. Image fields arrive with their width, height and alt text; links to pages carry the page's current address.
Pages
import { notFound, permanentRedirect, redirect } from 'next/navigation'import { draftMode } from 'next/headers'import { CmsPage } from '@bifrost-orbit-cms/sdk/react'import { gone } from '@bifrost-orbit-cms/sdk/next'import { toMetadata, JsonLd } from '@bifrost-orbit-cms/sdk/seo'import { cms } from '@/lib/cms'import { registry } from '@/cms/registry'type Props = { params: Promise<{ region: string; slug?: string[] }> }async function load({ params }: Props) { const { region, slug = [] } = await params const { isEnabled: preview } = await draftMode() return cms.resolve({ region, path: '/' + slug.join('/'), preview })}export async function generateMetadata(props: Props) { const result = await load(props) return result.kind === 'page' ? toMetadata(result.page) : {}}export default async function Page(props: Props) { const result = await load(props) if (result.kind === 'redirect') return result.permanent ? permanentRedirect(result.to) : redirect(result.to) if (result.kind === 'gone') gone() if (result.kind === 'notFound') notFound() return ( <> <JsonLd page={result.page} /> <CmsPage page={result.page} registry={registry} /> </> )}toMetadata gives the title, description, self-canonical, robots, Open Graph, X card and hreflang alternates. page.meta says which region and language answered and whether a fallback was used.
Next.js cannot answer 410 from a page, so gone() shows the not-found page. Send a true 410 from proxy.ts where that matters.
Menus and global content
const header = await cms.getNavigation({ key: 'header', region })const footer = await cms.getGlobal({ key: 'footer', region })const articles = await cms.list({ type: 'article', region, sort: '-publishedAt', perPage: 12 })Entries without an address, and references
Entries such as FAQ entries or authors have no page of their own. Read them with list and include: ['fields']:
const faqs = await cms.list<FaqEntryFields>({ type: 'faqEntry', region, category: 'pricing', include: ['fields'] })faqs.items.map((item) => item.fields?.question)Every item has its id. Reference and References fields hold such ids. Pass include: ['references'] to resolve, getPage, getGlobal or list to get the entries they point at instead, with their fields, so a component can show the FAQ entries an editor picked:
const result = await cms.resolve({ path, region, preview, include: ['references'] })export function FaqList({ block }: BlockProps<'faqList'>) { return block.questions?.map((entry) => <Question key={entry.id} question={entry.fields.question} answer={entry.fields.answer} />)}References are expanded one level deep, in the language of the request, and only to entries that are published (or drafts, when previewing). Answers read with references are tagged cms:references, and the webhook handler refreshes them whenever content is published, so a page shows an edited FAQ entry straight away. See References in the Delivery API.
Preview
import { createPreviewHandler } from '@bifrost-orbit-cms/sdk/next'import { cms } from '@/lib/cms'export const GET = createPreviewHandler({ client: cms })import { CmsBridge } from '@bifrost-orbit-cms/sdk/next'<CmsBridge client={cms} />The handler checks the CMS preview token, turns on draft mode and redirects to the page. The bridge renders only in draft mode, so editors can select and move blocks in the live preview. createPreviewExitHandler gives a route that leaves preview.
To show changes as editors type rather than after each save, render draft pages with CmsLivePage from a client component:
'use client'import { CmsLivePage } from '@bifrost-orbit-cms/sdk/react'import { registry } from '@/cms/registry'export function LivePage({ page }: { page: CmsPage }) { return <CmsLivePage page={page} registry={registry} />}Use it in place of CmsPage when page.draft is true. See Set up the live preview.
Cache refresh on publish
import { createRevalidateHandler } from '@bifrost-orbit-cms/sdk/next'export const POST = createRevalidateHandler({ secret: process.env.CMS_WEBHOOK_SECRET! })Register the URL as a webhook in the CMS. The handler verifies the signature, refuses messages over 5 minutes old, and refreshes only the pages, menus and sitemaps that changed. See Refresh the website when content changes.
Sitemaps and robots
import { createRobotsRoute, createSitemapIndexRoute, createSitemapRoute } from '@bifrost-orbit-cms/sdk/next'// app/[region]/sitemap.xml/route.tsexport const GET = createSitemapRoute(cms)// app/sitemap.xml/route.tsexport const GET = createSitemapIndexRoute(cms)// app/robots.txt/route.tsexport const GET = createRobotsRoute(cms)Staging always answers with a robots.txt that blocks crawlers.
Popups, banners and countdowns
const { serverTime, items } = await cms.getBanners({ path, region })const popups = await cms.getPopups({ path, region, loggedIn })Only entries live right now come back, so expired ones never flash on screen. Countdowns arrive worked out: display says whether to show the timer, a follow-up, "Offer ended" or nothing.
'use client'import { useCountdown, usePopup } from '@bifrost-orbit-cms/sdk/react'const { popup, dismiss } = usePopup(popups.items) // one per page view, respects hide-for-days and max viewsconst left = useCountdown(banner.countdown, serverTime) // ticks every second, corrected by the CMS clockTo read banners once for every page, such as in a cached layout, ask with everyPage: true and pick per page with bannerFor(items, path, placement). See Render banners, popups and countdowns.
Insights Orbit
When the space is connected to Insights Orbit under Settings, each delivered page carries id and insights: { site }. Everything below is opt-in: without a site key, nothing is added to the page.
// app/layout.tsx: the script, onceimport { CmsInsights } from '@bifrost-orbit-cms/sdk/react'<CmsInsights site={process.env.NEXT_PUBLIC_INSIGHTS_SITE} src="/stats/script.js" api="/stats/collect" />// app/[[...slug]]/page.tsx: tag each page view with the entryimport { insightsMetadata } from '@bifrost-orbit-cms/sdk/next'return { ...toMetadata(page), ...insightsMetadata(page) }sitepage to take it from the deliverypage or contentId<meta name="insights-content">, so the page's numbers follow it when its address changessrc, apidisable['scroll']campaignstrue by default: counts elements with data-banner="LABEL" or data-popup="LABEL", sending Banner viewed once a page and Banner clicked for a link or button inside it (and the same for popups). Put the campaign's trackingLabel there.| Prop | Means |
|---|---|
site | The site key. Or pass page to take it from the delivery |
page or contentId | Adds <meta name="insights-content">, so the page's numbers follow it when its address changes |
src, api | Where the script and the collector are; Insights Orbit's own addresses by default |
disable | Automatic events to switch off, such as ['scroll'] |
campaigns | true by default: counts elements with data-banner="LABEL" or data-popup="LABEL", sending Banner viewed once a page and Banner clicked for a link or button inside it (and the same for popups). Put the campaign's trackingLabel there. |
To serve it from your own domain, either add insightsRewrites() to rewrites() in next.config.ts, or, to pass on each visitor's address, add a route:
import { createInsightsProxy } from '@bifrost-orbit-cms/sdk/next'export const { GET, POST } = createInsightsProxy()It sends the visitor's address in X-Insights-Forwarded-For only when INSIGHTS_PROXY_TOKEN is set, the token from the site's settings in Insights Orbit; keep that on the server. It reads the address from CF-Connecting-IP, X-Real-IP or the first X-Forwarded-For entry, so run it behind a proxy you trust to set them. With trailingSlash: true, use api="/stats/collect/".
GraphQL
const data = await cms.graphql<{ page: { name: string } | null }>( 'query ($url: String!) { page(url: $url) { name } }', { variables: { url: '/products/hiking-boots' } },)Pass the expected shape as the type. preview: true sends the preview key; ask for drafts in the query itself, as the GraphQL reference shows. Errors in the answer are thrown, not returned beside partial data. See the reference for the full schema.
Client methods
resolvegetPage, findPagefind… answers null instead of throwing when nothing is published there.listgetGlobal, findGlobalgetNavigation, findNavigationgetBanners, getPopupsgetRegions, getLocalesgetSitemap, getSitemapIndex, getRobotsgetProducts, getTaxonomygraphqlgetSchema| Method | Answers with |
|---|---|
resolve | A page, a redirect, gone or not found for an address. Call it before rendering. |
getPage, findPage | One page by address. find… answers null instead of throwing when nothing is published there. |
list | Entries of one content type, filtered and paged, with their fields when asked |
getGlobal, findGlobal | Site-wide content such as a footer, by its key |
getNavigation, findNavigation | A menu, by its key |
getBanners, getPopups | Banners and popups live now for a page |
getRegions, getLocales | The regions and languages the key may read |
getSitemap, getSitemapIndex, getRobots | Sitemaps and robots rules |
getProducts, getTaxonomy | Shop products with live prices, and taxonomy terms |
graphql | Any GraphQL query |
getSchema | Content types and components, used by the type generator |
Every method takes region, locale and preview where they apply, include on resolve, getPage, getGlobal and list, and a signal to cancel. See the Delivery API for what each answer contains.
Errors
Failed calls throw a CmsError, or one of its kinds: CmsNotFoundError, CmsAuthError for a missing or wrong key, CmsRateLimitError (with retryAfter in seconds) and CmsUnavailableError. Each has the HTTP status and a code. Network errors and server errors are retried twice with back-off, and each attempt times out after 5 seconds.
Tests
import { createMockClient, fixtures } from '@bifrost-orbit-cms/sdk/testing'const cms = createMockClient({ defaultRegion: 'de', pages: { '/de/products/hiking-boots': fixtures.page({ url: '/products/hiking-boots' }) },})The mock answers from what you give it, and fixtures builds realistic answers so a test only states what it cares about. Give it entries: [fixtures.entry({ contentType: 'faqEntry', fields: { … } })] to answer list, with fields when include asks for them.
Versions
The version history is in the package's CHANGELOG.md. Breaking changes are listed under Breaking, with what to change.
Code explanations
Comments such as // [EXPLAIN-D84] in the package point to an entry in its EXPLAIN.md. The file is included in the package, and can also be read at unpkg.com/@bifrost-orbit-cms/sdk/EXPLAIN.md.
Was this page helpful?