Skip to content




Setting up live preview for sanity on next 13.4 using drafts mode

Setting up live preview for sanity on next 13.4 using drafts mode

Set up Sanity live preview with Next.js 13.4 draft mode. Step-by-step guide covering the draftMode API, preview routes, and real-time content updates.


Using Live Preview on Sanity and Next 13.4

Introduction

Have you ever found yourself wondering how your website will look before hitting the publish button? Do you want to make sure everything is perfect before presenting it to the world? Well, now you can with Live Preview on Sanity and Next 13.4.

Live Preview is a feature that allows you to see how your website will look in real-time. It provides you with an interactive preview of your content, so you can make changes and see how they affect the overall design of your website. It is a handy tool for web developers, content creators, and anyone who wants to ensure that their website looks and functions as intended.

In this article, we will explore the benefits of using Live Preview on Sanity and Next 13.4, how it works, and how to get started.

Understanding Sanity and Next 13.4

If you're new to Sanity and Next 13.4, it's essential to have a basic understanding of what they are and how they work together. Sanity is a headless CMS that allows you to create, manage, and distribute content to any platform or device. Next 13.4 is a popular React framework for building web applications. When used together, Sanity and Next 13.4 provide a powerful platform for creating dynamic, responsive websites.

One of the advantages of using Sanity and Next 13.4 is that they allow you to separate the content from the presentation layer. This means that you can focus on creating high-quality content without having to worry about how it will look on the website. The content is stored in Sanity, and Next 13.4 pulls the content from Sanity and uses it to generate the website.

Setting up Live Preview

On the sanity side,

we use a custom preview component.

There are three things to take care of.

  1. Resolving preview URLs
  2. sanity desk structure
  3. preview component

Resolving preview URLs

typescript
// resolveProductionUrl.ts
import type { SanityDocument } from 'sanity';

// Store secrets in environment variables, never in source code
const previewSecret = process.env.SANITY_PREVIEW_SECRET!;
const remoteUrl = process.env.NEXT_PUBLIC_SITE_URL!;
const localUrl = process.env.NEXT_PUBLIC_LOCAL_URL || 'http://localhost:3000';

function getSlug(slug: any) {
  if (!slug) return '/';
  if (slug.current) return slug.current;
  return '/';
}

export default function resolveProductionUrl(doc: SanityDocument) {
  const baseUrl =
    window.location.hostname === 'localhost' ? localUrl : remoteUrl;
  const previewUrl = new URL(baseUrl);
  const slug = doc.slug;
  previewUrl.pathname = `/api/draft`;
  previewUrl.searchParams.append(`secret`, previewSecret);
  previewUrl.searchParams.append(`slug`, getSlug(slug));
  return previewUrl.toString().replaceAll('%2F', '/');
}

Sanity Desk Structure

typescript
// structure.ts
import { FaHome, FaFile, FaFileWord, FaQuestionCircle } from 'react-icons/fa';
import { StructureBuilder } from 'sanity/desk';
import { PreviewIFrame } from './component/preview';


export const structure = (S: StructureBuilder) =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Main Page')
        .icon(FaHome)
        .child(
          S.document()
            .views([
              S.view.form(),
              S.view
                .component(PreviewIFrame)
                .options({ test: 'ss' })
                .title('Preview'),
            ])
            .schemaType('mainPage')
            .documentId('mainPage'),
        ),
      S.divider(),
      S.documentTypeListItem('page').title('Page').icon(FaFile),
      S.documentTypeListItem('blog').title('Blog').icon(FaFileWord),
      S.documentTypeListItem('faq').title('FAQs').icon(FaQuestionCircle),
    ]);

export const defaultDocumentNode = (S: StructureBuilder) =>
  S.document().views([
    S.view.form(),
    S.view.component(PreviewIFrame).options({}).title('Preview'),
  ]);

Preview Component

tsx
// studio/component/preview.tsx
import {
  Box,
  Button,
  Card,
  Flex,
  Spinner,
  Text,
  ThemeProvider,
} from '@sanity/ui';
import { AiOutlineReload } from 'react-icons/ai';
import { BiLinkExternal } from 'react-icons/bi';
import { useEffect, useState, useRef } from 'react';
import resolveProductionUrl from '../resolveProductionUrl';

export function PreviewIFrame(props: any) {
  const { options, document } = props;
  const [id, setId] = useState(1);
  const { displayed } = document;
  const [displayUrl, setDisplayUrl] = useState('');
  const iframe = useRef<HTMLIFrameElement>(null);

  function handleReload() {
    if (!iframe?.current) return;
    setId(id + 1);
  }
  useEffect(() => {
    function getUrl() {
      const productionUrl = resolveProductionUrl(displayed) ?? '';
      setDisplayUrl(productionUrl);
    }
    getUrl();
  }, [displayed]);

  if (displayUrl === '')
    return (
      <ThemeProvider>
        <Flex padding={5} align="center" justify="center">
          <Spinner />
        </Flex>
      </ThemeProvider>
    );

  return (
    <ThemeProvider>
      <Flex direction="column" style={{ height: `100%` }}>
        <Card padding={2} borderBottom>
          <Flex align="center" gap={2}>
            <Box flex={1}>
              <Text size={0} textOverflow="ellipsis">
                {displayUrl}
              </Text>
            </Box>
            <Flex align="center" gap={1}>
              <Button
                fontSize={[1]}
                padding={2}
                icon={AiOutlineReload}
                title="Reload"
                text="Reload"
                aria-label="Reload"
                onClick={() => handleReload()}
              />

              <Button
                fontSize={[1]}
                icon={BiLinkExternal}
                padding={[2]}
                text="Open"
                tone="primary"
                onClick={() => window.open(displayUrl)}
              />
            </Flex>
          </Flex>
        </Card>
        <Card tone="transparent" padding={0} style={{ height: `100%` }}>
          <Flex align="center" justify="center" style={{ height: `100%` }}>
            <iframe
              key={id}
              ref={iframe}
              title="preview"
              style={{ width: '100%', height: `100%`, maxHeight: `100%` }}
              src={displayUrl}
              referrerPolicy="origin-when-cross-origin"
              frameBorder={0}
            />
          </Flex>
        </Card>
      </Flex>
    </ThemeProvider>
  );
}

