Unlock seamless workflows and faster delivery with our latest releases - Join the deep dive

Implementing Visual Editing in an existing Next.js-project

Implement Visual Editing in an existing Next.js project using the app router and, optionally, the pages router.

Introduction

Visual Editing is an umbrella term for a set of features that enable content editors to work with live front-end previews within Sanity Studio, faithfully demonstrating how their drafted changes will appear. While other resources cover how to spin up a new project with a variety of front-end frameworks, this guide targets the specific and common scenario of adding Visual Editing to an existing Next.js 14 /Sanity app using the app router. If this is not your situation, see the list below for suggestions on where to go for a fresh start. If it is, read on.

In the interest of brevity and practicality, this article will focus on the key steps needed to implement Visual Editing without diving too deep into the underlying concepts and APIs. However, links to further reading on these topics are provided throughout for those who wish to learn more.

Protip

Still too wordy? To speed-run this guide, look for the pointing finger emoji 👉 to quickly find the next actionable step.

Prerequisites and presumptions

This article is aimed at developers who are at least somewhat familiar with both Sanity and Next.js, and wish to add Visual Editing to an existing code base without refactoring the entire project. The following starting setup is presumed:

Outline

The tasks at hand have been divided into two main sections – the studio and the front end. The first section will take you through all the steps needed to enable Visual Editing in your studio, and the second will do the same for your Next.js front-end application.

Visual Editing in Sanity Studio

Visual Editing in Next.js

Set up Presentation in Sanity Studio

In this first part, you'll add the Presentation tool to your studio configuration, which will provide a live interactive preview of your front end within your studio. Here are the steps we'll cover:

  • Installing dependencies
  • Setting up a token with viewer permissions in project settings
  • Configuring the presentationTool in sanity.config.ts
  • Creating a locations resolver function to map Sanity documents to their respective front-end routes

Dependencies

Everything we need to enable Visual Editing in the studio comes with the core Sanity Studio-package, so your first step is to make sure your studio is up to date.

👉 Update Sanity Studio by running this command in your CLI

npm install sanity@latest

Add and configure the Presentation tool

Next you'll need to add the presentationTool to your main studio configuration. This tool needs to know a few things about your front end:

  • Where does it live? I.e., what is its origin URL?
  • What are the endpoints to enable or disable previewMode?

It'll be a little while before we revisit the endpoints mentioned in the second bullet, but the gist of it is that we will define certain URLs in our front end – or "endpoints" – that when visited will enable or disable preview mode. The endpoints we specify below don't actually exist yet. You'll set them up in a later step in this article.

👉 Configure the presentationTool as outlined below

In your sanity.config.ts file, import the presentationTool from sanity/presentation and add it to the plugins array in your studio config.

// sanity.config.ts
import {defineConfig} from 'sanity'
import {presentationTool} from 'sanity/presentation'
export default defineConfig({ // ...rest of config plugins: [
presentationTool({
previewUrl: {
origin: 'https://my-cool-site.com',
previewMode: {
enable: '/api/draft-mode/enable',
disable: '/api/draft-mode/disable',
},
},
}),
// ...more plugins ], })

Protip

If you are working with an embedded studio you can skip the origin property on line 10 of the example.

Map content to front-end routes with locations resolver function

The content of a Sanity document can be used in multiple places across your front end. In the example shown below a post's title appers both in the individual post route – /posts/hello-world – and in a list of all posts on the home page.

To show where its content is used and can be previewed within a document form, you need to pass a location resolver function to the presentation tool configuration that maps document types to their front-end routes.

The locations resolver lets the studio know where content appears in front end

👉 Create ./lib/presentation/resolve.ts and add route definitions for your document types

Coupling document types to front-end routes is done using a pattern that might look familiar if you've ever configured list previews for document types. For each type we first select the fields we need to do our mapping, and then use those values to determine one or more locations. In the example below, we are mapping documents of type post to a route matching the pattern /posts/${slug}, and also adding it to the top-level index.

// ./presentation/resolve.ts

import {
  defineLocations,
  PresentationPluginOptions,
} from "sanity/presentation";

export const resolve: PresentationPluginOptions["resolve"] = {
  locations: {
    // Add locations for documents of type 'post'
    post: defineLocations({
      // Select one or more fields
      select: {
        title: "title",
        slug: "slug.current",
      },
      // Those fields are available in the resolve callback function
      resolve: (doc) => ({
        locations: [
          {
            title: doc?.title || "Untitled",
            href: `/posts/${doc?.slug}`,
          },
          { title: "Home", href: `/` },
        ],
      }),
    }),
  },
};

👉 Import the resolver function into sanity.config.ts and pass it to the presentationTool config

import {defineConfig} from 'sanity'
import {presentationTool} from 'sanity/presentation'
import resolveProductionUrl from './lib/presentation/resolve'
export default defineConfig({ // ... plugins: [ presentationTool({
resolve: resolveProductionUrl,
previewUrl: { origin: 'https://my-cool-site.com', previewMode: { enable: '/api/draft-mode/enable', disable: '/api/draft-mode/disable', }, }, }), ], })

