Back to Blog

Next.js Draft Mode: CMS Previews, ISR Invalidation, and Preview API Patterns

Implement Next.js Draft Mode for CMS content previews. Covers enabling draft mode via API route, bypassing ISR cache, Contentful and Sanity preview integration, and secure preview URL generation.

Viprasol Tech Team
March 18, 2027
11 min read

Next.js Draft Mode: Preview Content Before Publishing (2026)

Content management has always been a balancing act. You want creators to see exactly how their content will look before it goes live, but you don't want to rebuild your entire site every time someone makes a small change. Next.js Draft Mode solves this problem elegantly, and after implementing it across dozens of our projects, I want to share what we've learned.

Understanding the Draft Mode Problem

Before Draft Mode existed, teams faced a difficult choice: either rebuild your entire static site frequently (slow), or serve dynamic content from a database (slower and less secure). This created friction in the publishing workflow. Content editors would make changes, hit publish, and then wait for a build to complete—sometimes 10, 20, or 30 minutes for large sites.

Draft Mode flips the problem. You keep your site statically generated for maximum performance, but you enable a special preview mode that shows unpublished content without a rebuild. It's the best of both worlds.

How Draft Mode Works at a Technical Level

Next.js Draft Mode operates through a simple but powerful mechanism:

  1. When a user requests a page in Draft Mode, Next.js bypasses static caching
  2. It fetches the latest content from your data source (headless CMS, database, etc.)
  3. It renders the page dynamically with that fresh content
  4. The response is marked as non-cacheable, so each request gets current data
  5. Once the user exits Draft Mode, the cached version is served again

This means content creators can preview changes instantly without waiting for builds, while your public audience continues to enjoy fast, cached pages.

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 1000+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Setting Up Draft Mode in Your Next.js Application

The implementation is straightforward. In your pages/api/draft.ts:

Code:

import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // Verify the secret key (use environment variables)
  if (req.query.secret !== process.env.DRAFT_SECRET) {
    return res.status(401).json({ message: 'Invalid secret' });
  }

  // Enable Draft Mode
  res.setDraftMode({ enable: true });
  
  // Optionally redirect to a preview path
  const slug = req.query.slug as string;
  res.writeHead(307, { Location: **/posts/${slug}** });
  res.end();
}

In your content pages:

Code:

import { useRouter } from 'next/router';

export default function Post({ post, draftMode }: PostProps) {
  const router = useRouter();

  return (
    <div>
      {draftMode && (
        <div className="draft-banner">
          This is a draft preview. 
          <a href="/api/exit-draft">Exit Draft Mode</a>
        </div>
      )}
      <h1>{post.title}</h1>
      <article>{post.content}</article>
    </div>
  );
}

export async function getStaticProps({ draftMode }: GetStaticPropsContext) {
  const post = await fetchPost();

  return {
    props: { post, draftMode: !!draftMode },
    revalidate: draftMode ? false : 60 // Don't cache in draft mode
  };
}

For the exit endpoint (pages/api/exit-draft.ts):

Code:

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.setDraftMode({ enable: false });
  res.writeHead(307, { Location: '/' });
  res.end();
}

Integrating with Headless CMS Platforms

Most modern headless CMS platforms support Draft Mode webhooks. This is where things get really powerful.

For web development projects using Contentful, set up a webhook that triggers when content is published:

Code:

