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
https://staging.example.comhttps://www.example.comCMS_API_URLhttps://api.bifrostorbit.com/cmshttps://api.bifrostorbit.com/cmsCMS_PUBLIC_KEYCMS_PREVIEW_KEYCMS_DEFAULT_REGIONCMS_WEBHOOK_SECRETSITE_INDEXABLEtrue| Staging website | Live website | |
|---|---|---|
| Address | https://staging.example.com | https://www.example.com |
| Reads the environment | Staging | Production |
CMS_API_URL | https://api.bifrostorbit.com/cms | https://api.bifrostorbit.com/cms |
CMS_PUBLIC_KEY | A public key from Staging | A public key from Production |
CMS_PREVIEW_KEY | A key from Staging with Read drafts on | Not set |
CMS_DEFAULT_REGION | The region key | The region key |
CMS_WEBHOOK_SECRET | From a webhook for Staging | From a webhook for Production |
SITE_INDEXABLE | Not set | true |
| Used as | Preview URL in the space's settings | 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
- Create the keys for each environment in Configuration › API keys, as in step 4 of the tutorial.
- Add the settings above to each deployment in your hosting's dashboard. Never commit them, and never give them a
NEXT_PUBLIC_prefix. - 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.
- Deploy.
- 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'sCMS_WEBHOOK_SECRETand deploy again so it is read. - Set the Preview URL in the space's Settings to the staging website's address.
- 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:
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 nextConfigDo 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
- Import the repository. Vercel detects Next.js.
- In Settings › Environment Variables, add the live settings for Production, and the staging settings for Preview, or for a separate staging project.
- Deploy. Caches are shared across Vercel's servers, and
revalidateTagfrom 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:
const nextConfig = { output: 'standalone' }export default nextConfig# 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"]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
generateStaticParamsreturns none, but Next.js renders its own 404 page with your layout while building, andlib/cms.tsthrows withoutCMS_API_URLandCMS_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
cacheHandleroption, backed by a store such as Redis, so a refresh reaches every copy. - Behind a proxy or CDN, pass the
Hostheader through, and do not cache HTML for longer than yourrevalidatetime, 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 fromcms.getSitemap()orcms.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:
'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:
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
/ and a deep pagecurl -sI https://www.example.com/nothing-here/robots.txt on the live website; it must not say Disallow: //robots.txt on the staging website/sitemap.xml; addresses use the primary domain| Check | How |
|---|---|
| Pages render on the live address | Open / and a deep page |
| Unknown addresses answer 404 | curl -sI https://www.example.com/nothing-here |
| robots.txt allows indexing | Open /robots.txt on the live website; it must not say Disallow: / |
| robots.txt refuses indexing on staging | Open /robots.txt on the staging website |
| The sitemap has live addresses | Open /sitemap.xml; addresses use the primary domain |
| Canonical links use the live domain | View the source of a page |
| The live website has no preview key | Check its settings |
| Publishing refreshes the live website | Promote or publish a change in Production and reload |
| Webhooks answer 200 | Send a test for each webhook |
| The editor shows the staging website | Open a page in the editor |
| Errors look right | Stop the network to the CMS on a test deployment and open a new page |
Was this page helpful?