No spam, only good stuff

Subscribe, for more hot takes

Only god knows why anybody would purposefully subscribe themselves to a newsletter that moans about development. These poor souls did though
Profile 1
Profile 2
Profile 3
Profile 4
Profile 5

On the Next.js Side of things

creating an API route to configure the draft mode

enabling drafts mode

typescript
// src/app/api/draft/route.ts
import { draftMode } from 'next/headers';

import { SANITY_PREVIEW_SECRET } from '~/config';
import { nativeRedirect } from '~/lib/utils';

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

  // SANITY_PREVIEW_SECRET is the same secret from above resolve preview url.
  if (secret !== SANITY_PREVIEW_SECRET || !slug) {
    return new Response('Invalid token', { status: 401 });
  }

  // Validate slug to prevent open redirects - must be a relative path
  if (!slug.startsWith('/') || slug.startsWith('//') || slug.includes('://')) {
    return new Response('Invalid slug', { status: 400 });
  }

  const draft = await draftMode();

  draft.enable();

  return nativeRedirect(slug);
}

disabling draft mode

typescript
// src/app/api/disable-draft/route.ts
import { draftMode } from 'next/headers';

import { nativeRedirect } from '~/lib/utils';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  const draft = draftMode();

  draft.disable();

  return nativeRedirect('/');
  // there was a bug using redirect on this next version 
  // but now its patched up you can simply use 
  // redirect from "next/navigation";
}

As there was a bug on using redirects on next 13.4 which is fixed by the way, that made me use this custom redirect helper

Services
$ Sanity CMS development
Live preview, visual editing, and Sanity + Next.js integration for real-time content workflows.
>Learn more
typescript
// lib/utils

export const nativeRedirect = (path: string) =>
  new Response(null, {
    status: 307,
    headers: {
      Location: path,
    },
  });

using Preview on the route.

typescript
import { Metadata } from 'next';
import { draftMode } from 'next/headers';
import { notFound } from 'next/navigation';
import { getMetaData } from '~/helper';
import { getClient } from '~/lib/sanity';
import { mainPageQuery } from '~/lib/sanity.query';
import { MainPage } from '~/schema';
import { MainPageBlock } from './component';
import { PreviewWrapper } from './preview';

const getMainPageData = (preview?: boolean) =>
  getClient(preview).fetch<MainPage>(mainPageQuery);

export const generateMetadata = async (): Promise<Metadata> => {
  const data = await getMainPageData();
  if (!data) return {};
  return getMetaData(data);
};

export default async function HomePageWrapper() {
  const { isEnabled } = draftMode();

  const mainPage = await getMainPageData(isEnabled);

  if (!mainPage) notFound();

  if (!isEnabled) return <MainPageBlock data={mainPage} />;

  return (
    <PreviewWrapper
      initialData={mainPage}
      query={mainPageQuery}
      queryParams={{}}
    />
  );
}

Live Preview in Action

Frequently asked questions

What is live preview in Sanity and Next.js?
Live preview lets you see how your website will look in real time before you hit publish. Content is stored in Sanity, Next.js pulls it to generate the site, and the preview shows draft changes in an interactive view so you can check everything before presenting it to the world.
How do I set up live preview on the Sanity side?
There are three pieces on the Sanity side: a resolveProductionUrl function that builds the preview URL, a desk structure that adds a Preview view next to the form view, and a custom PreviewIFrame component that loads that URL in an iframe with reload and open buttons.
How does the Sanity preview URL get built?
resolveProductionUrl takes the document, picks the local or remote base URL depending on the hostname, points the path at /api/draft, and appends a secret and the document slug as search parameters. The secret comes from an environment variable, never from source code.
How do I enable draft mode in Next.js 13.4?
Create an API route at src/app/api/draft/route.ts. It reads the secret and slug from the query string, returns a 401 if the secret does not match, rejects slugs that are not relative paths to prevent open redirects, calls draftMode().enable(), then redirects to the slug.
How do I disable draft mode in Next.js?
Add a second route at src/app/api/disable-draft/route.ts that calls draftMode().disable() and redirects back to the homepage. It mirrors the enable route with the reverse action.
Why do we use a custom redirect helper instead of redirect from next/navigation?
At the time, Next 13.4 had a bug with redirect in route handlers, so we used a small nativeRedirect helper that returns a Response with status 307 and a Location header. The bug has since been patched, so redirect from next/navigation now works.
How does a page use draft mode to show preview content?
The page reads isEnabled from draftMode() and passes it to the Sanity client so drafts are fetched when preview is on. If preview is off it renders the normal page component; if it is on, it renders a PreviewWrapper with the initial data, the GROQ query and its params.

About the author

Hrithik Prasad

Tech Lead with expertise in React, Next.js, and Sanity CMS. Loves building performant web applications and sharing knowledge through technical content.



Related posts





Related videos





Get in touch

Tell us what you're building. We reply within one working day. Jono or someone on the team picks up every message personally.

By sending this you agree to our privacy policy. We only use your details to reply.