---
title: "Next.js Draft Mode + ISR: Single-Route Live Previews"
slug: "nextjs-draft-mode-isr-single-route-previews"
published: "2026-03-04"
updated: "2026-04-06"
validated: "2026-02-19"
categories:
  - "Next.js"
tags:
  - "Next.js Draft Mode"
  - "ISR"
  - "static generation"
  - "Payload CMS"
  - "live preview"
  - "tenant routing"
  - "unstable_cache"
  - "draftMode"
  - "incremental static regeneration"
  - "@payloadcms/live-preview-react"
  - "revalidate cache"
llm-intent: "reference"
audience-level: "advanced"
framework-versions:
  - "next.js"
  - "payload cms"
  - "react"
  - "typescript"
  - "@payloadcms/live-preview-react"
status: "stable"
llm-purpose: "Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…"
llm-prereqs:
  - "Access to Next.js"
  - "Access to Payload CMS"
  - "Access to React"
  - "Access to TypeScript"
  - "Access to @payloadcms/live-preview-react"
llm-outputs:
  - "Completed outcome: Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…"
---

**Summary Triples**
- (Route architecture, uses, a single tenant-aware entry that delegates to a catch-all route (src/app/(frontend)/tenant-slugs/[tenant]/page.tsx delegating to [...slug]/page.tsx))
- (Static params generation, implemented via, generateStaticParams() which calls a Payload helper (getStaticPaths) to produce static paths per tenant)
- (Editor previews, enabled by, Next.js Draft Mode combined with @payloadcms/live-preview-react to render draft content on the same route)
- (Public pages, kept, fast and cached through static generation and ISR (revalidate) rather than making pages fully dynamic)
- (Caching strategy, leverages, next/cache unstable_cache for memoized server fetches and manual revalidation to reconcile draft updates with ISR)
- (No duplicate route trees, achieved by, using request-level draftMode detection to change data source/rendering without separate preview routes)
- (Revalidation on content change, performed with, Next.js revalidate API (revalidatePath or revalidate option) triggered on Payload save/webhook to refresh ISR cache)

### {GOAL}
Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…

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

### {STEPS}
1. Create single tenant route entry
2. Enable draft mode via secure API
3. Branch route logic on draft state
4. Split data layer for draft vs published
5. Add live preview refresh on save
6. Invalidate caches only on publish
7. Rewrite host routes while preserving API

<!-- llm:goal="Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…" -->
<!-- llm:prereq="Access to Next.js" -->
<!-- llm:prereq="Access to Payload CMS" -->
<!-- llm:prereq="Access to React" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to @payloadcms/live-preview-react" -->
<!-- llm:output="Completed outcome: Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…" -->

# Next.js Draft Mode + ISR: Single-Route Live Previews
> Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…
Matija Žiberna · 2026-03-04

I was building a multi-tenant Next.js site with Payload CMS when I hit a classic problem: editors needed instant preview updates, but production needed static performance. After trying a few patterns, I settled on one approach that gave us both without duplicating route trees. This guide walks you through the exact implementation so you can run ISR and Draft Mode on the same route.

## The Problem Setup

When you run a content-heavy site, production speed depends on static generation and caching. At the same time, editors expect a live preview workflow where saving content immediately updates what they see. Most teams choose one of two paths: make pages fully dynamic and lose static performance, or build separate preview routes and maintain duplicate logic.

In this implementation, the goal is narrower and more practical: keep one route, keep static generation, and still provide live draft preview behavior for editors.

## Step 1: Build a Single Route Entry for Tenant Pages

The first decision is route architecture. In this project, `src/app/(frontend)/tenant-slugs/[tenant]/page.tsx` is a wrapper that delegates everything to the catch-all route and shares static params generation.

```tsx
// File: src/app/(frontend)/tenant-slugs/[tenant]/page.tsx
import Page from './[...slug]/page'
import { getStaticPaths } from '@/payload/db'

export async function generateStaticParams() {
  return await getStaticPaths()
}

export default Page
```

The actual route logic lives in `src/app/(frontend)/tenant-slugs/[tenant]/[...slug]/page.tsx`, where static and dynamic behavior are configured together.

