Projects
Services
Migration
Blog
Alt textAlt text

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

Reading Time

2 min read

Published on

May 23, 2023

Author

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
1// resolveProductionUrl.ts
2import type { SanityDocument } from 'sanity';
3
4const previewSecret = '__some__secret__text__';
5const remoteUrl = `__PROD__URL__`;
6const localUrl = `http://localhost:3000`;
7
8function getSlug(slug: any) {
9 if (!slug) return '/';
10 if (slug.current) return slug.current;
11 return '/';
12}
13
14export default function resolveProductionUrl(doc: SanityDocument) {
15 const baseUrl =
16 window.location.hostname === 'localhost' ? localUrl : remoteUrl;
17 const previewUrl = new URL(baseUrl);
18 const slug = doc.slug;
19 previewUrl.pathname = `/api/draft`;
20 previewUrl.searchParams.append(`secret`, previewSecret);
21 previewUrl.searchParams.append(`slug`, getSlug(slug));
22 return previewUrl.toString().replaceAll('%2F', '/');
23}
24

Sanity Desk Structure

typescript
1
2// structure.ts
3import { FaHome, FaFile, FaFileWord, FaQuestionCircle } from 'react-icons/fa';
4import { StructureBuilder } from 'sanity/desk';
5import { PreviewIFrame } from './component/preview';
6
7
8export const structure = (S: StructureBuilder) =>
9 S.list()
10 .title('Content')
11 .items([
12 S.listItem()
13 .title('Main Page')
14 .icon(FaHome)
15 .child(
16 S.document()
17 .views([
18 S.view.form(),
19 S.view
20 .component(PreviewIFrame)
21 .options({ tes: 'ss' })
22 .title('Preview'),
23 ])
24 .schemaType('mainPage')
25 .documentId('mainPage'),
26 ),
27 S.divider(),
28 S.documentTypeListItem('page').title('Page').icon(FaFile),
29 S.documentTypeListItem('blog').title('Blog').icon(FaFileWord),
30 S.documentTypeListItem('faq').title('FAQs').icon(FaQuestionCircle),
31 ]);
32
33export const defaultDocumentNode = (S: StructureBuilder) =>
34 S.document().views([
35 S.view.form(),
36 S.view.component(PreviewIFrame).options({}).title('Preview'),
37 ]);
38

Preview Component

tsx
1// studio/component/preview.tsx
2import {
3 Box,
4 Button,
5 Card,
6 Flex,
7 Spinner,
8 Text,
9 ThemeProvider,
10} from '@sanity/ui';
11import { AiOutlineReload } from 'react-icons/ai';
12import { BiLinkExternal } from 'react-icons/bi';
13import { useEffect, useState, useRef } from 'react';
14import resolveProductionUrl from '../resolveProductionUrl';
15
16export function PreviewIFrame(props: any) {
17 const { options, document } = props;
18 const [id, setId] = useState(1);
19 const { displayed } = document;
20 const [displayUrl, setDisplayUrl] = useState('');
21 const iframe = useRef<HTMLIFrameElement>(null);
22
23 function handleReload() {
24 if (!iframe?.current) return;
25 setId(id + 1);
26 }
27 useEffect(() => {
28 function getUrl() {
29 const productionUrl = resolveProductionUrl(displayed) ?? '';
30 setDisplayUrl(productionUrl);
31 }
32 getUrl();
33 }, [displayed]);
34
35 if (displayUrl === '')
36 return (
37 <ThemeProvider>
38 <Flex padding={5} align="center" justify="center">
39 <Spinner />
40 </Flex>
41 </ThemeProvider>
42 );
43
44 return (
45 <ThemeProvider>
46 <Flex direction="column" style={{ height: `100%` }}>
47 <Card padding={2} borderBottom>
48 <Flex align="center" gap={2}>
49 <Box flex={1}>
50 <Text size={0} textOverflow="ellipsis">
51 {displayUrl}
52 </Text>
53 </Box>
54 <Flex align="center" gap={1}>
55 <Button
56 fontSize={[1]}
57 padding={2}
58 icon={AiOutlineReload}
59 title="Reload"
60 text="Reload"
61 aria-label="Reload"
62 onClick={() => handleReload()}
63 />
64
65 <Button
66 fontSize={[1]}
67 icon={BiLinkExternal}
68 padding={[2]}
69 text="Open"
70 tone="primary"
71 onClick={() => window.open(displayUrl)}
72 />
73 </Flex>
74 </Flex>
75 </Card>
76 <Card tone="transparent" padding={0} style={{ height: `100%` }}>
77 <Flex align="center" justify="center" style={{ height: `100%` }}>
78 <iframe
79 key={id}
80 ref={iframe}
81 title="preview"
82 style={{ width: '100%', height: `100%`, maxHeight: `100%` }}
83 src={displayUrl}
84 referrerPolicy="origin-when-cross-origin"
85 frameBorder={0}
86 />
87 </Flex>
88 </Card>
89 </Flex>
90 </ThemeProvider>
91 );
92}
93

