Skip to content

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.

JavaScript and TypeScript
Package
@bifrost-orbit-cms/sdk on npm
For
Next.js, React and any Node.js website
Guide
This page
PHP
Package
bifrost-orbit/cms-sdk with Composer
For
Laravel, Symfony and plain PHP
Guide
Swift
Package
CMSOrbit with Swift Package Manager
For
iOS and macOS apps, and server-side Swift
Kotlin
Package
com.bifrostorbit:cms-sdk with Gradle or Maven
For
Android apps and JVM servers, Java too

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
For
The client, its errors, cache tags and image addresses
@bifrost-orbit-cms/sdk/react
For
CmsPage, CmsLivePage, useLivePage, CmsBlocks, defineRegistry, BlockProps, CmsImage, RichText, CmsInsights, and hooks for banners, popups and countdowns
@bifrost-orbit-cms/sdk/next
For
Handlers for preview, cache refresh, sitemaps and robots, gone, CmsBridge, and insightsMetadata, insightsRewrites and createInsightsProxy for Insights Orbit
@bifrost-orbit-cms/sdk/seo
For
toMetadata, hreflang, JsonLd and sitemap XML
@bifrost-orbit-cms/sdk/testing
For
createMockClient and fixtures
npx cms-orbit generate
For
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:

Install

Shell
npm install @bifrost-orbit-cms/sdk

It 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.

.env.localShell
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 › Environments
lib/cms.tsTypeScript
import { 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 },})
baseUrl
What it does
The API address, https://api.bifrostorbit.com/cms. Required.
apiKey
What it does
A public key (cms_pk_…) for published content. Required.
previewKey
What it does
A secret key (cms_sk_…) for drafts. Server only: refused in browser code, so it cannot leak into a public bundle.
defaultRegion
What it does
Used when a call names no region.
cache
What it does
Next.js cache settings for published reads, { revalidate, tags }. Previews are never cached.
timeout
What it does
Per attempt, in milliseconds. 5 seconds by default.
retries
What it does
Extra attempts after a network error or a server error. 2 by default.
fetch
What it does
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

Shell
npx cms-orbit generate --out src/cms/types.ts

Reads 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

src/cms/registry.tsxTypeScript
import '@/cms/types'import { defineRegistry } from '@bifrost-orbit-cms/sdk/react'export const registry = defineRegistry({ hero: Hero, 'card-grid': CardGrid, faq: Faq })
TypeScript
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

app/[region]/[[...slug]]/page.tsxTypeScript
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.

TypeScript
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']:

TypeScript
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:

TypeScript
const result = await cms.resolve({ path, region, preview, include: ['references'] })
TypeScript
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

app/api/preview/route.tsTypeScript
import { createPreviewHandler } from '@bifrost-orbit-cms/sdk/next'import { cms } from '@/lib/cms'export const GET = createPreviewHandler({ client: cms })
app/[region]/layout.tsx, inside <body>TypeScript
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:

TypeScript
'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

app/api/cms-webhook/route.tsTypeScript
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

TypeScript
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

TypeScript
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.

TypeScript
'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 clock

To 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.

TypeScript
// 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" />
TypeScript
// app/[[...slug]]/page.tsx: tag each page view with the entryimport { insightsMetadata } from '@bifrost-orbit-cms/sdk/next'return { ...toMetadata(page), ...insightsMetadata(page) }
site
Means
The site key. Or pass page to take it from the delivery
page or contentId
Means
Adds <meta name="insights-content">, so the page's numbers follow it when its address changes
src, api
Means
Where the script and the collector are; Insights Orbit's own addresses by default
disable
Means
Automatic events to switch off, such as ['scroll']
campaigns
Means
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:

app/stats/[...path]/route.tsTypeScript
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

TypeScript
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

resolve
Answers with
A page, a redirect, gone or not found for an address. Call it before rendering.
getPage, findPage
Answers with
One page by address. find… answers null instead of throwing when nothing is published there.
list
Answers with
Entries of one content type, filtered and paged, with their fields when asked
getGlobal, findGlobal
Answers with
Site-wide content such as a footer, by its key
getNavigation, findNavigation
Answers with
A menu, by its key
getBanners, getPopups
Answers with
Banners and popups live now for a page
getRegions, getLocales
Answers with
The regions and languages the key may read
getSitemap, getSitemapIndex, getRobots
Answers with
Sitemaps and robots rules
getProducts, getTaxonomy
Answers with
Shop products with live prices, and taxonomy terms
graphql
Answers with
Any GraphQL query
getSchema
Answers with
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

TypeScript
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?

Read next

The PHP SDKEverything in bifrost-orbit/cms-sdk for PHP websites, from setup and every client method to rendering components, preview, refresh on publish, SEO, errors, Laravel and Symfony.The Swift SDKRead CMS Orbit content in iOS, macOS and server-side Swift apps: install, set up, every method, your own types, SwiftUI, preview and errors.The Kotlin SDKRead CMS Orbit content in Android apps and on JVM servers, from Kotlin or Java: install, set up, every method, your own types, Jetpack Compose, preview and errors.Connect a website to CMS OrbitEvery step from an empty Next.js app to a website that renders CMS pages, previews drafts in the editor and refreshes the moment editors publish, with every file in full.Caching and refreshing pagesEvery cache between the CMS and a visitor, the settings that control them, which cache tags each call and each webhook uses, how to test refreshes on your computer, and what to check when a page stays out of date.Render componentsDraw the components editors place on pages with your own React components: the registry, the props each component gets, nested blocks and slots, images, rich text, links, shared sections, client components and the click-to-select preview.Troubleshooting your websiteEvery error message the SDK and API give a website, and every common symptom with pages, previews, cache refresh, types, SEO and regions, with the cause and the fix.Delivery APIEvery endpoint websites read content from, with parameters, example answers, keys, fallback, caching, rate limits, limits, timeouts and errors.GraphQLRead pages, lists, menus, globals and campaigns in one request, with the schema, examples, errors and limits.