Guides
Set up the live preview
Let editors see drafts in your website inside the CMS editor, and click blocks to select them.
How-to guide6 min read
What the editor opens
Beside the editor, the CMS loads your website in a frame at the space's Preview URL followed by the page's address and a preview token, such as https://staging.example.com/products/hiking-boots?preview=eyJ…. Each token is for one page's draft and lasts an hour; the editor makes new ones as needed. Your website needs:
- a proxy that sends any address with
?preview=to the preview route, keeping the page's address; - the preview route, which checks the token and turns on draft mode;
- a page route that reads drafts with
preview: truewhile draft mode is on; - the bridge, so editors can click blocks and see changes as they type.
Step 14 of Connect a website has every file in full. The website must be served over https and allow the CMS, https://app.bifrostorbit.com, to show it in a frame; see Deploy your website.
The proxy
import { NextResponse, type NextRequest } from 'next/server'export function proxy(request: NextRequest) { const token = request.nextUrl.searchParams.get('preview') if (!token) return NextResponse.next() const target = new URL('/api/preview', request.url) target.searchParams.set('preview', token) target.searchParams.set('path', request.nextUrl.pathname) return NextResponse.redirect(target)}export const config = { matcher: [{ source: '/((?!api/|_next/).*)', has: [{ type: 'query', key: 'preview' }] }],}Without it, the page opens without draft mode and shows the published version.
The preview route
import { createPreviewHandler } from '@bifrost-orbit-cms/sdk/next'import { cms } from '@/lib/cms'export const GET = createPreviewHandler({ client: cms })It checks the CMS preview token, turns on Next.js draft mode and redirects to the page: to ?path= if given and on your website, otherwise to the page the token is for. A missing token answers 400, and an invalid or expired one 401.
To leave draft mode, add an exit route and link to /api/preview/exit?path=/current-page:
import { createPreviewExitHandler } from '@bifrost-orbit-cms/sdk/next'export const GET = createPreviewExitHandler()The bridge
import { CmsBridge } from '@bifrost-orbit-cms/sdk/next'<CmsBridge client={cms} />The bridge renders only in draft mode. It lets editors click a block in the preview to select it.
For clicking to work, each component's outer element needs data-cms-id set to the block's id, such as <section data-cms-id={block.id}>. The bridge selects the nearest element with it, so a component without it cannot be picked from the preview, and its parent is selected instead.
Parts of the website that are not the page itself, such as the header, footer and banner, can point editors to where they are edited. Add data-cms-edit with what the part is made from, separated by spaces: global:<key> for a settings page, navigation:<key> for a menu and banner:<id> for a banner. data-cms-edit-label names it:
<header data-cms-edit="global:header navigation:header" data-cms-edit-label="Header">Hovering it in the preview shows a tag with that name, and its menu opens each settings page, menu or banner it lists.
In the editor, clicking a link to another page of the same website opens that page for editing, in the same environment, region and language. Links to other websites open in a new tab, and holding ⌘ or Ctrl while clicking leaves the link to the browser.
Show changes as editors type
With the bridge alone, the preview reloads each time the editor saves, about a second after they stop typing. To show every change at once, render the page with CmsLivePage in a client component:
'use client'import { CmsLivePage } from '@bifrost-orbit-cms/sdk/react'import type { CmsPage } from '@bifrost-orbit-cms/sdk'import { registry } from '@/cms/registry'export function LivePage({ page }: { page: CmsPage }) { return <CmsLivePage page={page} registry={registry} />}// in your page, where you render the page bodyimport { CmsPage } from '@bifrost-orbit-cms/sdk/react'import { LivePage } from '@/components/live-page'{result.page.draft ? <LivePage page={result.page} /> : <CmsPage page={result.page} registry={registry} />}While the page is shown in the editor, the CMS sends each change to the preview as the delivery API would answer it: images and links filled in, and references expanded if the page was read with include: ['references']. CmsLivePage swaps the new fields and components into the page and renders them with your registry, so the preview changes as the editor types, without reloading. Outside the editor, and for published pages, it renders exactly like CmsPage.
- Your components then also run in the browser while previewing. Keep them free of server-only code, such as reading files, secrets or environment variables that only exist on the server. Visitors to published pages still get them rendered on the server, and the browser code for the preview is only loaded in draft mode.
- Anything outside the page body, such as the header or the title in the browser tab, updates when the editor reloads the preview.
- To render more than the components, for example a template for a content type, use the hook instead:
const live = useLivePage(page)gives the page with the editor's latest fields and components. onSavedonCmsLivePageoruseLivePageis called when the editor saves or publishes.
Without the SDK, the bridge gives the same changes to any framework:
window.cmsOrbit.on('page', ({ url, fields, components }) => render(fields, components), { include: ['references'] })window.cmsOrbit.on('saved', () => { /* the draft was saved or published */ })window.cmsOrbit exists once the bridge has loaded, which the bridge announces with a cms-orbit:ready event on window. A website that listens for page, input or saved handles changes itself, so the editor stops reloading the preview after each save. input gives the editor's own copy of the page, with image ids instead of images.
Entries without a body
Some content types have fields but no body, such as a blog post whose text is a rich text field. When entries of such a type have their own address, the editor shows the fields beside your website's preview of the entry, the same frame and token as for pages. Your page route reads the draft with preview: true as it does for any page, and the bridge works the same way:
- The preview reloads each time the editor saves. With
CmsLivePage,useLivePageorwindow.cmsOrbit.on('page', …), the new fields arrive as the editor types, withcomponentsempty, so render the entry fromfields, such aslive.fields.text. - There are no blocks to click, so
data-cms-idis not needed. Editors change the entry in the fields beside the preview. data-cms-editworks as on pages, so the header, footer and banner still open where they are edited.
Types without an address, such as authors or FAQ entries, have no page of their own, so the editor shows their fields without a website preview. Their entries appear in the preview of the pages that use them. Which editor a collection opens in is set on its content type; see Choose how a collection is edited.
Tell the CMS where the website is
In the space's Settings, set the preview address and the device widths editors can switch between. Editors can also copy a share link that opens the preview for someone without an account.
Was this page helpful?