Et voila! The Sanity Studio setup for Visual Editing is complete! 🎉


If you run into any issues, double-check that your token has the correct permissions and that the presentationTool config matches the front-end routes you'll set up next (no pun intended).

Set up Visual Editing in Next.js

In this section, you'll set up Visual Editing in your Next.js project. This entails:

  • Adding and updating the required dependencies
  • Create a Sanity access token with viewer privileges
  • Creating API routes to enable and disable preview mode with the Next.js draftMode()-function
  • Configuring the Sanity client to use the previewDrafts perspective and enable stega encoding when draftMode().isEnabled is true
  • Conditionally rendering the <VisualEditing /> component based on the state of draftMode()

By the end of this section, you should be up and running with Visual Editing.

Add / update dependencies

In order to enable Visual Editing in your frontend you'll need to install the @sanity/visual-editing and @sanity/preview-url-secret packages. It's a good idea to also make sure you are using the latest version of next-sanity while you're at it.

👉 Install dependencies by running the following command in your CLI

npm install next-sanity @sanity/visual-editing @sanity/preview-url-secret

Create or repurpose viewer token

Visual Editing requires a token with viewer permissions to fetch draft content. You may already be using an appropriate token in your front end; if so you may skip the next step and re-use your existing token. If you do not have a token already in use or simply prefer to create a dedicated token for presentation purposes, read on.

👉 Create a viewer token by following the instructions below

  • Navigate to your project settings in sanity.io/manage
  • Navigate to the API page and scroll down to the Tokens section
  • Create a token with Viewer permissions
  • Make sure to copy the token when it is displayed, as it will not be shown again


👉 Add the viewer token you just created as an environment variable

# ./.env.local

SANITY_API_READ_TOKEN=your-token-here

Gotcha

After adding an environment variable, you may need to restart your development server for changes to take effect.

Create API routes to enable and disable preview mode

Previously, while setting up our studio, we defined two API endpoints that the Presentation tool will ping to enable and disable preview mode. You'll now actually create these routes in your front end and use the built-in draftMode() function in Next.js to activate and de-activate preview mode.

👉 Add the following files to your Next.js project

  • app/api/draft-mode/enable/route.ts
  • app/api/draft-mode/disable/route.ts

Enable preview mode

In short, this code will use the validatePreviewUrl function from the @sanity/preview-url-secret package to ensure that the incoming request should be allowed, and when thus satisfied, execute the draftMode().enable() function from Next.js to put the website in preview mode.

👉 Add the following code to app/api/draft-mode/enable/route.ts

// app/api/draft-mode/enable/route.ts

import {validatePreviewUrl} from '@sanity/preview-url-secret'
import {draftMode} from 'next/headers'
import {NextRequest, NextResponse} from 'next/server'

export async function GET(request: NextRequest) {
  const {isValid, redirectTo = '/'} = await validatePreviewUrl(
    request.url
  )

  if (!isValid) {
    return new Response('Invalid secret', {status: 401})
  }

  draftMode().enable()
  return NextResponse.redirect(new URL(redirectTo, request.url))
}

Disable preview mode

This endpoint will deactivate preview mode. While not strictly necessary, it's good practice to have an explicit route for disabling Presentation features, as it can help prevent any ambiguity around whether you are looking at production content or previewing a draft.


👉 Add the following code to app/api/draft-mode/disable/route.ts

// app/api/draft-mode/disable/route.ts

import {draftMode} from 'next/headers'
import {NextRequest, NextResponse} from 'next/server'

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

Add perspective config to client

