Using content › Live preview
Introduction
Content
Using content
Plugins
Recipes
Live preview
What you'll learn
Show editors the real page next to the form, updated as they type, before saving.
Before this page: Drafts, versions & scheduling.


Editors see the real page next to the form, updated as they type, before anything is saved.
{
slug: 'posts',
preview: ({ doc }) => (doc.slug ? `/posts/${doc.slug}` : null),
fields: [/* … */],
}preview returns the page that shows a document: a path on the site or an absolute URL, or
null when it has no page. Globals take the same option. The admin then shows a Preview
button; the page opens in a frame beside the form.
How it works
- The admin loads the page from
previewonce. - On every change it sends the form to the server (
POST /:collection/:id/preview), which returns the document as a normal read would: relationships and uploads populated,afterReadhooks applied. Nothing is saved. - The admin posts that document to the page with
postMessage; the page renders it.
So the page has to render from the document it receives. The helpers below do the wiring.
Nuxt
useLivePreview is auto-imported. Give it the ref that holds the document:
<script setup lang="ts">
import { renderRichText } from '@easy-cms/richtext'
const { data: post } = await useFetch(`/api/posts/${useRoute().params.slug}`)
useLivePreview(post)
const html = computed(() => renderRichText(post.value?.body))
</script>Next.js
Load in a Server Component, render in a Client Component with useLivePreview:
'use client'
import { useLivePreview } from '@easy-cms/next/live-preview'
export function PostView({ post: initial }: { post: Post }) {
const post = useLivePreview(initial)
return <h1>{post.title}</h1>
}Any frontend
import { subscribeLivePreview } from '@easy-cms/core/live-preview'
const stop = subscribeLivePreview((doc) => render(doc), {
origin: 'https://cms.example.com', // the admin's origin; default: the page's own
})Outside the admin's frame the helpers do nothing, so they can stay in production code.
Preview tokens
When the admin opens the preview, it adds easy-cms-preview=<token> to the URL. The token opens
one document's current draft (or one global) for an hour, without a login, so a frontend on
another origin can show drafts that were never published:
import { getPreviewToken } from '@easy-cms/core/live-preview'
const token = getPreviewToken() // from ?easy-cms-preview=, or null
const post = await fetch(
`${api}/posts/${id}?depth=2${token ? `&preview=${token}` : ''}`,
).then((r) => r.json())Put the document id in the preview URL for this, e.g.
preview: ({ doc }) => \https://www.example.com/preview/posts/${doc.id}\``.
On the server, cms.verifyPreviewToken(token) returns what a token opens
({ collection, id } or { global }) or null, and cms.createPreviewToken({ collection, id })
creates one, e.g. for a "share preview link" button.
Things to know
- Render rich text on the client too (
renderRichTextfrom@easy-cms/richtextworks in the browser), since the document carries the rich text JSON. - With the Nuxt and Next adapters, the page and the admin share an origin and the editor's session, so a page that reads drafts for logged-in users (as the examples do) needs no token.
- A preview token only opens the document it was made for, expires after an hour, and is refused when tampered with. Treat preview URLs like a temporary share link.
- Only users who may update a document (or create one, for new documents) can preview it.
Next steps
- Localization: preview each language.
- SEO: preview search results too.