# Course: Integrated Visual Editing with Next.js
https://www.sanity.io/learn/course/visual-editing-with-next-js

The ultimate upgrade for content authors is to have absolute confidence in the impact of their work before they press publish – as well as the tools to rapidly find and update even the most minor pieces of content.

---

## Navigation

**Track:** [Work-ready Next.js](https://www.sanity.io/learn/track/work-ready-next-js) · [View as markdown](https://www.sanity.io/learn/track/work-ready-next-js.md)

## Contents

1. [Understanding Visual Editing](https://www.sanity.io/learn/course/visual-editing-with-next-js/understanding-visual-editing) · [markdown](https://www.sanity.io/learn/course/visual-editing-with-next-js/understanding-visual-editing.md)
2. [Token handling and security](https://www.sanity.io/learn/course/visual-editing-with-next-js/token-handling-and-security) · [markdown](https://www.sanity.io/learn/course/visual-editing-with-next-js/token-handling-and-security.md)
3. [Receiving live edits to drafts](https://www.sanity.io/learn/course/visual-editing-with-next-js/fetching-preview-content-in-draft-mode) · [markdown](https://www.sanity.io/learn/course/visual-editing-with-next-js/fetching-preview-content-in-draft-mode.md)
4. [Configuring Presentation](https://www.sanity.io/learn/course/visual-editing-with-next-js/configuring-presentation) · [markdown](https://www.sanity.io/learn/course/visual-editing-with-next-js/configuring-presentation.md)
5. [Setup document locations](https://www.sanity.io/learn/course/visual-editing-with-next-js/setup-document-locations) · [markdown](https://www.sanity.io/learn/course/visual-editing-with-next-js/setup-document-locations.md)
6. [Add drag-and-drop elements](https://www.sanity.io/learn/course/visual-editing-with-next-js/add-drag-and-drop-elements) · [markdown](https://www.sanity.io/learn/course/visual-editing-with-next-js/add-drag-and-drop-elements.md)
7. [Conclusion](https://www.sanity.io/learn/course/visual-editing-with-next-js/conclusions) · [markdown](https://www.sanity.io/learn/course/visual-editing-with-next-js/conclusions.md)

---

## Lesson 1: Understanding Visual Editing
https://www.sanity.io/learn/course/visual-editing-with-next-js/understanding-visual-editing

Visual Editing is powered by a combination of Sanity features, which is helpful to understand before implementation.

> [Video: Understanding Visual Editing](https://www.sanity.io/learn/course/visual-editing-with-next-js/understanding-visual-editing)

Content creators will find it highly beneficial to preview the impact of their work before pressing publish. An interactive live preview will give them greater confidence to do so.



Visual Editing is the catch-all term for the ability for content creators to make and see the impact of content changes in real-time, even when working on draft documents. It also describes navigating the website to find and edit content instead of browsing through document lists in the Sanity Studio Structure tool.



## Goals of this course



Once you have completed this course, you will:



- Know how to create, store, and access Sanity project tokens so your application can query for private documents such as draft documents.

- Enable Next.js "draft mode" for authenticated users to put your application into a dynamic state.

- Configure the Presentation plugin to browse and edit the application within Sanity Studio.

- In the Studio, configure document "locations" so content creators can move quickly between the Structure and Presentation tools.

- Switch data fetching to React Loader for enhanced Visual Editing with faster previews.


### Glossary



The following terms describe the functions that combine to create an interactive live preview: [**Visual Editing**](https://www.sanity.io/docs/introduction-to-visual-editing).



Visual Editing can be enabled on **any** hosting platform or [front end](https://www.sanity.io/glossary/front-end) framework.



- [**Perspectives**](https://www.sanity.io/docs/perspectives) modify queries to return either draft or published content. These are especially useful for server-side fetching to display draft content on the initial load when previewing drafts.

- [**Content Source Maps**](https://www.sanity.io/docs/content-source-maps) aren't something you'll need to interact with directly, but they are used by Stega encoding (below) when enabled. They are an extra response from the [Content Lake](https://www.sanity.io/docs/datastore) that notes the full path of every field of returned content.

- [**Stega encoding**](https://www.sanity.io/docs/stega) is when the Sanity Client takes Content Source Maps and combines every field of returned content with an invisible string of characters, which contains the full path from the content to the field within its source document.

- [**Overlays**](https://www.sanity.io/docs/visual-editing-overlays) are created by a dedicated package that looks through the DOM for these Stega encoded strings and creates clickable links to edit documents.

- [**Presentation**](https://www.sanity.io/docs/presentation) is a plugin included with Sanity Studio to simplify displaying a front end inside an iframe with an adjacent document editor. It can communicate directly with the front end instead of making round-trips to the Content Lake for faster live preview.

- [**Draft mode**](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode) is a Next.js-specific way of enabling, checking, and disabling a global variable available during requests, primarily used to make your application query draft content.

- In other frameworks, you might replace this with an environment variable, cookie, or session.


Let's get started.



---

## Lesson 2: Token handling and security
https://www.sanity.io/learn/course/visual-editing-with-next-js/token-handling-and-security

To access draft content your application will need to be authenticated with a token. Learn how to do this securely.

> [Video: Token handling and security](https://www.sanity.io/learn/course/visual-editing-with-next-js/token-handling-and-security)

In a public dataset, documents are kept private in the Content Lake when they have a period (`.`) in the `_id` attribute. For example, draft document IDs begin with a `drafts.` prefix. 



Authentication will also be required to use the `previewDrafts` "perspective," a method of performing a GROQ query that returns the latest draft version of a document instead of an already-published document.



> [!TIP]
> Learn more about [Perspectives for Content Lake](https://www.sanity.io/learn/content-lake/perspectives) in the documentation



To view draft content, requests to the Content Lake require authentication. 



On the client side, the same credentials that allow authors to log in to Sanity Studio will handle this. On the server side, an API token will be required.



> [!TIP]
> Learn more about [Authentication and tokens](https://www.sanity.io/learn/content-lake/http-auth) in the documentation



## Creating an API token



Access tokens can be created from Manage or the API. 



You can access Manage for your project either from the menu at the top left of your Studio:



![Sanity Studio with "Manage project" button selected](https://cdn.sanity.io/images/3do82whm/next/58a1805b2385a3677dd409e4381e7207eb9e0ecf-2240x1488.png)

Or you can automatically open your browser to the Manage page of your project from the command line:



```
pnpm dlx sanity manage
```

- [ ] In Manage, go to the "API" tab and create a token with "Viewer" permissions


![Creating a new token in Manage](https://cdn.sanity.io/images/3do82whm/next/fb7030e01dc7102aae21a597db2b724a137596b0-2240x1488.png)

- [ ] **Update **your `.env.local` file to include the token


```:.env.local
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="your-dataset-name"

# 👇 add this line
SANITY_API_READ_TOKEN="your-new-token"
```

> [!WARNING]
> It is your responsibility to secure this token. Unencrypted access could allow a user to read any document from any dataset in your project. The way it is implemented in this course should never lead to it being included in your code bundle.



You may need to restart your development environment to make the token available. The file below will throw an error if the token is not found in your environment variables.



- [ ] **Create** a new file to store, protect, and export this token


```typescript:src/sanity/lib/token.ts
export const token = process.env.SANITY_API_READ_TOKEN

if (!token) {
  throw new Error('Missing SANITY_API_READ_TOKEN')
}
```

Now the token can be exported from a reliable location. In the next lesson you'll add it to the `defineLive` function.







---

## Lesson 3: Receiving live edits to drafts
https://www.sanity.io/learn/course/visual-editing-with-next-js/fetching-preview-content-in-draft-mode

Add perspectives to your Sanity data fetches to query for draft content, when Draft Mode is enabled.

> [Video: Receiving live edits to drafts](https://www.sanity.io/learn/course/visual-editing-with-next-js/fetching-preview-content-in-draft-mode)

For interactive live preview to be truly immersive, the same fast, cached web application your end users interact with must be put into an API-first, fully dynamic state. Thankfully, Next.js provides "Draft Mode."



> [!TIP]
> See the Next.js documentation for more details on [Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode).



For Visual Editing to work, the entire application must act differently when Draft Mode is enabled. Queries must use a different perspective and entirely skip the cache. Additional UI will be rendered into the page for clickable overlays. Thankfully this complexity is handled inside `SanityLive` and another component you'll import called `VisualEditing`.



## Fetching in draft mode



First you'll need to update the content fetching functions to apply token authentication and settings required for Visual Editing.



### Update Sanity Client



The update below adds Stega encoding to the Sanity Client configuration. This will only be used when Draft Mode is enabled. The URL is used to create clickable links in the preview, which open to the correct document and field from which the content came.



- [ ] **Update** the Sanity Client file to include Stega encoding


```typescript:src/sanity/lib/client.ts
export const client = createClient({
  projectId,
  dataset,
  apiVersion,
  useCdn: true,
  stega: { studioUrl: '/studio' },
})
```

### Update live mode helpers



The token you created in the previous lesson will now need to be passed to the live mode helpers, so that live draft content will be sent to the browser.



These tokens will only be used when the site is in Draft Mode, which is only enabled by users in the Presentation tool in the Studio, or by anyone you share a preview link with. 



The token is not stored in the production app code.



- [ ] **Update** the live mode helpers to set a `browserToken` and `serverToken`


```typescript:src/sanity/lib/live.ts
import { client } from "@/sanity/lib/client";
import { token } from "@/sanity/lib/token"
import { defineLive } from "next-sanity/live";

export const { sanityFetch, SanityLive } = defineLive({
  client,
  browserToken: token,
  serverToken: token,
});
```

## Powering Visual Editing in Draft Mode



Interactive live preview works by listening client-side to changes from your dataset and, when detected, prefetching data server-side. The machinery to do this can be configured manually if you like, but it gets a little complicated, so thankfully, it's been packaged up for us in `next-sanity`.



When Draft Mode is enabled, it's helpful to have a button to disable it.



- [ ] **Create** a component to allow a user to disable Draft Mode


```tsx:src/components/disable-draft-mode.tsx
'use client'

import { useIsPresentationTool } from 'next-sanity/hooks'

export function DisableDraftMode() {
  const isPresentationTool = useIsPresentationTool()

  // Only show the disable draft mode button when outside of Presentation Tool
  if (isPresentationTool === null && isPresentationTool === true) {
    return null
  }

  return (
    <a
      href="/api/draft-mode/disable"
      className="fixed bottom-4 right-4 bg-gray-50 px-4 py-2"
    >
      Disable Draft Mode
    </a>
  )
}
```

To power Visual Editing, all you need is one import.



- [ ] **Update** your root layout to import the `VisualEditing` component from `next-sanity/visual-editing`


```tsx:src/app/(frontend)/layout.tsx
import { draftMode } from 'next/headers'
import { VisualEditing } from 'next-sanity/visual-editing'
import { DisableDraftMode } from '@/components/disable-draft-mode'
import { Header } from '@/components/header'
import { SanityLive } from '@/sanity/lib/live'

export default async function FrontendLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <section className="bg-white min-h-screen">
      <Header />
      {children}
      <SanityLive />
      {(await draftMode()).isEnabled && (
        <>
          <DisableDraftMode />
          <VisualEditing />
        </>
      )}
    </section>
  )
}
```

## Activating draft mode



The Presentation tool maintains an automatically rotating secret stored in the dataset. This is so your Next.js application can confirm that same secret before proceeding with any attempt to enable draft mode.



Thankfully, this entire handshake has been made into a simple helper function from `next-sanity`.



- [ ] **Create** a new API route to enable draft mode


```typescript:src/app/api/draft-mode/enable/route.ts
/**
 * This file is used to allow Presentation to set the app in Draft Mode, which will load Visual Editing
 * and query draft content and preview the content as it will appear once everything is published
 */

import { defineEnableDraftMode } from 'next-sanity/draft-mode'
import { client } from '@/sanity/lib/client'
import { token } from '@/sanity/lib/token'

export const { GET } = defineEnableDraftMode({
  client: client.withConfig({ token }),
})
```

### Disabling draft mode



Once your browser is authenticated to view the web application in draft mode, you will see it in all other tabs in that browser.



The earlier update to the root layout included a button to disable preview mode. This is useful when content authors have finished their changes and want to see the application with the same published content that end users will see.



- [ ] **Create** a new API route to disable draft mode


```typescript:src/app/api/draft-mode/disable/route.ts
import { draftMode } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  ;(await draftMode()).disable()
  return NextResponse.redirect(new URL('/', request.url))
}

```

Your app is ready to start receiving draft content updates—the next step is to actually make that happen. It's easiest to do this within the Presentation plugin; let's set that up in the next lesson.



---

## Lesson 4: Configuring Presentation
https://www.sanity.io/learn/course/visual-editing-with-next-js/configuring-presentation

Install and configure the Presentation plugin to enable draft preview and a web preview from within Sanity Studio

> [Video: Configuring Presentation](https://www.sanity.io/learn/course/visual-editing-with-next-js/configuring-presentation)

Everything you have configured so far has been to prepare the site for when it is put into Draft Mode. To do this securely and automatically, you'll install and configure the [Presentation plugin](https://www.sanity.io/docs/configuring-the-presentation-tool). It will handle creating and requesting a URL to enable Draft Mode with a secret string in the URL that is checked in your API route.



It is also the most convenient way to browse and edit the website in Draft Mode, with an iframe displaying an interactive preview inside the Sanity Studio.



First you'll need to add the plugin to your Sanity Studio configuration.



- [ ] **Update** your `sanity.config.ts` file to import the Presentation tool


```typescript:sanity.config.ts
// ...all other imports
import { presentationTool } from 'sanity/presentation'

export default defineConfig({
  // ... all other config settings
  plugins: [
    // ...all other plugins
    presentationTool({
      previewUrl: {
        previewMode: {
          enable: '/api/draft-mode/enable',
        },
      },
    }),
  ],
})
```

Notice how the plugin's configuration includes the "enable" API route you created in the previous lesson. Presentation will visit this route first, confirm an automatically generated secret from the dataset, and if successful, activate draft mode in the Next.js application.



You should now see the Presentation tool in the top toolbar of the Studio or by visiting [http://localhost:3000/studio/presentation](http://localhost:3000/studio/presentation) where you can navigate the site and click on any Sanity content to open the document – and focus the field – from which that content came.



Because of the `previewDrafts` perspective, the post index route now displays a list of draft **and** published post documents. The latest draft content should appear on already published post documents.



Make edits to any content, and you should see them reflected live on the page. 



Success! You now have an interactive live preview conveniently located within your Sanity Studio. Content authors can browse the front end to find pieces of content they need to edit and instantly see the impact of their changes before pressing publish.



We can go deeper. In the next lesson, you'll make the experience of switching between the Structure and Presentation tools even better.



---

## Lesson 5: Setup document locations
https://www.sanity.io/learn/course/visual-editing-with-next-js/setup-document-locations

Showing where in the application the document they're editing may be displayed can help content creators understand the impact of their changes.

> [Video: Setup document locations](https://www.sanity.io/learn/course/visual-editing-with-next-js/setup-document-locations)

The content of a document may be used in multiple places. For example, currently in your blog, a post’s title is shown both on the individual post route and on the post index page.



When viewing a document, to show where its content is used, you can list paths in your web application to generate one-click links to those pages and view them inside the Presentation tool.



- [ ] **Create** a new file for the `resolve` option in the Presentation plugin options:


```typescript:src/sanity/presentation/resolve.ts
import { defineLocations, PresentationPluginOptions } from 'sanity/presentation'

export const resolve: PresentationPluginOptions['resolve'] = {
  locations: {
    // Add more locations for other post types
    post: defineLocations({
      select: {
        title: 'title',
        slug: 'slug.current',
      },
      resolve: (doc) => ({
        locations: [
          {
            title: doc?.title || 'Untitled',
            href: `/posts/${doc?.slug}`,
          },
          { title: 'Posts index', href: `/posts` },
        ],
      }),
    }),
  },
}
```

In the `locations` key above, `post` is the document schema type from which these locations will render. As you build out your web application and content model, you will extend this configuration to include more document types that render routes – or appear on them.



- [ ] **Update** your `sanity.config.ts` file to import the locate function into the Presentation plugin.


```typescript:sanity.config.ts
// ...all other imports
import { resolve } from '@/sanity/presentation/resolve'

export default defineConfig({
  // ... all other config settings
  plugins: [
    // ...all other plugins
    presentationTool({
      resolve,
      previewUrl: {
        draftMode: {
          enable: '/api/draft-mode/enable',
        },
      },
    }),
  ],
})
```

You should now see the locations at the top of all `post`-type documents:



![Sanity Studio with the Presentation tool showing a preview](https://cdn.sanity.io/images/3do82whm/next/dce8bfd1b6739fe4e57f09c68e998907d22448fd-2144x1388.png)

Now, your content authors can seamlessly move between the front-end-focused Presentation tool – and the structure-focused Structure tool.



Believe it or not, we can go *even deeper*. One part of the Presentation tool is disabled: the toggle to switch between Drafts and Published perspectives. To enable that, we need to implement React Loader, which has the added benefit of even faster live previews.



---

## Lesson 6: Add drag-and-drop elements
https://www.sanity.io/learn/course/visual-editing-with-next-js/add-drag-and-drop-elements

Go beyond "click-to-edit" with additional affordances for rearranging arrays in your front end

> [Video: Add drag-and-drop elements](https://www.sanity.io/learn/course/visual-editing-with-next-js/add-drag-and-drop-elements)

### Add "related posts" to your posts



- [ ] **Update** the `post` schema type fields to include an array of "related posts" to render at the bottom of your `post` type documents.


```typescript:src/sanity/schemaTypes/postType.ts
export const postType = defineType({
  // ...all other settings
  fields: [
    // ...all other fields
    defineField({
      name: "relatedPosts",
      type: "array",
      of: [{ type: "reference", to: { type: "post" } }],
    }),
  ],
});
```

- [ ] **Update** your single post query to return the array and resolve any references. 


```typescript:src/sanity/lib/queries.ts
export const POST_QUERY =
  defineQuery(`*[_type == "post" && slug.current == $slug][0]{
  _id,
  title,
  body,
  mainImage,
  publishedAt,
  "categories": coalesce(
    categories[]->{
      _id,
      slug,
      title
    },
    []
  ),
  author->{
    name,
    image
  },
  relatedPosts[]{
    _key, // required for drag and drop
    ...@->{_id, title, slug} // get fields from the referenced post
  }
}`);
```

- [ ] **Update** your types now that the GROQ query has changed.


```sh
pnpm run typegen
```

- [ ] **Create** a new component to render the related Posts


```tsx:src/components/related-posts.tsx
'use client'

import Link from 'next/link'
import { createDataAttribute } from 'next-sanity'
import { useOptimistic } from 'next-sanity/hooks'
import { POST_QUERYResult } from '@/sanity/types'
import { client } from '@/sanity/lib/client'

const { projectId, dataset, stega } = client.config()
export const createDataAttributeConfig = {
  projectId,
  dataset,
  baseUrl: typeof stega.studioUrl === 'string' ? stega.studioUrl : '',
}

export function RelatedPosts({
  relatedPosts,
  documentId,
  documentType,
}: {
  relatedPosts: NonNullable<POST_QUERYResult>['relatedPosts']
  documentId: string
  documentType: string
}) {
  const posts = useOptimistic<
    NonNullable<POST_QUERYResult>['relatedPosts'] | undefined,
    NonNullable<POST_QUERYResult>
  >(relatedPosts, (state, action) => {
    if (action.id === documentId && action?.document?.relatedPosts) {
      // Optimistic document only has _ref values, not resolved references
      return action.document.relatedPosts.map(
        (post) => state?.find((p) => p._key === post._key) ?? post
      )
    }
    return state
  })
  if (!posts) {
    return null
  }
  return (
    <aside className="border-t">
      <h2>Related Posts</h2>
      <div className="not-prose text-balance">
        <ul
          className="flex flex-col sm:flex-row gap-0.5"
          data-sanity={createDataAttribute({
            ...createDataAttributeConfig,
            id: documentId,
            type: documentType,
            path: 'relatedPosts',
          }).toString()}
        >
          {posts.map((post) => (
            <li
              key={post._key}
              className="p-4 bg-blue-50 sm:w-1/3 flex-shrink-0"
              data-sanity={createDataAttribute({
                ...createDataAttributeConfig,
                id: documentId,
                type: documentType,
                path: `relatedPosts[_key=="${post._key}"]`,
              }).toString()}
            >
              <Link href={`/posts/${post?.slug?.current}`}>{post.title}</Link>
            </li>
          ))}
        </ul>
      </div>
    </aside>
  )
}
```

You will notice `data-sanity` attributes being added to the wrapping and individual tags of the list. As well as a useOptimistic hook to apply these changes in the UI immediately, while the mutation in the content lake is still happening.



- [ ] **Update** the `Post` component to include the `RelatedPosts` component.


```tsx:src/components/post.tsx
import Image from "next/image";
import { PortableText } from "next-sanity";

import { Author } from "@/components/author";
import { Categories } from "@/components/categories";
import { components } from "@/sanity/portableTextComponents";
import { POST_QUERYResult } from "@/sanity/types";
import { PublishedAt } from "@/components/published-at";
import { Title } from "@/components/title";
import { urlFor } from "@/sanity/lib/image";
import { RelatedPosts } from "@/components/related-posts";

export function Post(props: NonNullable<POST_QUERYResult>) {
  const {
    _id,
    title,
    author,
    mainImage,
    body,
    publishedAt,
    categories,
    relatedPosts,
  } = props;

  return (
    <article className="grid lg:grid-cols-12 gap-y-12">
      <header className="lg:col-span-12 flex flex-col gap-4 items-start">
        <div className="flex gap-4 items-center">
          <Categories categories={categories} />
          <PublishedAt publishedAt={publishedAt} />
        </div>
        <Title>{title}</Title>
        <Author author={author} />
      </header>
      {mainImage ? (
        <figure className="lg:col-span-4 flex flex-col gap-2 items-start">
          <Image
            src={urlFor(mainImage).width(400).height(400).url()}
            width={400}
            height={400}
            alt=""
          />
        </figure>
      ) : null}
      {body ? (
        <div className="lg:col-span-7 lg:col-start-6 prose lg:prose-lg">
          <PortableText value={body} components={components} />
          <RelatedPosts
            relatedPosts={relatedPosts}
            documentId={_id}
            documentType="post"
          />
        </div>
      ) : null}
    </article>
  );
}
```

Add a few Related Posts to any post document. Now within Presentation, you should be able to drag-and-drop to reorder their position, and see the content change in the Studio.



---

## Lesson 7: Conclusion
https://www.sanity.io/learn/course/visual-editing-with-next-js/conclusions

Let's review

> [Video: Conclusion](https://www.sanity.io/learn/course/visual-editing-with-next-js/conclusions)

With Visual Editing, your content creators have absolute confidence in pressing publish. They also have a more convenient method of browsing your content, using the application to find even the most minor text that needs editing.



Here are a few questions to clarify what you've learned in this course.



> **Question:** What kind of Sanity token is required to enable draft mode?
>
> 1. Admin
> 2. Writer
> 3. Viewer **[correct]**
> 4. Guest

> **Question:** "Draft Mode" is a...
>
> 1. Web standard
> 2. Next.js feature **[correct]**
> 3. Sanity feature
> 4. Networking protocol

> **Question:** Visual Editing can be implemented...
>
> 1. On any framework and hosting **[correct]**
> 2. Only in Next.js but on any hosting
> 3. Only in Next.js and only on Vercel
> 4. Only by enterprise customers

> **Question:** The Presentation tool...
>
> 1. Replaces the Structure tool
> 2. Simplifies Visual Editing setup **[correct]**
> 3. Is required for Visual Editing
> 4. Requires a separate package

> **Question:** Why would you use React Loader?
>
> 1. It is required for Visual Editing
> 2. It sounds cooler
> 3. It enhances Visual Editing **[correct]**
> 4. It's cheaper than Sanity Client

---

## Related Resources

- [Track overview](https://www.sanity.io/learn/track/work-ready-next-js.md)
- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
