Codegea

Development

Custom software developmentSystems, platforms and integrations built around your operationCorporate websitesProfessional, fast and built to inspire trustOnline storesCatalog, payments and shipping ready to sell 24/7WooCommerceFast WooCommerce stores with each country’s payments and invoicingLanding pagesA page focused on one goal: turning visitors into customersMobile appsCustom iOS and Android apps that put your business in your customers’ pockets

Integration and CMS

CMS integrationsWordPress, Strapi, Directus and Arc XP connected to your whole ecosystemArc XPSupport, implementation and integrations for the media platformMigrationsMigrate your site, platform or database without losing anything

Growth and infrastructure

AI automationTechnology does the repetitive work and your team focuses on what mattersOn-page SEOBe found and recommended by Google: technical and content optimizationServers & hostingFast, secure servers monitored 24/7
RetainerYour dev team, by the monthFrom 20h a month. Fixed fee.
An AI quoting tool to quote services in minutesProfessional quotes in minutes, with projects, sellers and payments tracked in one place.See the case

By type

EcommerceStores and catalogsCorporate sitesBrand and lead genPlatforms & appsCustom software

Explore

All projectsCases and resultsClientsWho trusts usRetainerMonthly dev team

Blog

All articlesGuides and opinionNewsletterOne email a monthContactWe reply in 24h

Latest articles

How codegea.com is built: Next.js, Payload, Turso and R2CMS · 2026-10-20Cloudflare Workers: what they are and when to use themNode.js · 2026-10-19Live Preview in Payload with Next.jsCMS · 2026-10-19
NewsletterJoin our newsletterPractical web, SEO and AI notes.
ServicesCustom software developmentCorporate websitesOnline storesWooCommerceLanding pagesMobile appsCMS integrationsArc XPMigrationsAI automationOn-page SEOServers & hostingYour dev team, by the month
ProjectsProfessional quotes in minutes, with projects, sellers and payments tracked in one place.EcommerceCorporate sitesPlatforms & appsAll projectsClientsRetainer
Retainer
BlogAll articlesNewsletterContactHow codegea.com is built: Next.js, Payload, Turso and R2Cloudflare Workers: what they are and when to use themLive Preview in Payload with Next.jsJoin our newsletter
ESEN
Let's talk

CMS

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.

Derian Pinto · Founder & Full Stack Developer2026-10-192 min

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:

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:

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:

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:

'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, and how it fits the site in how codegea.com is built.

Share this article?LinkedInWhatsAppX

Written by

Derian Pinto

Founder & Full Stack Developer

Founder of Codegea. Builds custom websites, stores and platforms, and integrates and supports Arc XP for media companies.

Join our newsletter

An email with news and practical web, SEO and AI notes. No spam, unsubscribe anytime.

Shall we build your project?

Need help? Chat with us