```tsx
// File: src/app/(frontend)/tenant-slugs/[tenant]/[...slug]/page.tsx
export const dynamicParams = true
export const revalidate = 604800 // 7 days

export async function generateStaticParams() {
  return await getStaticPaths()
}
```

This gives you a strong base: known routes are pre-rendered, unknown routes can still render on demand, and every request goes through one code path. That one-path design is what makes the rest of this pattern clean.

With route structure in place, the next step is enabling Draft Mode safely.

## Step 2: Enable Draft Mode Through a Secure API Entry Point

Draft Mode is turned on through `src/app/api/draft/route.ts`. Payload preview URLs call this endpoint with a secret and slug.

```ts
// File: src/app/api/draft/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const slug = searchParams.get('slug')

  if (secret !== process.env.PAYLOAD_SECRET && secret !== 'demo-secret') {
    return new Response('Invalid token', { status: 401 })
  }

  if (!slug) {
    return new Response('Missing slug', { status: 400 })
  }

  const draft = await draftMode()
  draft.enable()

  redirect(slug)
}
```

Preview URLs are produced from Payload config helpers and routed to this endpoint.

```ts
// File: src/payload/utilities/generatePreviewUrl.ts
const params = new URLSearchParams({
  slug: path,
  secret: process.env.PAYLOAD_SECRET as string,
})

return `${protocol}://${finalDomain}/api/draft?${params.toString()}`
```

This works because the endpoint sets the draft cookie and sends the editor back to the normal frontend path, not a separate preview path. From here, the same route can decide whether to render draft or published data.

## Step 3: Branch Behavior by Draft Mode Inside the Same Page Route

In the catch-all route, the key switch is `draftMode().isEnabled`. That value is read in both `generateMetadata` and the page component, so metadata and content stay in sync.

```tsx
// File: src/app/(frontend)/tenant-slugs/[tenant]/[...slug]/page.tsx
import { draftMode } from 'next/headers'

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug, tenant } = await params
  const { isEnabled: isDraft } = await draftMode()

  // pass isDraft into metadata handlers and data fetchers
}

export default async function SlugPage({ params: paramsPromise }: Props) {
  const { slug, tenant } = await paramsPromise
  const { isEnabled: isDraft } = await draftMode()

  // pass isDraft into route handlers and page query functions
}
```

The route then tries specialized handlers (product, careers, project, post, collection) and falls back to general page rendering. Every handler receives the same `isDraft` value. That keeps behavior consistent across all content types and avoids drift between preview and production implementations.

Once the route can detect draft state, your data layer must enforce the right caching behavior.

## Step 4: Split the Data Layer into Draft-Uncached and Published-Cached Paths

The core data pattern is implemented in `src/payload/db/index.ts`. `queryPageBySlug` uses an internal fetcher wrapped in React `cache` for request dedupe, then applies `unstable_cache` only for published mode.

```ts
// File: src/payload/db/index.ts
import { cache } from 'react'
import { unstable_cache } from 'next/cache'

const fetchPageInternal = cache(async (tenant: string, slug: string, draft: boolean) => {
  const payload = await getPayload({ config: configPromise })

  const where: Where = { and: [{ slug: { equals: slug } }] }
  if (tenant) where.and?.push({ 'tenant.slug': { equals: tenant } })
  if (!draft) where.and?.push({ hide: { equals: false } })

  const { docs } = await payload.find({
    collection: 'page',
    overrideAccess: true,
    draft,
    depth: 1,
    limit: 1,
    where,
  })

  return docs.length > 0 ? (docs[0] as Page) : null
})

