---
title: "Ultimate Payload CMS Live Preview Guide for Next.js"
slug: "payload-live-preview-nextjs-production-guide"
published: "2026-09-06"
updated: "2026-09-14"
categories:
  - "Next.js"
tags:
  - "Payload CMS live preview"
  - "Next.js Draft Mode"
  - "preview token"
  - "draft endpoint"
  - "multi-site live preview"
  - "locale isolation"
  - "@payloadcms/live-preview-react"
  - "Cache Components"
  - "server components"
  - "template definitions"
llm-intent: "reference"
audience-level: "advanced"
framework-versions:
  - "payload cms"
  - "next.js"
  - "@payloadcms/live-preview-react"
  - "typescript"
  - "pnpm"
status: "stable"
llm-purpose: "Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…"
llm-prereqs:
  - "Access to Payload CMS"
  - "Access to Next.js"
  - "Access to @payloadcms/live-preview-react"
  - "Access to TypeScript"
  - "Access to pnpm"
llm-outputs:
  - "Completed outcome: Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…"
---

**Summary Triples**
- (Preview token, mustBe, signed with HMAC-SHA256 and short-lived)
- (Payload draft endpoint, should, issue signed short-lived preview tokens and include site+locale context)
- (Next.js preview route, must, verify token, call draftMode(), set preview cookies, and redirect to target path)
- (Draft reads, mustBypass, published caches and Next.js Cache Components (use no-store/request-time rendering))
- (Multi-site installs, require, site and locale resolution in token and draft endpoint to preserve tenant isolation)
- (@payloadcms/live-preview-react, isUsedFor, wiring live preview UI with Next.js App Router in examples)
- (Template-definition records, describe, presentation variants that have no public page but can be previewed)
- (Admin iframe and Preview button, shouldBoth, use the same signed-token flow to avoid weaker isolation)
- (Preview URL exposure, mustNot, weaken tenant or locale isolation (validate context server-side))
- (Implementation, wasConvertedTo, a reusable setup for Payload CMS + Next.js App Router)
- (Examples in guide, reflect, Payload 3.88.0, @payloadcms/live-preview-react 3.88.0, Next.js 16.3.3)

### {GOAL}
Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…

### {PREREQS}
- Access to Payload CMS
- Access to Next.js
- Access to @payloadcms/live-preview-react
- Access to TypeScript
- Access to pnpm

### {STEPS}
1. Define your use case and constraints
2. Enable drafts and autosave in Payload
3. Configure admin preview and livePreview
4. Build a canonical, signed preview URL builder
5. Implement the draft entry endpoint
6. Read the correct version in Next.js
7. Choose an iframe update strategy
8. Preview template definitions safely

<!-- llm:goal="Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…" -->
<!-- llm:prereq="Access to Payload CMS" -->
<!-- llm:prereq="Access to Next.js" -->
<!-- llm:prereq="Access to @payloadcms/live-preview-react" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to pnpm" -->
<!-- llm:output="Completed outcome: Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…" -->

# Ultimate Payload CMS Live Preview Guide for Next.js
> Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…
Matija Žiberna · 2026-09-06

Live Preview looks simple in a demo: put a URL in a collection config, open an
iframe, and show the draft. A production implementation has more moving parts.
The CMS must know the correct public URL, the frontend must deliberately enter
draft mode, draft reads must bypass published caches, and neither the preview
URL nor the iframe may weaken tenant or locale isolation.

This guide starts with the implementation used by our multi-site website, then
turns it into a reusable setup for Payload CMS and the Next.js App Router. It
also covers template-definition records, which describe presentation but do
not have a public page of their own.

The examples reflect Payload `3.88.0`, `@payloadcms/live-preview-react`
`3.88.0`, and Next.js `16.3.3`. Check the reference links at the end when using
a later version.

## 1. Our use case

Our application has all of the conditions that make preview URLs more than a
string interpolation exercise:

- one Payload installation serves multiple sites;
- each site can enable a different set of locales;
- English and French use different route segments, such as `/en-CA/blog/...`
  and `/fr-CA/blogue/...`;
