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.
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.
- Visual Editing – Documentation overview
- Guide: Visual Editing with Next.JS App Router
- Guide: Visual Editing with Next.JS Pages Router
- Guide: Visual Editing with Remix
- Starter templates
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.
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:
- A Sanity-powered Next.js 14 project using the app router
- Sanity Studio v3.40.0 or later (
@latest
is always recommended)
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.
- Add / update dependencies in your studio
- Add a viewer token to your project in the project settings
- Add and configure the Presentation tool in your studio configuration
- Map document types to front-end routes with a locations resolver function
- Add / update dependencies in your front end
- Add API routes to enable and disable preview mode
- Configure Sanity client
- Perspective: 'previewDrafts'
- Stega-encoded content
- Define canonical
studioUrl
- Conditionally render <VisualEditing /> component in main layout.tsx
- Add support for pages router
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
insanity.config.ts
- Creating a
locations
resolver function to map Sanity documents to their respective front-end routes
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.
npm install sanity@latest
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
ordisable
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.
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.
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.
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 {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).
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 enablestega
encoding whendraftMode().isEnabled
istrue
- Conditionally rendering the
<VisualEditing />
component based on the state ofdraftMode()
By the end of this section, you should be up and running with Visual Editing.
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.
npm install next-sanity @sanity/visual-editing @sanity/preview-url-secret
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.
- 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
# ./.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.
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.
app/api/draft-mode/enable/route.ts
app/api/draft-mode/disable/route.ts
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.
// 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))
}
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.
// 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))
}
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.
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.
// ./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)
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.
<body>
{children}
{draftMode().isEnabled && (
<>
<button onClick={() => fetch('/api/draft-mode/disable')}>Exit Draft Mode</button>
<VisualEditing />
</>
)}
</body>
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 insanity.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
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:
- Visual Editing overview
- Presentation tool
- Perspectives
- Content source maps
- Stega encoding
- Locations resolver API
Or check out this course on sanity.io/learn!