# Connect a website without Next.js

> Use the SDK from any JavaScript framework, or call the delivery API over plain HTTP from any language, with preview and refresh on publish.

Source: https://docs.bifrostorbit.com/cms/developers/connect-any-website/

CMS Orbit works with any website that can make an HTTP request. Pick the option that fits your stack. Every option uses the same keys and returns the same content.

Create the keys first, as in the [Next.js tutorial](https://docs.bifrostorbit.com/cms/developers/connect-a-website/): a public `cms_pk_` key for published content and, on the server only, a secret `cms_sk_` key for drafts.

## 1. Any JavaScript framework

The client in `@bifrost-orbit-cms/sdk` needs only `fetch`, so it runs in Node 20 or later, Bun, Deno, Cloudflare Workers and other edge runtimes. React and Next.js are optional.

```ts
// src/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: 'germany',
})
```

Leave out the `cache` option: it only means something to Next.js. Render a page from the result of `resolve`, here in an Astro page:

```ts
// src/pages/[...slug].astro, in the front matter
import { cms } from '../lib/cms'

const result = await cms.resolve({ path: '/' + (Astro.params.slug ?? '') })
if (result.kind === 'redirect') return Astro.redirect(result.to, result.status)
if (result.kind === 'gone') return new Response(null, { status: 410 })
if (result.kind === 'notFound') return new Response(null, { status: 404 })
const page = result.page
```

Then map each entry in `page.components` to your own component by its `type`. React-based frameworks can use `CmsPage` and `defineRegistry` from `@bifrost-orbit-cms/sdk/react` exactly as the Next.js tutorial does. Types from `npx cms-orbit generate` work in any TypeScript project.

## 2. Any language over HTTP

Every endpoint is a `GET` under `https://api.bifrostorbit.com/cms/v1`, with the key as a bearer token:

```bash
curl -G "https://api.bifrostorbit.com/cms/v1/resolve" \
  -d url=/products/hiking-boots \
  -d region=de \
  -H "Authorization: Bearer cms_pk_..."
```

The answer is JSON with the content under `data`. For `resolve`, `data.kind` is `page`, `redirect`, `gone` or `notFound`, and the HTTP status matches: 200, 200, 410 and 404.

```json
{
  "data": {
    "kind": "page",
    "page": {
      "url": "/products/hiking-boots",
      "name": "Hiking boots",
      "components": []
    }
  }
}
```

The same call from Python:

```py
import requests

response = requests.get(
    "https://api.bifrostorbit.com/cms/v1/resolve",
    params={"url": "/products/hiking-boots", "region": "de"},
    headers={"Authorization": "Bearer cms_pk_..."},
)
result = response.json()["data"]
```

See the [Delivery API](https://docs.bifrostorbit.com/cms/developers/delivery-api/) for every endpoint and parameter, and try them in **Configuration › API playground**. Answers are cached for up to 60 seconds, so you can cache them on your side for as long.

## 3. GraphQL

GraphQL is at `https://api.bifrostorbit.com/cms/graphql` and takes the same keys. Use it to fetch a page, its menu and its banners in one request. From the SDK, call `cms.graphql(query, { variables })`; from anywhere else, `POST` the query as JSON with the bearer key.

```bash
curl 'https://api.bifrostorbit.com/cms/graphql' \
  -H 'Authorization: Bearer cms_pk_…' \
  -H 'Content-Type: application/json' \
  -d '{"query":"query ($url: String!) { page(url: $url, region: \"de\") { name components } navigation(key: \"main\", region: \"de\") { items { label href } } banners(path: $url, region: \"de\") }","variables":{"url":"/products/hiking-boots"}}'
```

See [GraphQL](https://docs.bifrostorbit.com/cms/developers/graphql/) for every query, type and error.

## 4. Preview without Next.js

The live preview opens your preview address with `?token=…&path=…`. Your website then:

1. Checks the token by calling `GET https://api.bifrostorbit.com/cms/preview?token=…`. A 200 means it is valid.
2. Remembers that this visitor is previewing, for example in a cookie, and redirects to `path`.
3. While previewing, reads with the secret key and `draft=true` (with the SDK, pass `preview: true`), and adds the bridge script below.
4. Puts `data-cms-id` with the component's `id` on each component's outer element, so editors can click it to select it.

```html
<script src="https://api.bifrostorbit.com/cms/bridge.js" defer></script>

<section data-cms-id="{component.id}">…</section>
```

The preview then reloads after each save. To show changes as editors type, see **Show changes as editors type** in [Set up the live preview](https://docs.bifrostorbit.com/cms/developers/preview/).

## 5. Refresh on publish without Next.js

Add a `POST` endpoint and register it in **Configuration › Webhooks**. Each delivery carries `x-orbit-timestamp` and `x-orbit-signature` headers. The signature is the hex HMAC-SHA256 of `timestamp + "." + body`, keyed with the webhook secret.

```ts
// src/routes/cms-webhook.ts
import { createHmac, timingSafeEqual } from 'node:crypto'

export async function POST(request: Request) {
  const timestamp = request.headers.get('x-orbit-timestamp') ?? ''
  const signature = request.headers.get('x-orbit-signature') ?? ''
  const body = await request.text()
  const expected = createHmac('sha256', process.env.CMS_WEBHOOK_SECRET!)
    .update(`${timestamp}.${body}`)
    .digest('hex')
  const valid =
    signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  const fresh = Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000
  if (!valid || !fresh) return new Response(null, { status: 401 })
  const { event, data } = JSON.parse(body)
  await purge(event, data)
  return new Response(null, { status: 204 })
}
```

`purge` is yours: clear the cached page at `data.url`, or your whole cache for events without one. [Events and webhooks](https://docs.bifrostorbit.com/cms/developers/events/) lists every event and what it carries.
