Unlock seamless workflows and faster delivery with our latest releases – get the details

Visual Editing with Next.js App Router

Get started with Sanity Visual Editing in a new or existing Next.js application using App Router.

Following this guide will enable you to:

  • Render overlays in your application, allowing content editors to jump directly from Sanity content to its source in Sanity Studio.
  • Edit your content and see changes reflected in an embedded preview of your application in Sanity’s Presentation tool.
  • Optional: Provide live content updates and seamless switching between draft and published content.

Prerequisites

Next.js application setup

The following steps should be performed in your Next.js application.

Install dependencies

Install the dependencies that will provide your application with data fetching and Visual Editing capabilities.

npm install @sanity/client next-sanity

Set environment variables

Create a .env file in your application’s root directory to provide Sanity specific configuration.

You can use Manage to find your project ID and dataset, and to create a token with Viewer permissions which will be used to fetch preview content.

The URL of your Sanity Studio will depend on where it is hosted or embedded.

# .env

# Public
NEXT_PUBLIC_SANITY_PROJECT_ID="YOUR_PROJECT_ID"
NEXT_PUBLIC_SANITY_DATASET="YOUR_DATASET"
NEXT_PUBLIC_SANITY_STUDIO_URL="https://YOUR_PROJECT.sanity.studio"
# Private
SANITY_VIEWER_TOKEN="YOUR_VIEWER_TOKEN"

Application setup

Configure the Sanity client

Create a Sanity client instance to handle fetching data from Content Lake.

Configuring the stega option enables automatic overlays for basic data types when preview mode is enabled. You can read more about how stega works here.

// src/sanity/client.ts

import { createClient } from "next-sanity";

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
  apiVersion: "2024-12-01",
  useCdn: true,
  token: process.env.SANITY_VIEWER_TOKEN,
  stega: {
    studioUrl: process.env.NEXT_PUBLIC_SANITY_STUDIO_URL,
  },
});

Draft mode

Draft mode allows authorized content editors to view and interact with draft content.

Create an API endpoint to enable draft mode when viewing your application in Presentation tool.

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

import { client } from "@/sanity/client";
import { defineEnableDraftMode } from "next-sanity/draft-mode";

export const { GET } = defineEnableDraftMode({
  client: client.withConfig({
    token: process.env.SANITY_VIEWER_TOKEN,
  }),
});

Create a server action which can be used to disable draft mode. Add a delay to ensure a loading state is shown.

// src/app/actions.ts

'use server'

import {draftMode} from 'next/headers'

export async function disableDraftMode() {
  const disable = (await draftMode()).disable()
  const delay = new Promise((resolve) => setTimeout(resolve, 1000))

  await Promise.allSettled([disable, delay]);
}

Create a new component for disabling draft mode. We will render this for content authors when viewing draft content in a non-Presentation context.

// src/components/DisableDraftMode.tsx

"use client";

import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { disableDraftMode } from "@/app/actions";

export function DisableDraftMode() {
  const router = useRouter();
  const [pending, startTransition] = useTransition();
  
  if (window !== window.parent || !!window.opener) {
    return null;
  }

  const disable = () =>
    startTransition(async () => {
      await disableDraftMode();
      router.refresh();
    });

  return (
    <div>
      {pending ? (
        "Disabling draft mode..."
      ) : (
        <button type="button" onClick={disable}>
          Disable draft mode
        </button>
      )}
    </div>
  );
}

Enable Visual Editing

The <VisualEditing> component handles rendering overlays, enabling click to edit, and refreshing pages in your application when content changes.

Import it into your root layout, and render it conditionally when draft mode is enabled alongside the <DisableDraftMode> component you created above.

// src/app/layout.tsx

import { VisualEditing } from "next-sanity";
import { draftMode } from "next/headers";
import { DisableDraftMode } from "@/components/DisableDraftMode";

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        {children}
        {(await draftMode()).isEnabled && (
          <>
            <VisualEditing />
            <DisableDraftMode />
          </>
        )}
      </body>
    </html>
  );
}

Render a page in preview mode

Add configuration to your client.fetch calls when draft mode is enabled in order to fetch up-to-date preview content with stega encoding.

// ./src/app/[slug]/page.tsx

import { defineQuery } from "next-sanity";
import { draftMode } from "next/headers";
import { client } from "@/sanity/client";

const query = defineQuery(
  `*[_type == "page" && slug.current == $slug][0]{title}`
);

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const { isEnabled } = await draftMode();

  const data = await client.fetch(
    query,
    { slug },
    isEnabled
      ? {
          perspective: "previewDrafts",
          useCdn: false,
          stega: true,
        }
      : undefined
  );

  return <h1>{data.title}</h1>;
}

Studio setup

To setup Presentation tool in your Sanity Studio, import the tool from sanity/presentation, add it to your plugins array, and configure previewUrl, optionally passing an origin, path, and endpoints to enable and disable preview mode.

We similarly recommend using environment variables loaded via a .env file to support development and production environments.

// sanity.config.ts

import { defineConfig } from "sanity";
import { presentationTool } from "sanity/presentation";

export default defineConfig({
  // ... project configuration
  plugins: [
    presentationTool({
      previewUrl: {
        origin: process.env.SANITY_STUDIO_PREVIEW_ORIGIN,
        preview: "/",
        previewMode: {
          enable: "/api/draft-mode/enable",
        },
      },
    }),
    // ... other plugins
  ],
});

Optional Extras

Live Content API

Gotcha

The Live Content API is currently considered experimental and may change in the future.

The Live Content API can be used to receive real time updates in your application when viewing both draft content in contexts like Presentation tool, and published content in your user-facing production application.

Implementing Visual Editing using the Live Content API is recommended for the best experience for both users and content editors.

Update Sanity client

First, update your client configuration. The token can be removed from the base client instance as we pass it as configuration in the next step.

// src/sanity/client.ts

import { createClient } from "next-sanity";

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
  apiVersion: "2024-12-01",
  useCdn: true,
--  token: process.env.SANITY_VIEWER_TOKEN,
  stega: {
    studioUrl: process.env.NEXT_PUBLIC_SANITY_STUDIO_URL,
  },
});

Configure defineLive

Configure defineLive to enable automatic revalidation and refreshing of your fetched content.

The Viewer token can be used as both browserToken and serverToken, as the browserToken is only shared with the browser when draft mode is enabled.

// src/sanity/live.ts

import { defineLive } from "next-sanity";
import { client } from "./client";

const token = process.env.SANITY_VIEWER_TOKEN;

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

Layout and pages

The <SanityLive> component is responsible for making all sanityFetch calls in your application live, so should always be rendered. It will also enable seamless switching between draft and published content when viewing your application in Presentation tool.

// src/app/layout.tsx

import { VisualEditing } from "next-sanity";
import { draftMode } from "next/headers";
import { SanityLive } from "@/sanity/live";
export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body> {children}
<SanityLive />
{(await draftMode()).isEnabled && <VisualEditing />} </body> </html> ); }

Replace your client.fetch calls with the newly exported sanityFetch function.

Explicitly passing the options parameter based on the draft mode status is no longer necessary as sanityFetch handles setting the correct options internally.

// src/app/[slug]/page.tsx

import { defineQuery } from 'next-sanity'
import { sanityFetch } from '@/sanity/live'
const query = defineQuery( `*[_type == "page" && slug.current == $slug][0]{title}`, ) export default async function Page({ params }: { params: Promise<{slug: string}>; }) {
const { data } = await sanityFetch({
query,
params,
});
return <h1>{data.title}</h1>; }

Was this article helpful?