When preview mode is enabled, we want to fetch draft content instead of the latest published versions. This is where Sanity's previewDrafts perspective comes in. In short, perspectives is a Content Lake feature that allows the Sanity client to switch between viewing your content in either published or previewDrafts mode. The former returns the latest published version of the relevant document(s), and the latter returns live draft(s) with all pending unpublished changes included.

👉 Configure your Sanity client to use the previewDrafts perspective when draftMode().isEnabled is true

// ./lib/sanity.client.ts

import {createClient} from 'next-sanity'
import {draftMode} from 'next/headers'

const client = createClient({
  // ...
  perspective: draftMode().isEnabled ? 'previewDrafts' : 'published',
  token: process.env.SANITY_API_READ_TOKEN,
})

You'll notice your token from earlier finally being put to good use.

Configure client to fetch stega-enriched content

Stega, derived from steganography, is an encoding method that embeds content source maps into your data, linking front-end elements to their source content in Sanity. Metadata is inserted into the content in the form of invisible, zero-space unicode glyphs. Stega-encoding is crucial to enable the Visual Editing overlays that let you click an element in your front end and have the relevant document open in your studio.

👉 Enable stega encoding by adding the following code to your client config

// ./lib/sanity.client.ts
import {createClient} from 'next-sanity'
import {draftMode} from 'next/headers'

const client = createClient({
  // ...
  perspective: draftMode().isEnabled ? 'previewDrafts' : 'published',
  token: process.env.SANITY_API_READ_TOKEN,
  stega: {
    enabled: draftMode().isEnabled,
    studioUrl: '/studio',
  },
})

Note that stega should only be enabled when preview mode is active. This avoids the encoded data showing up in your production deployments.

Gotcha

The example above shows the typical setup for an embedded studio. If your studio is not embedded in your Next.js app you will have to provide a complete URL and not just a relative path as in the example.

Gotcha

stega-encoded content can be a nuisance if you are trying to evaluate strings in your application logic, as it pollutes the strings with stega junk which may result in unexpected behavior. If you need to use the raw field values, you should use the stegaClean function to strip out any extraneous leftover cruft from stega.

import {stegaClean} from 'next-sanity/stega'


const rawValue = stegaClean(stegaEncodedValue)

Add VisualEditing component to main layout.tsx

Finally, you need to conditionally render the <VisualEditing /> component in your front end app. The following example presumes that you want to add the component to the main layout.tsx file located in the root of your app folder and will thus be applied to all pages in your app. The component is wrapped in a conditional that checks the value of draftMode().isEnabled. This means that the <VisualEditing /> component will only render when preview mode is enabled, allowing you to preview draft content and use the Visual Editing features.

👉 Conditionally render the <VisualEditing />-component by adding the following code to app/layout.tsx

// app/layout.tsx
import {draftMode} from 'next/headers'
import {VisualEditing} from 'next-sanity'

export default function RootLayout({children}: {children: React.ReactNode}) {
  return (
    <html lang="en">
      {/* ... */}
      <body>
        {children}
{draftMode().isEnabled && <VisualEditing />}
</body> </html> ) }

To provide a clear indication that the site is in preview mode, consider adding a button or header to be conditionally rendered along with the <VisualEditing />-component. This will give editors a visual cue that they are previewing unpublished content and provide an easy way to return to the regular view of the site.

👉 Add a button that will let editors exit preview mode

<body>
  {children}
  {draftMode().isEnabled && (
    <>
      <button onClick={() => fetch('/api/draft-mode/disable')}>Exit Draft Mode</button>
      <VisualEditing />
    </>
  )}
</body>

Check that everything works

With the setup complete, run your Next.js and Sanity apps and open the Presentation tool from the Studio. You should see:

  • Your front-end routes mapped in the Presentation Tool
  • Visual Editing overlays linking to your Sanity content fields
  • Changes made in the Studio reflected live in the front-end preview
  • Ability to exit draft mode with the added button

If you encounter any issues, double-check the following:

  • The viewer token is set correctly and has the right permissions
  • The presentationTool config in sanity.config.ts matches your front-end routes
  • stega is enabled only for draft mode
  • The locations resolver function properly maps document types to front-end routes

Conclusion and further reading

Congratulations! You have successfully implemented Visual Editing in your Next.js app. With this powerful feature, your content editors can now make changes directly in the front end and see their content live as they edit.To dive deeper into the concepts covered here, check out the docs:

Or check out this course on sanity.io/learn!

Was this article helpful?