Discussion about resolving an issue with preview mode not logging out properly and causing errors.
I can see you're experiencing a crash when you log out of Sanity Studio while Draft Mode is still active in your Next.js app. The issue is that the Draft Mode cookie persists after logout, but your app throws an error when it can't authenticate, causing the entire application to crash.
The Core Problem
Your onPublicAccessOnly callback in definePreview throws an error when the user isn't authenticated, which crashes your app. According to the Draft Mode documentation, Draft Mode uses cookie-based authentication that persists independently of your Sanity Studio session. When you log out of Studio, the Draft Mode cookie stays active in your browser, but the authentication tokens needed to fetch preview content are no longer valid.
Important Context: Deprecated API
The definePreview and usePreview approach you're using is part of an older preview implementation pattern. Sanity has moved toward newer approaches for live content and preview, including the Live Content API and modern Visual Editing patterns. However, if you're maintaining an existing codebase using this pattern, here's how to fix your immediate issue.
Solution: Handle Authentication Failures Gracefully
Instead of throwing an error in onPublicAccessOnly, you need to handle the unauthenticated state gracefully. Here are the best approaches:
Option 1: Redirect to Disable Draft Mode (Recommended)
Modify your sanity.preview.ts to redirect to a route that disables Draft Mode when authentication fails:
// sanity.preview.ts
"use client"
import { definePreview } from "next-sanity/preview"
import type { UsePreview } from "next-sanity/preview"
import { config } from "./sanity.client"
const onPublicAccessOnly = () => {
// Instead of throwing, redirect to disable draft mode
if (typeof window !== 'undefined') {
window.location.href = '/api/draft/disable'
}
}
export const usePreview: UsePreview = definePreview({
projectId: config.projectId,
dataset: config.dataset,
onPublicAccessOnly
})Then create or verify you have a disable route:
// app/api/draft/disable/route.ts
import { draftMode } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
draftMode().disable()
// Redirect back to the current page or home
const redirectUrl = request.nextUrl.searchParams.get('redirect') || '/'
return NextResponse.redirect(new URL(redirectUrl, request.url))
}This approach automatically cleans up the Draft Mode cookie when authentication fails, preventing the error from recurring and eliminating the need for manual cookie deletion.
Option 2: Silent Return with Cleanup
A simpler approach that doesn't throw an error:
const onPublicAccessOnly = () => {
// Silently handle the error and trigger cleanup
if (typeof window !== 'undefined') {
// Attempt to disable draft mode
fetch('/api/draft/disable').then(() => {
// Optionally reload to show published content
window.location.reload()
})
}
// Return null instead of throwing
return null
}Option 3: Error Boundary with Auto-Recovery
Wrap your preview component in an error boundary that catches authentication failures:
// components/previews/PreviewErrorBoundary.tsx
"use client"
import { Component, ReactNode } from "react"
interface Props {
children: ReactNode
fallback: ReactNode
}
interface State {
hasError: boolean
}
export class PreviewErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error) {
// Check if it's an authentication error
if (error.message.includes("not logged in") || error.message.includes("preview")) {
// Disable draft mode
fetch('/api/draft/disable').then(() => {
window.location.reload()
})
return { hasError: true }
}
throw error // Re-throw if it's a different error
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}Then update your page to use it:
// page.tsx
if (isEnabled) {
return (
<PreviewSuspense fallback={<PageLayout page={page} />}>
<PreviewErrorBoundary fallback={<PageLayout page={page} />}>
<PagePreview query={pageQuery} variables={{ slug: "frontpage" }} />
</PreviewErrorBoundary>
<ExitPreviewMode />
</PreviewSuspense>
)
}Why This Happens
According to Sanity's documentation on presenting and previewing content, when Draft Mode is enabled, Next.js sets a cookie that bypasses static generation and fetches draft content using authenticated queries. The authentication tokens are only valid while you're logged into Studio. When you log out:
- The Studio session ends and tokens are invalidated
- The Draft Mode cookie remains active in your browser
- Your app tries to fetch preview content with invalid credentials
- The
onPublicAccessOnlycallback is triggered - Your current implementation throws an error, crashing the app
Recommended Approach
Option 1 is the cleanest solution because it:
- Automatically cleans up the problematic state by disabling Draft Mode
- Prevents the error from recurring without manual cookie deletion
- Provides a smooth user experience with an automatic redirect
- Keeps your error handling logic simple and centralized
Make sure your "Exit Preview Mode" button also uses the same /api/draft/disable route for consistency.
Consider Upgrading to Live Content API
If you're starting a new project or can refactor your existing one, consider using Sanity's modern Live Content API with the next-sanity toolkit's newer patterns. The Live Content API provides real-time content updates without the complexity of Draft Mode cookie management, making "live by default" content much simpler to implement. This approach eliminates many of the authentication and cookie management issues you're experiencing with the older preview patterns.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.