// pages/api/revalidate.ts
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // Verify webhook signature
  if (!verifyWebhookSignature(req)) {
    return res.status(401).json({ message: 'Invalid signature' });
  }

  try {
    // Revalidate static pages that were affected
    await res.revalidate(**/blog/${req.body.fields.slug}**);
    
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

When your editor hits publish in Contentful, the webhook fires, revalidates the page, and your static cache updates. The next visitor gets the fresh version instantly.

CMS Integration Strategies

Here's what works well across different platforms:

  • Contentful: Use preview URLs feature to launch Draft Mode directly from the editor
  • Sanity: Set up draft documents with separate queries for draft vs. published
  • Strapi: Use Draft/Publish workflow with webhook triggers
  • GraphCMS: Create separate environments for drafts and production
Next.js - Next.js Draft Mode: CMS Previews, ISR Invalidation, and Preview API Patterns

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Architecture Patterns That Work

The Publishing Workflow

At Viprasol, we've implemented this flow for our SaaS development clients:

  1. Editor creates content in CMS
  2. Clicks "Preview" → opens Draft Mode URL with secret
  3. Views changes in real-time as they edit
  4. Once satisfied, clicks "Publish"
  5. CMS webhook triggers revalidation
  6. On-demand revalidation updates the static page
  7. Within seconds, public sees updates

Tiered Publishing Strategy

For large sites with different content types:

Code:

// Different revalidation times based on content importance
const REVALIDATE_TIME = {
  homepage: 60,        // Revalidate every minute
  blog: 3600,          // Revalidate every hour
  archived: 86400,     // Revalidate daily
  products: false      // Never auto-revalidate, only on-demand
};

Advanced Draft Mode Techniques

Role-Based Preview Access

Not all users should see draft content:

Code:

async function getStaticProps(context) {
  const draftMode = context.draftMode;
  
  // Verify user role from JWT in cookies
  const userRole = await verifyUserRole(context.req.cookies);
  
  const content = await fetchContent({
    includeDrafts: draftMode && userRole === 'editor'
  });

  return {
    props: { content, canEdit: userRole === 'editor' },
    revalidate: draftMode ? false : 60
  };
}

Scheduled Content Publishing

Sometimes you want content to go live at a specific time:

Code:

async function getStaticProps(context) {
  const content = await fetchContent();
  
  // Check if content is scheduled but not yet published
  const isScheduled = content.publishDate > new Date();
  
  if (isScheduled && !context.draftMode) {
    return {
      notFound: true // Hide scheduled content
    };
  }

  return {
    props: { content },
    revalidate: isScheduled ? 60 : 3600 // Check frequently for scheduled content
  };
}

Draft-Only Features

Preview features that haven't launched yet:

Code:

export default function Page({ features, draftMode }) {
  const visibleFeatures = features.filter(f => 
    !f.isDraft || draftMode
  );

  return (
    <div>
      {visibleFeatures.map(feature => (
        <FeatureCard key={feature.id} feature={feature} />
      ))}
    </div>
  );
}

Performance Considerations

Caching Strategy

For cloud solutions architecture:

  • Static pages (production): CDN caching, infinite TTL
  • Draft pages: No caching, always fresh
  • Assets: Separate CDN with long-term caching

Database Query Optimization

Draft Mode doesn't bypass your database queries. Optimize them:

Code:

// Use query parameters to fetch only needed fields
const query = draftMode 
  ? **query { post { id title content author updated } }**
  : **query { post { id title excerpt author } }**;

Memory and Load Testing

Test Draft Mode under realistic conditions. A few editors in Draft Mode don't stress your infrastructure, but the Contentful team showed that at scale, simultaneous draft requests can impact performance. Set rate limits:

Code:

// Limit draft requests per IP
const rateLimit = new Map();

function checkDraftRateLimit(ip: string) {
  const limit = rateLimit.get(ip) || 0;
  if (limit > 100) return false; // Max 100 requests per minute
  
  rateLimit.set(ip, limit + 1);
  return true;
}

Security Considerations

Secret Management

Your draft secret is critical:

Code:

// In .env.local (never commit this)
DRAFT_SECRET=your-very-long-and-random-secret-here-at-least-32-chars

// Always verify in your handler
if (req.query.secret !== process.env.DRAFT_SECRET) {
  return res.status(401).json({ message: 'Unauthorized' });
}

HTTPS and Secure Tokens

Always use HTTPS for Draft Mode URLs. Consider using JWT tokens instead of query parameters:

Code:

import jwt from 'jsonwebtoken';

// In CMS webhook
const token = jwt.sign({ editor: true }, process.env.JWT_SECRET, {
  expiresIn: '24h'
});

// In preview URL
const previewUrl = **/api/draft?token=${token}&slug=${slug}**;

Preventing Unauthorized Access

Monitor and log all Draft Mode access:

Code:

async function handler(req, res) {
  const ip = req.headers['x-forwarded-for'];
  
  await logAccess({
    ip,
    timestamp: new Date(),
    slug: req.query.slug,
    success: true
  });

  // Block suspicious patterns
  const recentAttempts = await countRecentAttempts(ip);
  if (recentAttempts > 1000) {
    return res.status(429).json({ message: 'Rate limited' });
  }
}

Troubleshooting Common Issues

IssueSolution
Draft content not showingVerify secret matches .env.local
Stale content in draft modeDisable caching in getStaticProps
Performance degradationAdd rate limiting and request deduplication
CMS webhook not firingCheck webhook URL and verify headers match
Cache not updatingEnsure revalidation path matches page route

For comprehensive implementation details, refer to the official Next.js Draft Mode documentation and explore Contentful's Next.js integration guide for best practices with headless CMS platforms. Also check Vercel's deployment guides for optimization strategies.

FAQ

Q: Can Draft Mode work with App Router? A: Yes. In App Router, use draftMode() from 'next/headers' instead of context properties.

Q: What happens if my CMS is down while in Draft Mode? A: Your page will throw an error. Implement fallback handling to show cached content instead.

Q: Should I publish content directly to production from the CMS? A: No. Always revalidate through Next.js to ensure consistency. Let the CMS trigger webhooks that call your revalidation API.

Q: How do I handle Draft Mode with dynamic routes? A: Use revalidatePath() for dynamic segments: revalidatePath('/blog/[slug]', 'page').

Q: Can I use Draft Mode with static exports? A: No. Draft Mode requires a running server. Static exports are fully pre-built and can't support dynamic preview.

Q: How long can a draft preview URL be active? A: The cookie persists for the browser session. You can control this with sameSite and maxAge options in setDraftMode().

Real-World Example: Blog Publishing Workflow

Here's how we implemented Draft Mode for a client's blog:

  1. Blogger writes post in CMS (marked as draft)
  2. Clicks preview → arrives at /blog/my-new-post?preview=true
  3. Our preview handler validates secret and enables Draft Mode
  4. Post renders with draft banner at the top
  5. Blogger reviews, makes changes in CMS in real-time
  6. Once satisfied, clicks "Publish"
  7. CMS fires webhook to /api/revalidate?type=post&id=123
  8. Webhook calls revalidatePath('/blog/[slug]')
  9. Next.js rebuilds that page's static version
  10. Next visitor gets fresh, fast, static version

Advanced Implementation Patterns

Multi-CMS Support with Draft Mode

Many large organizations use multiple content sources. Here's how to handle that:

Code:

export async function getStaticProps(context: GetStaticPropsContext) {
  const { draftMode } = context;
  const slug = context.params?.slug as string;

  // Determine which CMS to use
  let content;
  try {
    if (context.revalidating && draftMode) {
      // In draft mode, always try fresh data
      content = await fetchFromContentful(slug);
    } else {
      // Fall back to backup source if primary fails
      try {
        content = await fetchFromSanity(slug);
      } catch {
        content = await fetchFromStrapi(slug);
      }
    }
  } catch (error) {
    return { notFound: true };
  }

  return {
    props: { content, draftMode: !!draftMode },
    revalidate: draftMode ? false : 300
  };
}

Incremental Static Regeneration with Draft Mode

For large sites with thousands of pages, ISR prevents long build times:

Code:

export async function getStaticProps(context: GetStaticPropsContext) {
  const post = await fetchPost(context.params?.slug);

  if (!post) {
    return { notFound: true };
  }

  // Don't cache while editing
  if (context.draftMode) {
    return {
      props: { post, draftMode: true },
      revalidate: false
    };
  }

  // Revalidate on-demand for published content
  return {
    props: { post, draftMode: false },
    revalidate: 60 // Revalidate every 60 seconds
  };
}

Team Collaboration Features

Track who's editing what:

Code:

interface DraftSession {
  userId: string;
  postId: string;
  startedAt: Date;
  lastActivity: Date;
}

// API to track active editors
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    const { userId, postId } = req.body;
    
    await recordDraftSession({
      userId,
      postId,
      startedAt: new Date(),
      lastActivity: new Date()
    });

    res.status(200).json({ success: true });
  }

  if (req.method === 'GET') {
    const { postId } = req.query;
    const activeSessions = await getActiveSessions(postId as string);
    res.status(200).json({ activeSessions });
  }
}