- published pages use Next.js Cache Components, while drafts must remain
  request-time and uncached;
- Blogs, Pages, Recipes, Giveaways, and Products share Live Preview;
- a separate `template-definitions` collection selects approved, code-owned
  presentation variants;
- editors need both the in-admin iframe and a normal Preview button.

For a Blog document, the flow is:

```mermaid
sequenceDiagram
    participant E as Editor
    participant A as Payload Admin
    participant D as Draft endpoint
    participant N as Next.js page
    participant P as Payload Local API

    E->>A: Edit a Blog
    A->>A: Resolve site, locale, and canonical route
    A->>A: Sign origin, path, and expiry
    A->>D: Load /api/draft?token=...
    D->>D: Verify signature, origin, path, and expiry
    D-->>N: Set Draft Mode cookie and redirect
    N->>N: Read await draftMode()
    N->>P: Query with draft: true and exact site/locale
    P-->>N: Return current draft version
    N-->>E: Render the real Blog template
    A-->>N: Notify after save/autosave
    N->>N: router.refresh()
```

The Blog collection supplies a conventional `admin.preview` callback:

```ts
admin: {
  preview: (data, { locale, req }) =>
    buildLivePreviewUrl({
      kind: "blog",
      locale,
      req,
      site: data.site,
      slug: typeof data.slug === "string" ? data.slug : "",
    }),
}
```

The shared admin config enables the iframe for several collections:

```ts
admin: {
  livePreview: {
    collections: ["pages", "blogs", "recipes", "giveaways", "products"],
    url: ({ collectionConfig, data, locale, req }) => {
      const kind = collectionConfig
        ? kindByCollection[collectionConfig.slug]
        : undefined

      if (!kind || !locale?.code) return null

      return buildLivePreviewUrl({
        kind,
        locale: locale.code,
        req,
        site: data.site,
        slug: typeof data.slug === "string" ? data.slug : "home",
      })
    },
  },
}
```

Both controls use the same routing and security rules, but they are separate:

| Feature      | Configuration                                                | Editor experience                                                 |
| ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------- |
| Preview      | `admin.preview`                                              | Opens the generated URL as a normal page/tab                      |
| Live Preview | `admin.livePreview` or collection/global `admin.livePreview` | Embeds the URL in Payload's preview panel and sends update events |

Configuring one does not automatically configure the other.

## 2. Why the implementation is split into layers

A robust preview system has five responsibilities:

1. **Payload drafts and versions** preserve unpublished state.
2. **The URL builder** converts CMS state into the correct public site, locale,
   and route.
3. **The draft endpoint** authenticates the request and enables Next.js Draft
   Mode.
4. **The data loader** selects draft or published records without mixing their
   cache behavior.
5. **The frontend bridge** refreshes a Server Component route after saves or
   merges real-time form state in a Client Component.

This separation prevents a common mistake: treating the appearance of a Live
Preview toggle as proof that unpublished content is fetched safely. The Admin
UI is only the entry point; the server query remains the security boundary.

## 3. Prerequisites

Install the React bridge if needed:

```bash
pnpm add @payloadcms/live-preview-react
```

Its version should normally match Payload. Enable drafts on each collection:

```ts
import type { CollectionConfig } from "payload";

export const Blogs: CollectionConfig = {
  slug: "blogs",
  versions: {
    drafts: {
      autosave: { interval: 800 },
    },
  },
  fields: [
    { name: "title", type: "text", localized: true, required: true },
    { name: "slug", type: "text", localized: true, required: true },
    { name: "body", type: "richText", localized: true },
  ],
};
```

Live Preview works without autosave, but server refresh only shows saved
state. Autosave makes that approach feel live while preserving Server
Component rendering.

Configure the frontend and secrets:

```dotenv
SERVER_URL=https://www.example.com
NEXT_PUBLIC_SERVER_URL=https://www.example.com
PAYLOAD_SECRET=your-existing-payload-application-secret
PREVIEW_SECRET=a-different-random-secret-with-at-least-32-characters
```

