# Live Preview in Payload with Next.js

> How to see the real article while you write it in the Payload admin: the preview URL, a route that turns on Draft Mode and a component that refreshes the page.

URL: https://codegea.com/en/live-preview-in-payload-with-next-js
CMS · 2026-10-19 · Derian Pinto

Live Preview is the split screen in the Payload admin: the form on one side, your real site showing the content you are writing on the other. For an editor it is the difference between publishing blind and seeing the result first. Setting it up with Next.js takes three small pieces.

## How it works

The admin loads your site inside an `iframe`. For that iframe to show a **draft** and not the published content, you have to ask Next.js to turn on *Draft Mode*. Every time you save, the page must refresh. The three pieces:

1. A **preview URL** in the Payload config.
2. An **API route** that checks your session and turns on Draft Mode.
3. A **client component** on the page that refreshes on save.

## Piece 1: the URL in the config

In `payload.config.ts`:

```ts
admin: {
  livePreview: {
    collections: ['posts', 'projects'],
    breakpoints: [
      { label: 'Mobile', name: 'mobile', width: 375, height: 667 },
      { label: 'Tablet', name: 'tablet', width: 768, height: 1024 },
      { label: 'Desktop', name: 'desktop', width: 1440, height: 900 },
    ],
    url: ({ data, collectionConfig, locale }) => {
      if (!data?.slug || !collectionConfig) return null;
      const params = new URLSearchParams({
        collection: collectionConfig.slug,
        slug: String(data.slug),
        locale: locale.code,
      });
      return `/api/preview?${params}`;
    },
  },
},
```

The preview doesn't point straight at the article page, but at an intermediate route, `/api/preview`, to which it passes the collection, slug and language. If the document doesn't have a slug yet, it returns `null` and there is no preview. The `breakpoints` add the mobile, tablet and desktop buttons.

## Piece 2: the route that turns on Draft Mode

In `app/api/preview/route.ts`:

```ts
export async function GET(req: NextRequest) {
  const params = req.nextUrl.searchParams;
  const draft = await draftMode();

  if (params.get('exit')) {
    draft.disable();
    redirect('/');
  }

  const payload = await payloadClient();
  const { user } = await payload.auth({ headers: req.headers });
  const collection = params.get('collection');
  const slug = params.get('slug');
  const locale = params.get('locale');

  if (!user || !slug || !isLocale(locale) ||
      (collection !== 'posts' && collection !== 'projects')) {
    return new Response('Forbidden', { status: 403 });
  }

  draft.enable();
  redirect(collection === 'posts' ? postPath(locale, slug) : projectPath(locale, slug));
}
```

This route **requires an admin session**. `payload.auth({ headers })` checks the cookie, and since the iframe is on the same origin, the cookie travels on its own. Without a session, 403: nobody can see your drafts by guessing the URL. With a session, it turns on Draft Mode and redirects to the article's real URL, built with the same function the rest of the site uses. `?exit=1` turns it off.

## Piece 3: the page reads drafts and refreshes

On the article page, you ask whether Draft Mode is on and pass it to the query:

```ts
const { isEnabled: draft } = await draftMode();
const post = await getPost({ slug, locale, draft });
// …
{draft && <LivePreviewListener />}
```

With `draft` set to `true`, the query asks for the latest version saved as a draft, not the published one. And `LivePreviewListener` is a small client component:

```ts
'use client';

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

export function LivePreviewListener() {
  const router = useRouter();
  return <RefreshRouteOnSave refresh={() => router.refresh()} serverURL={SITE_URL} />;
}
```

Every time you save in the admin, `RefreshRouteOnSave` calls `router.refresh()` and Next.js renders the page again on the server, with the new draft.

## Two things worth knowing

- **It refreshes on save, not as you type.** This variant uses the server refresh, which is the simplest and doesn't require your component to read data on the client.
- **The admin and the site must share an origin.** If the admin is on another domain, the session cookie doesn't travel in the iframe and you have to solve authentication another way.

## What it is for in practice

Whoever writes sees right away how a long title, a table or a code block looks on mobile, without publishing. It is one of the reasons we chose Payload; the rest is in [Payload vs Strapi vs Directus](/en/payload-cms-vs-strapi-vs-directus-in-2026), and how it fits the site in [how codegea.com is built](/en/how-codegea-is-built-nextjs-payload-turso-r2).