Preview Notifications and Webhooks

Alert when previews are viewed:

Code:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.query.secret !== process.env.DRAFT_SECRET) {
    return res.status(401).json({ message: 'Invalid secret' });
  }

  // Enable Draft Mode
  res.setDraftMode({ enable: true });

  // Notify CMS of preview
  const slug = req.query.slug as string;
  await notifyCMS({
    event: 'preview_accessed',
    slug,
    timestamp: new Date(),
    ip: req.headers['x-forwarded-for']
  });

  res.writeHead(307, { Location: **/blog/${slug}** });
  res.end();
}

Analytics and Monitoring

Monitor Draft Mode usage to understand team workflows:

Code:

export async function logDraftAccess(
  slug: string,
  userId: string,
  action: 'preview' | 'exit'
) {
  await analytics.track({
    event: **draft_mode_${action}**,
    properties: {
      slug,
      userId,
      timestamp: new Date(),
      userAgent: navigator.userAgent
    }
  });
}

Track metrics like:

  • Average time in draft mode
  • Preview to publish duration
  • Number of preview iterations
  • Team collaboration patterns

Scaling Draft Mode

For enterprise sites with high traffic to draft pages:

Code:

// Implement caching for draft content during a session
interface DraftCache {
  content: any;
  expiresAt: Date;
}

const draftCache = new Map<string, DraftCache>();

export async function getCachedDraftContent(slug: string) {
  const cached = draftCache.get(slug);
  
  if (cached && cached.expiresAt > new Date()) {
    return cached.content;
  }

  const content = await fetchLatestContent(slug);
  draftCache.set(slug, {
    content,
    expiresAt: new Date(Date.now() + 5 * 60 * 1000) // 5 minute cache
  });

  return content;
}

This reduces database queries during editing without making the preview stale.

Conclusion

Draft Mode represents a maturity point in Next.js that makes it genuinely competitive with traditional WordPress-style workflows. Your editors get instant feedback, your audience gets fast pages, and your infrastructure stays simple.

We've implemented this across dozens of projects—marketing sites, documentation portals, product catalogs—and it consistently improves the content creation experience. The slight setup effort pays for itself within the first few publishing cycles.

The combination of static generation for performance, dynamic preview for iteration, and webhook-driven invalidation creates a system that just works. Your content team will thank you, and your users will thank you with their engagement metrics.

Next.jsCMSDraft ModeISRContentfulSanityTypeScript
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.