Generate a secret with `openssl rand -base64 48`. Never place
`PAYLOAD_SECRET` in a query string: it signs Payload authentication data. Use
a separate `PREVIEW_SECRET` and store production values in a secret manager.

## 4. Configure a collection

### Option A: collection-local Live Preview

Use this when a collection has unique routing logic:

```ts
export const Blogs: CollectionConfig = {
  slug: "blogs",
  admin: {
    livePreview: {
      openByDefault: false,
      breakpoints: [
        { name: "mobile", label: "Mobile", width: 390, height: 844 },
        { name: "tablet", label: "Tablet", width: 768, height: 1024 },
        { name: "desktop", label: "Desktop", width: 1440, height: 900 },
      ],
      url: ({ data, locale, req }) => {
        if (!locale?.code || typeof data.slug !== "string") return null;
        return buildLivePreviewUrl({
          kind: "blog",
          locale: locale.code,
          req,
          site: data.site,
          slug: data.slug,
        });
      },
    },
    preview: (data, { locale, req }) => {
      if (typeof data.slug !== "string") return null;
      return buildLivePreviewUrl({
        kind: "blog",
        locale,
        req,
        site: data.site,
        slug: data.slug,
      });
    },
  },
  versions: { drafts: true },
  fields: [],
};
```

The locale shapes differ: `livePreview.url` receives a locale object, while
`preview` receives the locale code as a string. Either callback may return a
URL, `null`, or a promise. Return `null` when required context is missing.

### Option B: root Live Preview

Use root configuration when many collections share one URL policy:

```ts
const kindByCollection: Partial<Record<string, "page" | "blog" | "recipe">> = {
  pages: "page",
  blogs: "blog",
  recipes: "recipe",
};

export const config: Config = {
  admin: {
    livePreview: {
      collections: Object.keys(kindByCollection),
      openByDefault: false,
      breakpoints: [
        { name: "mobile", label: "Mobile", width: 390, height: 844 },
        { name: "desktop", label: "Desktop", width: 1440, height: 900 },
      ],
      url: ({ collectionConfig, data, locale, req }) => {
        if (!collectionConfig || !locale?.code) return null;
        const kind = kindByCollection[collectionConfig.slug];
        if (!kind || typeof data.slug !== "string") return null;
        return buildLivePreviewUrl({
          kind,
          locale: locale.code,
          req,
          site: data.site,
          slug: data.slug,
        });
      },
    },
  },
  collections: [Blogs],
};
```

Prefer a typed lookup or switch over known slugs in application code. Root
configuration adds `collections` and `globals` target lists. Collection and
global configs already know their target. A local config can override root
behavior for that entity.

## 5. Build canonical, secure preview URLs

A single-site tutorial can interpolate `/posts/${slug}`. A production URL
builder should validate and bind the routing context:

```ts
export async function buildLivePreviewUrl(input: {
  kind: "page" | "blog" | "recipe";
  locale: string;
  req: PayloadRequest;
  site: unknown;
  slug: string;
}): Promise<string | null> {
  const site = await resolveAuthorizedSite(input.site, input.req);
  if (!site || !site.enabledLocales.includes(input.locale)) return null;

  const origin = resolveTrustedOrigin(site);
  if (!origin) return null;

  const path = buildCanonicalPublicPath({
    kind: input.kind,
    locale: input.locale,
    slug: input.slug,
  });
  const token = createShortLivedPreviewToken({
    origin,
    path,
    secret: process.env.PREVIEW_SECRET,
  });

  return token ? `${origin}/api/draft?${new URLSearchParams({ token })}` : null;
}
```

Those helper contracts should enforce:

- the Site exists and the current user may read it;
- the Site and requested locale are enabled;
- there is no silent locale fallback;
- the origin comes from trusted configuration, never a browser `Referer`;
- the path uses the same canonical route registry as the frontend;
- the token expires and binds the exact origin and path.

Our token is HMAC-SHA256 signed and includes `{ origin, path, exp, version }`.
Verification uses a timing-safe comparison, rejects unsafe paths, and refuses
a token presented on another origin. JWT is also valid if it enforces the same
claims.

