> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frogbot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-side Live Preview

> Implement Live Preview in a server-side front-end application.

> [!NOTE]
>
> Server-side Live Preview is for frameworks that support [Server Components](https://react.dev/reference/rsc/server-components). For client-side frameworks, use [client-side Live Preview](https://docs.frogbot.ai/live-preview/client.md).

Server-side Live Preview makes a round trip to the server whenever a document is saved as a draft, autosaved, or published. The Admin Panel sends a `window.postMessage` event that your frontend uses to refresh the route. In Next.js, `router.refresh()` hydrates the page with fresh data from the [Local API](https://docs.frogbot.ai/local-api/overview.md).

> [!WARNING]
>
> Enable [Autosave](https://docs.frogbot.ai/versions/autosave.md) with server-side Live Preview for a more responsive editing experience.

## React

For server-side React applications such as the [Next.js App Router](https://nextjs.org/docs/app), use `RefreshRouteOnSave` from `@frogbotai/live-preview-react`.

```bash theme={"theme":"tokyo-night"}
npm install @frogbotai/live-preview-react
```

Render a client wrapper from your preview page. This example uses a draft-enabled `pages` collection with `title` and `slug` fields. Authenticate the visitor before reading drafts, and enforce the collection's read access with the authenticated request. The frontend route must be protected even when it is normally opened inside the Admin Panel iframe.

```tsx theme={"theme":"tokyo-night"}
import config from '@/frogbot.config'
import { getFrogBot } from 'frogbot'
import { headers } from 'next/headers'
import { notFound } from 'next/navigation'

import { LivePreviewRefresh } from './LivePreviewRefresh'

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const frogbot = await getFrogBot({ config })
  const requestHeaders = await headers()
  const { user } = await frogbot.auth({ headers: requestHeaders })

  if (!user) {
    notFound()
  }

  const req = await frogbot.createRequest({ headers: requestHeaders, user })
  const result = await frogbot.find({
    collection: 'pages',
    where: { slug: { equals: slug } },
    draft: true,
    depth: 0,
    limit: 1,
    req,
    overrideAccess: false,
  })
  const page = result.docs[0]

  if (!page) {
    notFound()
  }

  return (
    <>
      <LivePreviewRefresh />
      <h1>{page.title}</h1>
    </>
  )
}
```

```tsx theme={"theme":"tokyo-night"}
'use client'

import { RefreshRouteOnSave } from '@frogbotai/live-preview-react'
import { useRouter } from 'next/navigation'

export function LivePreviewRefresh() {
  const router = useRouter()

  return (
    <RefreshRouteOnSave
      refresh={() => router.refresh()}
      serverURL={process.env.NEXT_PUBLIC_FROGBOT_URL as string}
    />
  )
}
```

`RefreshRouteOnSave` accepts these props:

| Prop | Type | Default | Description |
| - | - | - | - |
| `refresh` | `() => void` | Required | Called once after the component tells the Admin Panel it is ready, then after every save, autosave, or publish. In Next.js, call `router.refresh()`. |
| `serverURL` | `string` | Required | Origin of your FrogBot server, such as `https://cms.example.com`. Messages from any other origin are ignored. |
| `apiRoute` | `string` | None | Accepted for parity with `useLivePreview`; the component does not use it. |
| `depth` | `number` | None | Accepted for parity with `useLivePreview`; the component does not use it. Set depth in the query that renders the page. |

## Building your own router refresh component

Use `@frogbotai/live-preview` to integrate another framework or routing system.

```bash theme={"theme":"tokyo-night"}
npm install @frogbotai/live-preview
```

| Export | Description |
| - | - |
| `ready({ serverURL })` | Tells the Admin Panel that the frontend is ready to receive messages. It posts to the parent frame, or to the opening window when the preview runs in a separate window. |
| `isDocumentEvent(event, serverURL)` | Returns whether a `MessageEvent` came from `serverURL` and reports a document save, autosave, or publish. |

```tsx theme={"theme":"tokyo-night"}
'use client'

import { isDocumentEvent, ready } from '@frogbotai/live-preview'
import { useEffect, useRef } from 'react'

export function LivePreviewRefresh({
  refresh,
  serverURL,
}: {
  refresh: () => void
  serverURL: string
}) {
  const hasSentReadyMessage = useRef(false)

  useEffect(() => {
    const onMessage = (event: MessageEvent) => {
      if (isDocumentEvent(event, serverURL)) refresh()
    }

    window.addEventListener('message', onMessage)

    if (!hasSentReadyMessage.current) {
      hasSentReadyMessage.current = true
      ready({ serverURL })
    }

    return () => window.removeEventListener('message', onMessage)
  }, [refresh, serverURL])

  return null
}
```

## Troubleshooting

### Updates are slower than client-side Live Preview

Server-side Live Preview refreshes only after a document is saved, while client-side Live Preview reacts to form state. Enable [Autosave](https://docs.frogbot.ai/versions/autosave.md) and reduce `versions.drafts.autosave.interval` when appropriate:

```ts theme={"theme":"tokyo-night"}
export const Pages = {
  versions: {
    drafts: {
      autosave: {
        interval: 375,
      },
    },
  },
}
```

### Iframe refuses to connect

If your frontend's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents the Admin Panel from embedding it, allow the Admin Panel domain with `frame-ancestors`:

```plaintext theme={"theme":"tokyo-night"}
frame-ancestors: "self" localhost:* https://your-site.com;
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.