# Live preview

::: info 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](./drafts).
:::

<Screenshot name="preview" alt="Live preview next to the form" />

Editors see the real page next to the form, updated as they type, before anything is saved.

```ts
{
  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

1. The admin loads the page from `preview` once.
2. 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, `afterRead`
   hooks applied. Nothing is saved.
3. 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:

```vue
<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`:

```tsx
'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

```ts
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:

```ts
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 (`renderRichText` from `@easy-cms/richtext` works 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](./localization): preview each language.
- [SEO](./seo): preview search results too.