Signing only a shared secret while accepting arbitrary `?slug=` input still
permits an open redirect. Redirect to the path recovered from the verified
token or from a verified CMS record.

## 6. Enable Next.js Draft Mode

The entry endpoint verifies the token, sets Next.js's cookie, and redirects to
the verified public path. Our app registers it as a Payload custom endpoint:

```ts
import { draftMode } from "next/headers";
import { connection } from "next/server";
import type { Endpoint } from "payload";

export const draftEndpoint: Endpoint = {
  path: "/draft",
  method: "get",
  handler: async (req) => {
    await connection();
    const requestURL = req.url ? new URL(req.url) : null;
    const origin = resolveValidatedRequestOrigin(req);
    if (!requestURL || !origin) {
      return new Response("Invalid preview request", { status: 400 });
    }

    const result = verifyPreviewToken({
      origin,
      secret: process.env.PREVIEW_SECRET,
      token: requestURL.searchParams.get("token"),
    });
    if (!result.valid) {
      return new Response("Invalid preview token", {
        status: 401,
        headers: { "Cache-Control": "no-store" },
      });
    }

    const draft = await draftMode();
    draft.enable();
    return new Response(null, {
      status: 307,
      headers: {
        "Cache-Control": "no-store",
        "Referrer-Policy": "no-referrer",
        Location: new URL(result.path, result.origin).toString(),
      },
    });
  },
};
```

`resolveValidatedRequestOrigin` must validate forwarded protocol and host
headers for your proxy topology. Payload endpoints sit below its API route, so
with the default `/api`, `path: "/draft"` becomes `/api/draft`.

A Next.js Route Handler at `app/api/draft/route.ts` is the common alternative.
Use a Payload endpoint for one API surface and Payload request context; use a
Route Handler when CMS and frontend are separate or that matches the existing
architecture. The validation contract is identical. Do not implement both
without a reason.

Cookie-changing operations normally belong to POST. CMS preview links and
iframe sources are navigations, so they commonly need a GET entry. Authenticate
it, keep tokens short-lived, mutate no application data, and send `no-store`.

Provide an exit endpoint or Server Action using:

```ts
const draft = await draftMode();
draft.disable();
```

Set `prefetch={false}` on an exit `<Link>` so prefetching cannot disable the
cookie unexpectedly.

## 7. Read the correct version

In Next.js 16, `draftMode()` is asynchronous:

```tsx
export default async function BlogPage({ params }: PageProps<"/blog/[slug]">) {
  const { slug } = await params;
  const { isEnabled } = await draftMode();
  const post = await getBlogBySlug({ slug, draft: isEnabled });
  if (!post) notFound();
  return <BlogTemplate document={post} />;
}
```

Switch Payload version behavior explicitly:

```ts
const result = await payload.find({
  collection: "blogs",
  depth: 1,
  draft: input.draft,
  fallbackLocale: false,
  limit: 1,
  locale: input.locale,
  overrideAccess: true,
  select: {
    body: true,
    featuredMedia: true,
    publishedAt: true,
    slug: true,
    summary: true,
    title: true,
  },
  where: {
    and: [
      { "site.slug": { equals: input.siteSlug } },
      { slug: { equals: input.slug } },
      ...(input.draft ? [] : [{ _status: { equals: "published" } }]),
    ],
  },
});
```

Use `overrideAccess: true` only for trusted server frontend code that enforces
site, locale, slug, and publication rules independently. Local API work on
behalf of a user should use `overrideAccess: false` and pass `req`.

Return a narrow presentation projection, not a raw Payload document. Match
relationship depth to the renderer and handle relationships that are IDs,
missing, or inaccessible.

### Cache Components

Make the draft/published boundary explicit:

```tsx
async function DraftModeGate(props: PageInput) {
  const { isEnabled } = await draftMode();
  return isEnabled ? (
    <UncachedDraftPage {...props} />
  ) : (
    <CachedPublishedPage {...props} />
  );
}
```