export const queryPageBySlug = async ({
  tenant,
  slug,
  draft,
}: {
  tenant: string
  slug: string
  draft?: boolean
}) => {
  if (draft) {
    return await fetchPageInternal(tenant, slug, true)
  }

  return await unstable_cache(
    async () => await fetchPageInternal(tenant, slug, false),
    [CACHE_KEY.PAGE_BY_SLUG(slug, tenant)],
    {
      tags: [TAGS.PAGES, CACHE_KEY.PAGE_BY_SLUG(slug, tenant)],
      revalidate: false,
    },
  )()
}
```

This is the technical center of the whole design:

1. Draft requests skip Next.js data caching and read fresh draft-aware content.
2. Published requests use cached fetches and are invalidated by tags/paths on publish.
3. The same query function handles both modes without duplicating route code.

The same draft-vs-published pattern is used across `getPostBySlug`, `getProjectBySlug`, `getJobOpeningBySlug`, `getCollectionBySlug`, `getProductBySlug`, and related design-data accessors. That consistency is what keeps behavior predictable.

Now that draft requests fetch fresh content, we need save-triggered refresh in the browser.

## Step 5: Add Live Preview Refresh to Complete the Editor Loop

To reflect new draft saves immediately, this project uses `@payloadcms/live-preview-react` in a client component and calls `router.refresh()`.

```tsx
// File: src/app/(frontend)/tenant-slugs/[tenant]/[...slug]/refresh-route-on-save.tsx
'use client'

import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation.js'

export const RefreshRouteOnSave: React.FC<{ tenantSlug: string }> = ({ tenantSlug }) => {
  const router = useRouter()

  return (
    <PayloadLivePreview
      refresh={() => router.refresh()}
      serverURL={finalPath}
    />
  )
}
```

In the server route, this component is only rendered when Draft Mode is enabled:

```tsx
// File: src/app/(frontend)/tenant-slugs/[tenant]/[...slug]/page.tsx
{isDraft && <RefreshRouteOnSave tenantSlug={tenant} />}
```

This keeps preview tooling out of normal production traffic while giving editors immediate visual feedback after save events.

The last piece is making sure publish events invalidate cached output for public users.

## Step 6: Revalidate Caches and Paths Only for Published Content

Publish-time invalidation is centralized in `createCollectionRevalidationHook`.

```ts
// File: src/payload/hooks/revalidateCollectionCache.ts
if (doc._status === 'draft') {
  // Draft Mode uses request-time fetching
  return
}

revalidateTag(tagName, 'max')
revalidatePath(path)
```

Collections register this factory in their `afterChange` hooks with path settings. For example, pages register:

```ts
// File: src/payload/collections/content/pages/hooks/revalidatePageCache.ts
export const revalidatePageCache = createCollectionRevalidationHook({
  collectionSlug: 'page',
  pathPrefix: 'tenant-slugs',
})
```

This separation is important. Saving a draft should not churn ISR caches, but publishing should invalidate both data tags and route paths. That gives editors smooth preview while keeping public caches accurate.

One more operational detail matters in this setup: tenant URL rewriting.

## Step 7: Use Host-Based Tenant Rewrites While Keeping Draft Entry Separate

`src/proxy.ts` rewrites public URLs to internal tenant routes and intentionally excludes `/api/*`.

```ts
// File: src/proxy.ts
if (
  tenant &&
  !pathname.startsWith('/api') &&
  !pathname.startsWith('/_next') &&
  !pathname.startsWith('/admin')
) {
  const slugPath = pathname === '/' ? '/home' : pathname
  const newUrl = new URL(`/tenant-slugs/${tenant}${slugPath}`, req.url)
  return NextResponse.rewrite(newUrl)
}
```

Because `/api/draft` is excluded, Draft Mode is enabled first. After redirect, the normal frontend path is rewritten into `tenant-slugs/[tenant]/...` and rendered through the same catch-all page. This is what lets preview and production share route files cleanly in a multi-tenant environment.

## Conclusion

This implementation solves a specific but common problem: how to keep static/ISR performance for public traffic while supporting live draft previews for editors. The key was not adding a parallel preview route tree, but making one route draft-aware from top to bottom. Draft Mode controls request behavior, db accessors split cached and uncached paths, and publish hooks handle invalidation only when content goes live.

By the end of this setup, you can keep production pages fast and cache-friendly while editors preview draft changes on real routes with near real-time refresh.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija

## LLM Response Snippet
```json
{
  "goal": "Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…",
  "responses": [
    {
      "question": "What does the article \"Next.js Draft Mode + ISR: Single-Route Live Previews\" cover?",
      "answer": "Next.js Draft Mode: combine ISR, static generation, and Payload CMS live previews on a single route—keep public pages fast and enable instant editor…"
    }
  ]
}
```