Visual Editing with Next.js Pages Router
This guide will get you started with Sanity Visual Editing in a new or existing Next.js application using the Pages 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.
- Provide instant updates and seamless switching between draft and published content.
Gotcha
This guide is for the Next.js Pages Router. Go here for the guide on Next.js App Router.
- A Sanity project with a hosted or embedded Studio.
- A Next.js application using Pages Router. Follow this guide to set one up.
The following steps should be performed in your Next.js application.
Install the dependencies that will provide your application with data fetching and Visual Editing capabilities.
npm install next-sanity @sanity/visual-editing @sanity/react-loader
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
PUBLIC_SANITY_PROJECT_ID="YOUR_PROJECT_ID"
PUBLIC_SANITY_DATASET="YOUR_DATASET"
PUBLIC_SANITY_STUDIO_URL="https://YOUR_PROJECT.sanity.studio"
# Private
SANITY_VIEWER_TOKEN="YOUR_VIEWER_TOKEN"
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 allows authorized content editors to view and interact with draft content.
Create an API endpoint (note we use the src/app
directory) 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,
}),
});
Similarly, create an API endpoint to disable draft mode.
// 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();
const url = new URL(request.nextUrl);
return NextResponse.redirect(new URL("/", url.origin));
}
Create a new component with a link to the disable endpoint. We add conditional logic to only render this for content authors when viewing draft content in a non-Presentation context.
// src/components/DisableDraftMode.tsx
import { useEffect, useState } from "react";
export function DisableDraftMode() {
const [show, setShow] = useState(false);
useEffect(() => {
setShow(window === window.parent && !window.opener);
}, []);
return show && <a href={"/api/draft-mode/disable"}>Disable Draft Mode</a>;
}
Create a Visual Editing wrapper component.
The <VisualEditing>
component handles rendering overlays, enabling click to edit, and refreshing pages in your application when content changes. Render it alongside the <DisableDraftMode>
component you created above.
We provide a basic refresh mechanism that will reload the page when changes are made in Presentation tool. You can optionally use loaders to provide seamless updates.
// src/components/SanityVisualEditing.tsx
import { VisualEditing } from "@sanity/visual-editing/next-pages-router";
import { useLiveMode } from '@sanity/react-loader'
import { DisableDraftMode } from "@/pages/components/DisableDraftMode";
import { client } from "@/sanity/client";
const stegaClient = client.withConfig({stega: true})
export default function SanityVisualEditing() {
useLiveMode({client: stegaClient})
return (
<>
<VisualEditing />
<DisableDraftMode />
</>
);
}
In the root layout file, dynamically import and render the <SanityVisualEditing>
wrapper component when draft mode is enabled.
// src/pages/_app.tsx
import type { AppProps } from "next/app";
import dynamic from "next/dynamic";
const SanityVisualEditing = dynamic(
() => import("../components/SanityVisualEditing")
);
export interface SharedProps {
draftMode: boolean;
}
export default function App({ Component, pageProps }: AppProps<SharedProps>) {
const { draftMode } = pageProps;
return (
<>
<Component {...pageProps} />
{draftMode && <SanityVisualEditing />}
</>
);
}
Create a new file to configure loaders. Call setServerClient
, with the client instance which should be used to fetch data on the server.
We also create a helper function to return fetch options based on the draft mode state, and export this alongside loadQuery
for convenience.
// src/sanity/ssr.ts
import * as serverOnly from "@sanity/react-loader";
import { client } from "./client";
import { ClientPerspective } from "next-sanity";
const { loadQuery, setServerClient } = serverOnly;
setServerClient(
client.withConfig({
token: process.env.SANITY_VIEWER_TOKEN,
})
);
const loadQueryOptions = (context: { draftMode?: boolean }) => {
const { draftMode } = context;
return draftMode
? {
perspective: "previewDrafts" as ClientPerspective,
stega: true,
useCdn: false,
}
: {};
};
export { loadQuery, loadQueryOptions };
In getStaticProps
use the loadQuery
function created above. The initial
data returned here is passed to useQuery
in the page component.
When in Presentation, useQuery
will handle live updates as content is edited.
// src/pages/index.tsx
import { loadQuery } from "@/sanity/ssr";
import { useQuery } from "@sanity/react-loader";
import type { GetStaticProps, InferGetStaticPropsType } from "next";
const query = `*[_type == "page"][0]{title}`;
export const getStaticProps = (async (context) => {
const options = loadQueryOptions(context);
const initial = await loadQuery<{ title?: string }>(query, {}, options);
return { props: { initial } };
}) satisfies GetStaticProps;
export type PageProps = InferGetStaticPropsType<typeof getStaticProps>;
export default function Page(props: PageProps) {
const { initial } = props;
const { data } = useQuery(query, {}, { initial });
return <h1>{data.title}</h1>;
}
To setup Presentation tool in your Sanity Studio, import the tool from sanity/presentation
, add it to your plugins
array, and set previewUrl
to the base URL of your application.
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",
disable: "/api/draft-mode/disable",
},
},
}),
// ... other plugins
],
});
useQuery
also returns an encodeDataAttribute
helper method for generating data-sanity
attributes. These attributes give you direct control over rendering overlays in your application, and are especially useful if not using stega encoding.
// src/pages/index.tsx
import { loadQuery } from "@/sanity/ssr";
import { useQuery } from "@sanity/react-loader";
import type { GetStaticProps, InferGetStaticPropsType } from "next";
const query = `*[_type == "page"][0]{title}`;
export const getStaticProps = (async (context) => {
const options = loadQueryOptions(context);
const initial = await loadQuery<{ title?: string }>(query, {}, options);
return { props: { initial } };
}) satisfies GetStaticProps;
export type PageProps = InferGetStaticPropsType<typeof getStaticProps>;
export default function Page(props: PageProps) {
const { initial } = props;
const { data, encodeDataAttribute } = useQuery(query, {}, { initial });
return <h1 data-sanity={encodeDataAttribute(["title"])}>{data.title}</h1>;
}