Our router puts this below `<Suspense>`, calls `await connection()` in the
draft branch, and keeps published loaders in dedicated `"use cache"`
functions. Next.js Draft Mode bypasses the fetch cache, Cache Components,
`unstable_cache`, and ISR for the request. `isEnabled` is readable in a cache
scope, but `enable()` and `disable()` must run outside one.

## 8. Choose an iframe update strategy

### A. Refresh Server Components after save

This uses the exact production renderer:

```tsx
"use client";

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

export function LivePreviewRefresh() {
  const router = useRouter();
  return (
    <RefreshRouteOnSave
      refresh={() => router.refresh()}
      serverURL={process.env.NEXT_PUBLIC_SERVER_URL ?? ""}
    />
  );
}
```

Mount it only in Draft Mode. With autosave, Payload saves the version, emits a
document event, and the bridge refreshes the route.

Advantages: Server Components work unchanged; relationship population, hooks,
and transforms come from a real Payload read; preview exercises production
routing. Tradeoffs: updates wait for save/autosave, include server latency,
and require the `serverURL` to match the Admin event origin.

### B. Merge form changes with `useLivePreview`

Use a Client Component for character-by-character feedback:

```tsx
"use client";

import type { Blog } from "@payload-types";
import { useLivePreview } from "@payloadcms/live-preview-react";

type BlogPreview = Pick<Blog, "body" | "title">;

export function LiveBlogPreview({ initialData }: { initialData: BlogPreview }) {
  const { data, isLoading } = useLivePreview<BlogPreview>({
    initialData,
    serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? "",
    depth: 1,
  });

  return (
    <article aria-busy={isLoading}>
      <h1>{data.title}</h1>
      {/* Render the live document state. */}
    </article>
  );
}
```

| Option           | Required | Purpose                                              |
| ---------------- | -------- | ---------------------------------------------------- |
| `initialData`    | Yes      | Server-fetched document beneath incoming changes     |
| `serverURL`      | Yes      | Trusted Payload origin whose messages are accepted   |
| `depth`          | No       | Relationship population depth                        |
| `apiRoute`       | No       | Non-default Payload API route                        |
| `requestHandler` | No       | Custom population handler for proxying or middleware |

The hook returns `{ data, isLoading }`. It gives immediate field changes, but
the preview must be client-side; server-only presentation needs a boundary;
and relationships/uploads still need careful population and ID-only handling.
Posted preview data is never authorization for a server mutation.

### C. Hybrid

A hybrid can use the hook for immediate local fields and refresh after saves
for relationships and derived output. It is richer but creates two preview
state sources. Define which source wins after save before adopting it.

## 9. Previewing Template Definitions

A Template Definition selects a layout; it is not itself a public article.
Preview a real document through the draft definition:

```ts
export const TemplateDefinitions: CollectionConfig = {
  slug: "template-definitions",
  admin: {
    livePreview: {
      url: ({ data, locale, req }) =>
        buildTemplateLivePreviewUrl({
          data,
          locale: locale?.code ?? "",
          req,
        }),
    },
    preview: (data, { locale, req }) =>
      buildTemplateLivePreviewUrl({ data, locale, req }),
  },
  versions: { drafts: true },
  fields: [],
};
```

The helper should:

1. Validate `contentType` against a closed, code-owned list.
2. Resolve and authorize the exact Site and locale.
3. Map content type to a Payload collection.
4. Find one representative document using `draft: true`,
   `fallbackLocale: false`, `overrideAccess: false`, and the current `req`.
5. Build its canonical public route.
6. Sign the origin and path like any other preview.

The redirected draft page then queries both the content and Template
Definition in draft mode.

### Keep a code-owned default template

Definitions select variants; they must not enable basic rendering:

```ts
export const blogTemplates = createTemplateRegistry("blog", {
  default: BlogTemplateDefault,
  editorial: BlogTemplateEditorial,
});

const definition = await getTemplateDefinition({
  contentType: "blog",
  draft,
  locale,
  siteSlug,
});
const Template = blogTemplates.resolve(definition?.templateKey);
```

