Handle logout in preview mode: clear cookies/cache to prevent crash

45 replies
Last updated: Dec 2, 2025
How can I solve a problem where when you logout during a Preview mode from the studio and return back to the website that it removes the cookie or cache?I am getting an error after logging out that I have no access to the preview mode because I am not logged in, resulting in the app crashing. I have to manually remove the cookies in order to make it work.


to load resource: the server responded with a status of 404 ()
26488-4e81157118c9fc11.js:1 Error: Unable to load preview as you're not logged in
    at onPublicAccessOnly (9151-fc58c70d1eec9468.js:1:10281)
    at 9151-fc58c70d1eec9468.js:1:12816
    at s.default (9151-fc58c70d1eec9468.js:1:9360)
    at rs (bce60fc1-d12f33ec043fc570.js:9:38883)
    at lg (bce60fc1-d12f33ec043fc570.js:9:54283)
    at iv (bce60fc1-d12f33ec043fc570.js:9:112451)
    at oR (bce60fc1-d12f33ec043fc570.js:9:90087)
    at bce60fc1-d12f33ec043fc570.js:9:89936
    at oD (bce60fc1-d12f33ec043fc570.js:9:89943)
    at ow (bce60fc1-d12f33ec043fc570.js:9:87645)
window.console.error @ 6488-4e81157118c9fc11.js:1
<http://sociaaladvocatenrotterdam.vercel.app/:1|sociaaladvocatenrotterdam.vercel.app/:1>     Failed 

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 = () => {
  throw new Error(`Unable to load preview as you're not logged in`)
}

export const usePreview: UsePreview = definePreview({
  projectId: config.projectId,
  dataset: config.dataset,
  onPublicAccessOnly
})

page.tsx:

import { draftMode } from "next/headers"
import { notFound } from "next/navigation"

import { getPage, getPageMeta } from "@/lib/sanity/sanity.helpers"

import { PageLayout } from "@/components/layout"
import { ExitPreviewMode, MetaData } from "@/components/shared"
import { PagePreview, PreviewSuspense } from "@/components/previews"

import { pageQuery } from "@/lib/queries"

export const generateMetadata = async () => {
  const page = await getPageMeta("frontpage")

  return MetaData({ page })
}

export const revalidate = 60 // revalidate this page every 60 seconds

const HomePage = async () => {
  const { isEnabled } = draftMode()

  const page = await getPage("frontpage")

  if (!page) {
    notFound()
  }

  if (isEnabled) {
    return (
      <PreviewSuspense fallback={<PageLayout page={page} />}>
        <PagePreview query={pageQuery} variables={{ slug: "frontpage" }} />
        <ExitPreviewMode />
      </PreviewSuspense>
    )
  }

  return <PageLayout page={page} />
}

export default HomePage
AI Update

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:

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:

  1. The Studio session ends and tokens are invalidated
  2. The Draft Mode cookie remains active in your browser
  3. Your app tries to fetch preview content with invalid credentials
  4. The onPublicAccessOnly callback is triggered
  5. Your current implementation throws an error, crashing the app

Option 1 is the cleanest solution because it:

  1. Automatically cleans up the problematic state by disabling Draft Mode
  2. Prevents the error from recurring without manual cookie deletion
  3. Provides a smooth user experience with an automatic redirect
  4. 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.

Show original thread
45 replies

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.

Was this answer helpful?