Skip to content

Guides

Deploy your website

Run a staging website for previews and a live website for visitors, with the right keys, settings and webhook for each, on Vercel, Netlify, Docker or a static host, and keep the website up if the CMS cannot be reached.

How-to guide8 min read

This page takes a website built as in Connect a website to production. Most websites run two copies of the same code: a staging website that editors preview drafts on, and the live website that visitors see.

Two websites, two environments

Address
Staging website
https://staging.example.com
Live website
https://www.example.com
Reads the environment
Staging website
Staging
Live website
Production
CMS_API_URL
Staging website
https://api.bifrostorbit.com/cms
Live website
https://api.bifrostorbit.com/cms
CMS_PUBLIC_KEY
Staging website
A public key from Staging
Live website
A public key from Production
CMS_PREVIEW_KEY
Staging website
A key from Staging with Read drafts on
Live website
Not set
CMS_DEFAULT_REGION
Staging website
The region key
Live website
The region key
CMS_WEBHOOK_SECRET
Staging website
From a webhook for Staging
Live website
From a webhook for Production
SITE_INDEXABLE
Staging website
Not set
Live website
true
Used as
Staging website
Preview URL in the space's settings
Live website
What visitors and search engines see

Editors work in Staging, preview on the staging website, and promote to Production when ready. Promoting sends Page published to Production's webhook, so the live website refreshes.

A key from Production cannot read drafts, so the live website cannot show previews. That is deliberate: drafts never come near the public website.

A small website can skip staging: one website reading Production with a public key, and the CMS's own preview for editors. Editors then see their content, but not in your design.

Before the first deploy

  1. Create the keys for each environment in Configuration › API keys, as in step 4 of the tutorial.
  2. Add the settings above to each deployment in your hosting's dashboard. Never commit them, and never give them a NEXT_PUBLIC_ prefix.
  3. Add your domains in Configuration › Domains for each environment and make the live one primary, so canonical links, hreflang and sitemaps use the right addresses. See Add a website address.
  4. Deploy.
  5. Create a webhook for each website, pointing at its /api/revalidate, with Environment set to the one it reads. Put each secret in that website's CMS_WEBHOOK_SECRET and deploy again so it is read.
  6. Set the Preview URL in the space's Settings to the staging website's address.
  7. Check it with the list at the end of this page.

Let the editor show your website

The editor shows your staging website in a frame on https://app.bifrostorbit.com. Two things must allow it.

Framing. If your website sends X-Frame-Options or a Content-Security-Policy with frame-ancestors, allow the CMS on the staging website:

next.config.tsTypeScript
const nextConfig = {  async headers() {    if (process.env.SITE_INDEXABLE === 'true') return [{ source: '/:path*', headers: [{ key: 'X-Frame-Options', value: 'DENY' }] }]    return [{ source: '/:path*', headers: [{ key: 'Content-Security-Policy', value: "frame-ancestors 'self' https://app.bifrostorbit.com" }] }]  },}export default nextConfig

Do not send X-Frame-Options on the staging website at all: browsers apply it even when frame-ancestors allows the CMS.

The bridge script. While previewing, the page loads https://api.bifrostorbit.com/cms/bridge.js. If you set a Content-Security-Policy with script-src, add https://api.bifrostorbit.com to it on the staging website.

Cookies. Draft mode is a cookie, which Next.js sends with SameSite=None; Secure in production so it works inside the editor's frame. The staging website must therefore be served over https. Browsers that block cookies inside frames from other websites, such as Safari by default, keep draft mode off inside the editor; editors there can use Open in a new tab above the preview.

Vercel

  1. Import the repository. Vercel detects Next.js.
  2. In Settings › Environment Variables, add the live settings for Production, and the staging settings for Preview, or for a separate staging project.
  3. Deploy. Caches are shared across Vercel's servers, and revalidateTag from the webhook clears them everywhere.

Vercel's preview deployments for pull requests get their own addresses. Add them to the space's Other preview URLs if editors should see them, or keep one fixed staging address.

Netlify

Netlify runs Next.js with its own runtime. Check its documentation for your Next.js version: this setup needs draft mode, proxy.ts and revalidateTag. Add the settings under Site configuration › Environment variables, with different values per deploy context if one site serves both staging and live.

Docker and your own servers

Build a standalone server:

