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

Source: https://docs.bifrostorbit.com/cms/developers/sdk/

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.

| 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](https://docs.bifrostorbit.com/cms/developers/sdk-php/) |
| Swift | `CMSOrbit` with Swift Package Manager | iOS and macOS apps, and server-side Swift | [Swift SDK](https://docs.bifrostorbit.com/cms/developers/sdk-swift/) |
| Kotlin | `com.bifrostorbit:cms-sdk` with Gradle or Maven | Android apps and JVM servers, Java too | [Kotlin SDK](https://docs.bifrostorbit.com/cms/developers/sdk-kotlin/) |

Using another language? Read content over the [delivery API](https://docs.bifrostorbit.com/cms/developers/delivery-api/) or [GraphQL](https://docs.bifrostorbit.com/cms/developers/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.

| 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](https://docs.bifrostorbit.com/cms/developers/connect-a-website/), which builds a complete website file by file, then:

- [How your website and CMS Orbit work together](https://docs.bifrostorbit.com/cms/developers/how-it-works/)
- [Render components](https://docs.bifrostorbit.com/cms/developers/render-blocks/)
- [Menus, site-wide content and lists](https://docs.bifrostorbit.com/cms/developers/menus-globals-and-lists/)
- [Caching and refreshing pages](https://docs.bifrostorbit.com/cms/developers/caching-and-refresh/)
- [Regions and languages](https://docs.bifrostorbit.com/cms/developers/regions-and-languages/)
- [Metadata, sitemaps and robots with the SDK](https://docs.bifrostorbit.com/cms/developers/seo-with-the-sdk/)
- [Test your website](https://docs.bifrostorbit.com/cms/developers/test-your-website/)
- [Deploy your website](https://docs.bifrostorbit.com/cms/developers/deploy-your-website/)
- [Troubleshooting your website](https://docs.bifrostorbit.com/cms/developers/troubleshooting/)

## Install

```bash
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](https://docs.bifrostorbit.com/cms/developers/connect-any-website/).

## Set up

Create the keys in **Configuration › API keys**: a public key for published content and, for previews, a secret key.

```bash
# .env.local
CMS_API_URL=https://api.bifrostorbit.com/cms
CMS_PUBLIC_KEY=cms_pk_...        # published content
CMS_PREVIEW_KEY=cms_sk_...       # drafts; server only, staging only
CMS_WEBHOOK_SECRET=...           # verifies publish webhooks
CMS_DEFAULT_REGION=germany       # the region's key, from Configuration › Environments
```

```ts
// lib/cms.ts
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 },
})
```

| 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

```bash
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

```tsx
// src/cms/registry.tsx
import '@/cms/types'
import { defineRegistry } from '@bifrost-orbit-cms/sdk/react'

export const registry = defineRegistry({ hero: Hero, 'card-grid': CardGrid, faq: Faq })
```

```tsx
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](https://docs.bifrostorbit.com/cms/developers/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

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

```ts
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']`:

```ts
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:

```ts
const result = await cms.resolve({ path, region, preview, include: ['references'] })
```

```tsx
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](https://docs.bifrostorbit.com/cms/developers/delivery-api/).

## Preview

```ts
// app/api/preview/route.ts
import { createPreviewHandler } from '@bifrost-orbit-cms/sdk/next'
import { cms } from '@/lib/cms'
export const GET = createPreviewHandler({ client: cms })
```

```tsx
// app/[region]/layout.tsx, inside <body>
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:

```tsx
'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](https://docs.bifrostorbit.com/cms/developers/preview/).

## Cache refresh on publish

```ts
// app/api/cms-webhook/route.ts
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](https://docs.bifrostorbit.com/cms/developers/webhooks/).

## Sitemaps and robots

```ts
import { createRobotsRoute, createSitemapIndexRoute, createSitemapRoute } from '@bifrost-orbit-cms/sdk/next'

// app/[region]/sitemap.xml/route.ts
export const GET = createSitemapRoute(cms)

// app/sitemap.xml/route.ts
export const GET = createSitemapIndexRoute(cms)

// app/robots.txt/route.ts
export const GET = createRobotsRoute(cms)
```

Staging always answers with a robots.txt that blocks crawlers.

## Popups, banners and countdowns

```tsx
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.

```tsx
'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 views
const 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](https://docs.bifrostorbit.com/cms/developers/render-campaigns/).

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

```tsx
// app/layout.tsx: the script, once
import { CmsInsights } from '@bifrost-orbit-cms/sdk/react'

<CmsInsights site={process.env.NEXT_PUBLIC_INSIGHTS_SITE} src="/stats/script.js" api="/stats/collect" />
```

```ts
// app/[[...slug]]/page.tsx: tag each page view with the entry
import { insightsMetadata } from '@bifrost-orbit-cms/sdk/next'

return { ...toMetadata(page), ...insightsMetadata(page) }
```

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

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

```ts
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](https://docs.bifrostorbit.com/cms/developers/graphql/) reference shows. Errors in the answer are thrown, not returned beside partial data. See the reference for the full schema.

## Client methods

| 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](https://docs.bifrostorbit.com/cms/developers/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

```ts
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](https://unpkg.com/@bifrost-orbit-cms/sdk/EXPLAIN.md).