Resolve missing or unknown keys to `default`. This protects new sites,
incomplete seeds, removed configuration, and preview sessions.

If no representative document exists, deliberately return `null`, use a
code-owned fixture, or render a dedicated template sandbox. Never borrow a
document from another Site or locale.

## 10. Configuration reference

These types reflect Payload `3.88.0`.

### `LivePreviewConfig`

| Property        | Type                               | Meaning                                                                                  |
| --------------- | ---------------------------------- | ---------------------------------------------------------------------------------------- |
| `url`           | URL value or sync/async callback   | Iframe source; `null`/`undefined` suppresses preview                                     |
| `breakpoints`   | `{ name, label, width, height }[]` | Toolbar devices; dimensions accept numbers or strings; responsive is included by default |
| `openByDefault` | `boolean`                          | Opens preview on first document view; stored user preference later wins; default `false` |

The URL callback receives `data`, `locale`, `req`, optional
`collectionConfig`, optional `globalConfig`, and deprecated `payload` (use
`req.payload`). It may run often with autosave, so avoid expensive work.

Root `admin.livePreview` adds `collections?: string[]` and
`globals?: string[]`.

### `admin.preview`

Its signature is:

```ts
type GeneratePreviewURL = (
  doc: Record<string, unknown>,
  options: {
    locale: string;
    req: PayloadRequest;
    token: string | null;
  },
) => string | null | Promise<string | null>;
```

Use `req` for authorized reads. Do not copy Payload's auth `token` into a
public URL; issue a purpose-specific preview token.

### Globals

Globals support local `admin.livePreview` and `admin.preview`. Because one
global may affect many routes, choose a fixed sandbox, homepage, or
representative page that visibly uses it.

### React exports

`@payloadcms/live-preview-react` exports:

- `RefreshRouteOnSave`, with required `refresh` and `serverURL`, plus optional
  `apiRoute` and `depth`;
- `useLivePreview`, with the options above.

The lower-level `@payloadcms/live-preview` package exposes `subscribe`,
`unsubscribe`, and `ready`. Prefer the React wrapper unless you need to own the
event lifecycle.

## 11. Security checklist

- Drafts/versions are enabled.
- URL callbacks derive Site and locale on the server.
- Site reads respect request access.
- The locale is enabled and `fallbackLocale: false` preserves isolation.
- Origins come from a trusted allowlist/host map, not `Referer`.
- `PAYLOAD_SECRET` never appears in a URL.
- `PREVIEW_SECRET` is separate, random, long, and secret-managed.
- Tokens expire and bind the exact origin and safe relative path.
- Signatures use safe comparison and the endpoint cannot open-redirect.
- Proxy host/protocol handling matches the deployment topology.
- Redirects send `no-store` and `no-referrer`.
- Draft loaders cannot cross Site or locale boundaries.
- Public loaders explicitly require published visibility.
- Client Components receive narrow projections, not raw documents.
- CSP, frame, cookie, and proxy policies allow the intended iframe.

In our staging environment, `PREVIEW_SECRET` is managed in Infisical with the
other runtime secrets.

## 12. Troubleshooting

### No Live Preview control

Register the collection in root `collections` or add local `livePreview`.
Configuring only `preview` creates no iframe control. Restart after config
changes.

### No Preview button

Add `admin.preview`; Live Preview does not create it. A new document may lack
Site, locale, or slug, in which case returning `null` is correct.

### Blank or refused iframe

Inspect the URL/response, `Content-Security-Policy` `frame-ancestors`,
`X-Frame-Options`, cookie policy, public origins, and reverse-proxy headers.

### Only published content appears

Confirm `draftMode().isEnabled`, `draft: true`, removal of public-only
`_status` conditions in draft mode, and the existence of a draft version.

### Changes do not update

For server refresh, mount the bridge in draft mode, match
`NEXT_PUBLIC_SERVER_URL`, and confirm save/autosave runs. For the hook, ensure
the component uses returned `data`, not only `initialData`.