next.config.tsTypeScript
const nextConfig = { output: 'standalone' }export default nextConfig
Dockerfile
# DockerfileFROM node:22-alpine AS buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run buildFROM node:22-alpineWORKDIR /appENV NODE_ENV=productionCOPY --from=build /app/.next/standalone ./COPY --from=build /app/.next/static ./.next/staticCOPY --from=build /app/public ./publicEXPOSE 3000CMD ["node", "server.js"]
Shell
docker build -t my-site .docker run -p 3000:3000 --env-file .env.production my-site
  • The build needs the settings too. No CMS pages are built ahead of time, because generateStaticParams returns none, but Next.js renders its own 404 page with your layout while building, and lib/cms.ts throws without CMS_API_URL and CMS_PUBLIC_KEY. Pass them as build arguments or secrets. The build still succeeds if the CMS cannot be reached, because the layout catches errors.
  • Pages read the settings when they run, so the running container's settings decide which environment visitors see.
  • The cache lives in the container, under .next/cache. It starts empty after each deploy, so the first visit to each page fetches from the CMS.
  • Several containers each keep their own cache, and a webhook only reaches one of them. Either run one container, or configure a shared cache with Next.js's cacheHandler option, backed by a store such as Redis, so a refresh reaches every copy.
  • Behind a proxy or CDN, pass the Host header through, and do not cache HTML for longer than your revalidate time, or purge it from the webhook; see Caching and refreshing pages.

Static hosting

With output: 'export', Next.js writes plain HTML at build time. There is no server, so there is no draft mode, no preview route, no proxy and no webhook route. To use the CMS with a static website:

  • Build every page in generateStaticParams, reading addresses from cms.getSitemap() or cms.list.
  • Rebuild on publish: create a webhook whose address is your host's build hook, such as a Vercel deploy hook or a Netlify build hook. The host ignores the body and starts a build. Builds take minutes rather than seconds.
  • Preview with the CMS's own preview, or run a small staging server with the full setup.

Most websites are better off with a server, even a small one.

When the CMS cannot be reached

The SDK retries each call twice, after 250 and 750 milliseconds, then throws CmsUnavailableError. What visitors see:

  • Pages already in the cache keep being served. Next.js only replaces a cached page once a fresh copy renders successfully.
  • Pages nobody has opened since the last deploy fail to render, and show your error page.

Add a friendly error page:

app/error.tsxTypeScript
'use client'export default function Error({ reset }: { reset: () => void }) {  return (    <main>      <h1>Something went wrong</h1>      <p>Please try again in a moment.</p>      <button onClick={reset}>Try again</button>    </main>  )}

Or handle it in the page route and render a message with the right status:

TypeScript
import { CmsUnavailableError } from '@bifrost-orbit-cms/sdk'const result = await load(path, preview).catch((error) => {  if (error instanceof CmsUnavailableError) return null  throw error})if (!result) return <p>This page is temporarily unavailable.</p>

A page rendered this way is cached like any other, so keep revalidate short or the message stays for the full time.

For layouts, read menus and globals with .catch(() => null) so a missing menu does not take the whole website down.

To go further, keep the last good answer for each address on disk or in a store, and serve it when the CMS fails. The CMS's own answers allow shared caches to serve a stale copy for a day if the API fails, so a CDN in front of your website gives the same protection.

Rate limits

Each key and each IP address may make 15,000 requests a minute. A website that caches as described stays far below this: most page views make no request at all. Going over answers 429, which the SDK throws as CmsRateLimitError with retryAfter in seconds. It does not retry these itself.

Watch for loops that read every page at build time or in a script. Read at most 100 entries per request with list, and wait retryAfter seconds if you get a 429.

Rotating keys and secrets

  • A key: create a new key, update the setting and deploy, check the website, then revoke the old key in Configuration › API keys.
  • The webhook secret: webhooks cannot be edited; see "Change the secret" in Refresh the website when content changes.
  • After someone leaves, or if a key may have leaked, rotate at once. A revoked key stops working straight away.

Go-live checklist

Pages render on the live address
How
Open / and a deep page
Unknown addresses answer 404
How
curl -sI https://www.example.com/nothing-here
robots.txt allows indexing
How
Open /robots.txt on the live website; it must not say Disallow: /
robots.txt refuses indexing on staging
How
Open /robots.txt on the staging website
The sitemap has live addresses
How
Open /sitemap.xml; addresses use the primary domain
Canonical links use the live domain
How
View the source of a page
The live website has no preview key
How
Check its settings
Publishing refreshes the live website
How
Promote or publish a change in Production and reload
Webhooks answer 200
How
Send a test for each webhook
The editor shows the staging website
How
Open a page in the editor
Errors look right
How
Stop the network to the CMS on a test deployment and open a new page

Was this page helpful?

Read next