On the Next.js Side of things

creating an API route to configure the draft mode

enabling drafts mode

typescript
1// src/app/api/draft/route.ts
2import { draftMode } from 'next/headers';
3
4import { SANITY_PREVIEW_SECRET } from '~/config';
5import { nativeRedirect } from '~/lib/utils';
6
7export async function GET(request: Request) {
8 const { searchParams } = new URL(request.url);
9 const secret = searchParams.get('secret');
10 const slug = searchParams.get('slug');
11
12 // SANITY_PREVIEW_SECRET is the same secret from above resolve preivew url.
13 if (secret !== SANITY_PREVIEW_SECRET || !slug) {
14 return new Response('Invalid token', { status: 401 });
15 }
16
17 const draft = draftMode();
18
19 draft.enable();
20
21 return nativeRedirect(slug);
22}
23

disabling draft mode

typescript
1// src/app/api/disable-draft/route.ts
2import { draftMode } from 'next/headers';
3
4import { nativeRedirect } from '~/lib/utils';
5
6export async function GET(request: Request) {
7 const { searchParams } = new URL(request.url);
8
9 const draft = draftMode();
10
11 draft.disable();
12
13 return nativeRedirect('/');
14 // there was a bug using redirect on this next version
15 // but now its patched up you can simply use
16 // redirect from "next/navigation";
17}
18

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

typescript
1// lib/utils
2
3export const nativeRedirect = (path: string) =>
4 new Response(null, {
5 status: 307,
6 headers: {
7 Location: path,
8 },
9 });

using Preview on the route.

typescript
1import { Metadata } from 'next';
2import { draftMode } from 'next/headers';
3import { notFound } from 'next/navigation';
4import { getMetaData } from '~/helper';
5import { getClient } from '~/lib/sanity';
6import { mainPageQuery } from '~/lib/sanity.query';
7import { MainPage } from '~/schema';
8import { MainPageBlock } from './component';
9import { PreviewWrapper } from './preview';
10
11const getMainPageData = (preview?: boolean) =>
12 getClient(preview).fetch<MainPage>(mainPageQuery);
13
14export const generateMetadata = async (): Promise<Metadata> => {
15 const data = await getMainPageData();
16 if (!data) return {};
17 return getMetaData(data);
18};
19
20export default async function HomePageWrapper() {
21 const { isEnabled } = draftMode();
22
23 const mainPage = await getMainPageData(isEnabled);
24
25 if (!mainPage) notFound();
26
27 if (!isEnabled) return <MainPageBlock data={mainPage} />;
28
29 return (
30 <PreviewWrapper
31 initialData={mainPage}
32 query={mainPageQuery}
33 queryParams={{}}
34 />
35 );
36}
37

Live Preview in Action

Got any questions on live preview ?

Got questions? We've got answers! Whether you're looking to build your first headless website, or you're a seasoned veteran that wants to throw us some hardball questions. See if these answer them. If not, get in touch

Logo

Services

Legal

Like what you see?

Sign up for our newsletter to stay up to date with our latest projects and insights.

© 2025 Roboto Studio Ltd - 11126043

Roboto Studio Ltd,

71-75 Shelton Street,

Covent Garden,

London, WC2H 9JQ

Registered in England and Wales | VAT Number 426637679