### Wrong Site or language

Scope every query by trusted Site context, use `fallbackLocale: false`, and
build localized paths from one route registry. Never derive tenant ownership
from a fetched content document.

### Stale Cache Components output

Keep preview branching close to `draftMode()`, do not send draft input through
published cache functions, and do not application-cache draft query results.

### Template preview does not change

Query the definition with `draft: true`; verify the representative document's
Site/locale; ensure the key exists; retain the default registry entry.

## 13. Test plan

1. Intended collections expose the iframe and, separately, Preview button.
2. English and French URLs use their exact localized routes.
3. Disabled Sites, bad locales, missing slugs, and untrusted origins return
   `null`.
4. Valid tokens enable Draft Mode and redirect only to their bound path.
5. Tampered, expired, wrong-origin, malformed, and unsafe-path tokens fail.
6. Draft queries render unpublished documents; public queries do not.
7. Site and locale isolation hold in both modes.
8. Save/autosave events refresh the Server Component route.
9. Missing/unknown template keys resolve to the code-owned default.
10. Template previews use only same-Site, same-locale representatives.

Test the workflow in a browser too. Unit tests cannot reveal iframe policy,
cookie partitioning, proxy headers, or responsive-toolbar problems.

## 14. Decision guide

| Situation                                | Recommended setup                                       |
| ---------------------------------------- | ------------------------------------------------------- |
| One collection, one route                | Local `admin.livePreview` plus `admin.preview`          |
| Many collections share routing           | Root config with typed collection-to-route mapping      |
| Mostly Server Components                 | `RefreshRouteOnSave` and autosave                       |
| Immediate typing feedback                | `useLivePreview` Client Component                       |
| Relationships/server transforms dominate | Server refresh or a carefully defined hybrid            |
| Config record has no public route        | Same-context representative or dedicated sandbox        |
| Multi-site/multilingual                  | Server-resolved origin and canonical route; no fallback |
| Optional template selector               | Code-owned default plus configured variants             |

## 15. References

- [Payload Live Preview overview](https://payloadcms.com/docs/live-preview/overview)
- [Payload frontend integration](https://payloadcms.com/docs/live-preview/frontend)
- [Payload drafts](https://payloadcms.com/docs/versions/drafts)
- [Payload custom endpoints](https://payloadcms.com/docs/rest-api/overview#custom-endpoints)
- [Next.js Draft Mode guide](https://nextjs.org/docs/app/guides/draft-mode)
- [Next.js `draftMode` API](https://nextjs.org/docs/app/api-reference/functions/draft-mode)
- [Next.js Cache Components](https://nextjs.org/docs/app/getting-started/cache-components)

Related repository files:

- `src/payload/config/admin.ts` — shared iframe registration
- `src/payload/collections/content/blogs/Blogs/index.ts` — Blog Preview button
- `src/payload/collections/content/site-chrome/TemplateDefinitions/index.ts`
  — Template Definition preview
- `src/payload/utilities/preview-url.ts` — Site/locale/origin resolution
- `src/payload/utilities/preview-token.ts` — signed tokens
- `src/payload/endpoints/draft-mode.ts` — Draft Mode entry and exit
- `src/routing/routed-page.tsx` — draft/published boundary
- `src/components/live-preview/refresh-route-on-save.tsx` — refresh bridge
- `src/components/templates/shared/create-registry.ts` — template fallback

The essential model is: Payload decides _where_ to preview, the entry endpoint
decides _whether_ the request is trusted, Next.js Draft Mode decides _which
cache behavior_ applies, and the data layer decides _which version_ may render.
Treat those as explicit contracts and preview stays predictable as the app
grows.

## LLM Response Snippet
```json
{
  "goal": "Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…",
  "responses": [
    {
      "question": "What does the article \"Ultimate Payload CMS Live Preview Guide for Next.js\" cover?",
      "answer": "Payload CMS live preview: a production-ready Next.js guide to secure draft endpoints, signed short-lived tokens, draftMode routing, and template previews…"
    }
  ]
}
```