Integrating Sanity's Presentation Tool with Next.js: Comprehensive Guide

Eugene Boruhov - Dec 22 '23 - - Dev Community

The world of web development is constantly evolving, with new tools and technologies emerging to optimize the content creation and management process. One such tool is the Sanity Presentation Tool, a powerful feature within the Sanity.io ecosystem designed to enhance the content editing experience. This tool bridges the gap between content management and frontend presentation, offering a seamless, real-time editing interface that is invaluable for content creators and developers alike.

While there is exhaustive documentation available, including the template-nextjs-personal-website repository which contains useful code examples, this guide aims to synthesize information from those resources to provide a detailed, practical explanation. Its focus is on demonstrating how to integrate the Sanity Presentation Tool into your existing Next.js application, transforming your content handling into a more dynamic, interactive, and visually intuitive process.

For those new to Sanity or looking to integrate it into their workflow, this guide offers a practical approach to implementing the Presentation Tool. Integrating this tool into your system involves two key components: configuring the Presentation Tool on the Sanity side using the sanity/presentation tool, and enabling live-editing and visual overlays in your frontend application. In this guide, we'll walk through the steps of enabling the Presentation Tool in a Next.js app, focusing on the frontend aspect. For those interested in the Sanity side implementation, I recommend checking out our article A Deep Dive into Sanity's Visual Editing and Presentation Tool: The developer view, which provides a comprehensive overview and practical configuration examples. Additionally, you can see all the code samples from this article in action in our demo project, which showcases all the new features of the Presentation Tool.

Table of Contents

Understanding the Presentation Tool

Before diving into the implementation, it's crucial to understand what the Sanity Presentation Tool is and how it improves the content editing experience. Here, we assume that you are already familiar with the Presentation Tool and will focus more on practical aspects. If you are new to the Presentation Tool, I recommend going through our previous article for a detailed introduction.

The core of the Presentation Tool's functionality lies in its ability to offer a live, interactive preview of content. This feature, as it allows content creators to see and edit content in a format that closely resembles its final presentation on frontend. Such real-time feedback is invaluable in creating a more efficient and error-free editing process.

Central to this tool are several key technologies:

  • @vercel/stega a technology used for encoding metadata within text strings. This encoding is necessary for linking the frontend content with its corresponding editable segments in the CMS.
  • @sanity/client/stega integrates Stega with the Sanity client and ensures communication between your content management system and frontend.
  • @sanity/react-loader facilitates data fetching and streaming in your application, playing a key role in managing content flow and real-time updates.
  • @sanity/overlays creates a visual editing interface on frontend, allowing users to click and directly edit content.

In the following sections, we will explore how these technologies can be integrated into an existing Next.js application to leverage the full capabilities of the Sanity Presentation Tool.

Implementation roadmap

Embarking on the journey to integrate the Sanity Presentation Tool into a Next.js application requires a strategic approach. This section outlines the roadmap that we will follow to enable the full functionality of the Presentation Tool in your existing Next.js app. By breaking down the process into manageable steps, we aim to provide a clear and concise guide that can be followed with ease, regardless of your experience level.

  1. Setting up the environment: Ensure that your development environment is ready for the integration. This includes having the latest version of Next.js installed and your Sanity Studio set up.
  2. Installation of required packages: We'll cover the necessary packages that need to be installed in your Next.js application to support the Presentation Tool. These include the Sanity client and specific packages like @sanity/react-loader and @sanity/overlays.
  3. Configuring loaders: Loaders play a pivotal role in fetching and rendering content. We will delve into the setup of loaders, discuss their importance, and demonstrate how to use them effectively in your application.
  4. Enabling overlays: Overlays provide the visual interface for editing content directly from the preview. We'll guide you through the process of enabling them in your application.
  5. Live mode and real-time preview: A crucial feature of the Presentation Tool is the real-time preview. We will explore how to enable and make the most of this live mode for immediate content updates.
  6. Final touches: Once all components are in place, we'll go through the process of fine-tuning and testing the integration to ensure everything works as expected.

Initial configuration

Before integrating the Sanity Presentation Tool into your Next.js application, it's essential to ensure your environment is up to date and has all the necessary components. This initial setup lays the groundwork for a smooth integration process.

  1. Updating Sanity: Start by updating your Sanity Studio to version 3.20 or higher. This update ensures that you have access to the latest features we rely on here.
  2. Updating Next.js: Ensure that your Next.js application is running on version 18 or higher. This update is needed for compatibility and taking advantage of the latest Next.js features and performance enhancements.
  3. Installing required packages: With your environment up to date, proceed to install the necessary packages. Use the following commands in your terminal to install these packages:
npm install @sanity/client @sanity/react-loader @sanity/overlays
Enter fullscreen mode Exit fullscreen mode

Alternatively, adjust these commands to suit your preferred package manager, such as Yarn.

With these initial configuration steps complete, your application will be well-prepared for the next stages of integrating the Sanity Presentation Tool.

Setting up loaders

Loaders provide a unified method for loading data from Sanity's Content Lake, ensuring consistency across different states (production, development, and preview) and rendering modes (server and client-side). They are key to implementing Visual Editing capabilities, enabling features like real-time previews and clickable overlays. In this section, we'll delve into setting up Loaders and explore their practical applications. Sanity offers loaders tailored for various frameworks, ensuring compatibility and ease of integration regardless of your project's specific technology stack. For this guide, we will focus on using the React Loader. However, the techniques discussed can be adapted to other frameworks – refer to the documentation for guidance on your framework.

Updating sanity client with stega support

Establish a connection between your Next.js application and your Sanity project using the @sanity/client package. Configure the client with necessary parameters like project ID, dataset, and API version.

If you're currently using @sanity/client or next-sanity, you'll need to switch to @sanity/client/stega. This updated client includes support for Stega, essential for the Presentation Tool's interactive and visual features. The upgrade process is straightforward, in most cases, all you need to do is update your import statement and configure the stega options. Here's how your updated client configuration should look:

// client.ts

import { createClient } from '@sanity/client/stega'

export const client = createClient({
  projectId: "<YOUR_SANITY_PROJECT_ID>",
  dataset: "<YOUR_SANITY_DATASET>",
  apiVersion: "<API_VERSION>",
  useCdn: false, // We will rely on Next.js cache
  perspective: 'published',
  stega: {
    studioUrl: "<SANITY_STUDIO_URL>",
  },
})
Enter fullscreen mode Exit fullscreen mode

Creating a QueryStore

Start by setting up a query store using the createQueryStore function from @sanity/react-loader. This store manages the data fetching and streaming in your application. It includes essential utilities like useQuery, loadQuery, and useLiveMode, each serving a specific function:

  • useQuery react hook for client-side data loading and streaming.
  • loadQuery used primarily for server-side rendering (SSR) to fetch content data.
  • useLiveMode activates real-time updates, ensuring that content changes in Sanity are instantly reflected in your application.
// queryStore.ts

import { createQueryStore } from '@sanity/react-loader/rsc'

// The `queryStore` instance is shared in RSC and client components, keep this file tiny as it will be included in the client bundle
export const queryStore = createQueryStore({
  client: false,
  ssr: true,
})
Enter fullscreen mode Exit fullscreen mode

Setting up a wrapper for loadQuery

In order to enhance the functionality of loadQuery and handle specific needs such as server-client consistency and effective caching strategies, we create a custom wrapper around this function. Additionally, we will integrate support for draft mode, ensuring that it responds dynamically to different content states in your application.

// Throw error if this file will be imported on the client side
import 'server-only'

import { draftMode } from 'next/headers'

import { client } from './client'
import { queryStore } from './queryStore'

// Configuring a separate client for server-side usage, enabling Stega in non-production environments
const serverClient = client.withConfig({
  stega: {
    enabled: process.env.NODE_ENV !== 'production',
  },
})

// Setting the server client in the query store for consistent server-client data handling
queryStore.setServerClient(serverClient)

// Custom wrapper function for `queryStore.loadQuery` to handle draft mode and keep configuration in one place
export const loadQuery = ((query, params = {}, options = {}) => {
  return queryStore.loadQuery(query, params, {
    cache: 'force-cache',
    perspective: draftMode().isEnabled ? 'previewDrafts' : 'published',
    ...options,
  })
}) satisfies typeof queryStore.loadQuery
Enter fullscreen mode Exit fullscreen mode

Setting up a wrapper for useQuery

Creating a custom wrapper around the useQuery hook is an effective way to extend its functionality and tailor it to the specific needs of your application. This wrapper can handle additional tasks like encoding data attributes for overlays and consistent errors managing. Here's how you can set it up:

import {
  type QueryParams,
  useEncodeDataAttribute,
  type UseQueryOptions,
} from '@sanity/react-loader/rsc'

import { queryStore } from './createQueryStore'

// Custom hook for `queryStore.useQuery` to simplify usage of `encodeDataAttribute`
export const useQuery = <
  QueryResponseResult = unknown,
  QueryResponseError = unknown,
>(
  query: string,
  params?: QueryParams,
  options?: UseQueryOptions<QueryResponseResult>,
) => {
  const snapshot = queryStore.useQuery<QueryResponseResult, QueryResponseError>(
    query,
    params,
    options,
  )

  // Generate data attributes for overlays using Sanity's data
  const encodeDataAttribute = useEncodeDataAttribute(
    snapshot.data,
    snapshot.sourceMap,
    '<YOUR_STUDIO_URL>',  // Replace with your Sanity Studio URL
  )

  // Consistent error handling by throwing any caught errors
  if (snapshot.error) {
    throw snapshot.error
  }

  // Return the query results along with the `encodeDataAttribute` function
  return {
    ...snapshot,
    encodeDataAttribute,
  }
}

// Export `useLiveMode` for enabling real-time updates in the `VisualEditing` component
export const { useLiveMode } = queryStore
Enter fullscreen mode Exit fullscreen mode

Along with data from queryStore.useQuery, our hook also returns the encodeDataAttribute function. This function is used to create the necessary data for the data-sanity attribute, which we will discuss in more detail below.

Implementing loaders in components

Integrate Loaders into your Next.js components using the useQuery and loadQuery hooks. For server-side rendered pages, use loadQuery to fetch initial data. For dynamic, client-side interactions, useQuery is your go-to hook. This ensures that your components are always displaying up-to-date content from Sanity.

Creating a preview component for Next.js routes

Begin by developing a client-side component, designated for loading only when the draft mode is enabled. The purpose of this component is to fetch and pass the most current, or 'fresh', data to your page, ensuring that the content being edited is up-to-date with the latest changes made in the Sanity Studio.

'use client'

import { type QueryResponseInitial } from '@sanity/react-loader/rsc'

import Page from './Page'
import { useQuery } from './useQuery'

type Props = {
  params: { slug: string }
  initial: QueryResponseInitial<PagePayload | null>
}

export default function PagePreview(props: Props) {
  const { params, initial } = props
  // Using the `useQuery` hook to fetch the most current data based on the page slug
  const { data } = useQuery<'YOUR_PAGE_PAYLOAD_TYPE' | null>('<YOUR_GROQ_QUERY>', params, {
    initial,
  })

  return <Page data={data!} />
}
Enter fullscreen mode Exit fullscreen mode

Use the preview component

With the preview component set up, let's now integrate it into a Next.js page:

import dynamic from 'next/dynamic'
import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'

import { Page } from './Page'
import { loadQuery } from './loadQuery'

// 1. Lazily load the Preview component for performance optimization
const PagePreview = dynamic(() => import('./PagePreview'))

type Props = {
  params: { slug: string }
}

export default async function PageRoute({ params }: Props) {
  // 2. Fetch initial data for the page
  const initial = await loadQuery(params.slug)

  // 3. Display the Preview component in draft mode with initial data
  if (draftMode().isEnabled) {
    return <PagePreview params={params} initial={initial} />
  }

  // Handle page not found scenario
  if (!initial.data) {
    return notFound()
  }

  // Render the standard page with fetched data
  return <Page data={initial.data} />
}
Enter fullscreen mode Exit fullscreen mode
  1. Dynamic loading: The PagePreview component is dynamically imported using dynamic function. This approach optimizes performance by only loading the component when necessary, specifically in draft mode for content editing.
  2. Server-side data fetching: The loadQuery function fetches initial data. Depending on the mode (draft or production), it provides the appropriate data to either the preview or standard page component.
  3. Draft mode check: The use of draftMode().isEnabled ensures that VisualEditing is only rendered when the application is in draft mode. This is crucial for maintaining performance and user experience in the production environment.

Using useQuery in production

While the useQuery function is primarily used in the context of editing and development, it's worth noting that it can also be employed in a production environment. Utilizing useQuery in production allows for the creation of highly dynamic components. However, this is a rarer use case, as it involves real-time updates and data streaming.

'use client';

import { useQuery } from './useQuery'

export function DocumentCount() {
  const { data, loading } = useQuery<number>('count(*)')

  if (loading) {
    return <div>Loading…</div>
  }

  return <div>Total # of documents: {data}</div>
}
Enter fullscreen mode Exit fullscreen mode

By carefully implementing these steps, you ensure that your Next.js application not only efficiently manages content in both production and draft modes but also leverages the full potential of Sanity's real-time content editing features.

Enabling overlays

With the Loaders set up, the next crucial step in integrating the Sanity Presentation Tool into your Next.js application is implementing overlays. They appear as clickable visual indicators on the frontend, guiding content creators directly to editable elements. When an overlay is clicked, it signals the Sanity Studio to open the corresponding field for editing, even if it's nested deeply within the document structure.

Creating the VisualEditing component

Start by creating a VisualEditing component. This component will incorporate the useLiveMode hook from @sanity/react-loader and enableOverlays function from @sanity/overlays:

'use client'

import { enableOverlays } from '@sanity/overlays'
import { useEffect } from 'react'

import { client } from './client'
import { useLiveMode } from './useQuery'

// Configure the client to always use Stega in Live Mode
const stegaClient = client.withConfig({ stega: true })

// Set the allowed studio origin, defaulting to localhost in a non-browser environment
const allowStudioOrigin =
  typeof location === 'undefined' ? 'http://localhost:3000' : location.origin

export default function VisualEditing() {
  useEffect(() => {
    // Enable overlays and set up a cleanup function
    const disable = enableOverlays({ allowStudioOrigin })

    return () => disable()
  }, [])

  // Activate live mode updates using the configured client
  useLiveMode({ allowStudioOrigin, client: stegaClient })

  return null
}
Enter fullscreen mode Exit fullscreen mode
  • useLiveMode hook is essential for reflecting any changes made in the Sanity Studio onto the frontend preview instantaneously. This real-time update capability is crucial for maintaining synchronization between the content in the CMS and its presentation.
  • enableOverlays function is responsible for turning on the overlay functionality in your application.

Adding VisualEditing component to your layout

Integrate the VisualEditing component into your application's layout. This ensures that the live preview and overlay functionality are available across all pages.

import dynamic from 'next/dynamic'
import { draftMode } from 'next/headers'

// Dynamically import the VisualEditing component for optimized loading
const VisualEditing = dynamic(() => import('./VisualEditing'))

export default function IndexRoute({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <>
      {/* ... */}

      {/* Conditionally render the VisualEditing component based on draft mode */}
      {draftMode().isEnabled && <VisualEditing />}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we load VisualEditing dynamically, in the same way, we did previously with PreviewPage, and render it only when in draft mode. This approach optimizes performance by only loading the component when necessary, specifically in draft mode for content editing.

Integrating overlays into your application

Ensure that your content elements have the necessary data attributes for overlays to recognize and interact with them. This is particularly important for custom components or more complex content structures where stega strings don't work.

Consider a scenario where you want to add an overlay around a non-text element. For such components, you can use the data-sanity attribute. In the example below we have two components: SvgIcon and Header. SvgIcon is a hardcoded component stored in a repository. isLogoEnabled is boolean option coming from the CMS to determine visibility of SvgIcon. To enable overlays for SvgIcon with the link to isLogoEnabled you need to:

  1. Pass down the encodeDataAttribute from our custom useQuery hook in the PreviewPage component.
  2. Add the data-sanity attribute and use the encodeDataAttribute function to generate a string containing the data necessary for overlays to open the correct field in the CMS. This function requires the path to the isLogoEnabled field as an argument.
// PreviewPage.tsx
export default function PagePreview(props: Props) {
  const { params, initial } = props
  const { data, encodeDataAttribute } = useQuery<PagePayload | null>(pagesBySlugQuery, params, {
    initial,
  })

  return <Page data={data!} encodeDataAttribute={encodeDataAttribute} />
}

// SvgIcon.tsx
import { SVGProps } from 'react'

export const SvgIcon = (props: SVGProps<SVGSVGElement>) => {
  return (
    <svg viewBox="0 0 170 61" width="170px" height="61px" {...props}>
      <path
        fillRule="evenodd"
        fill="rgb(248, 248, 248)"
        d="M14.959,59.996 L7.050,50.543 L5.742,50.543 L5.742,60.004 L0.001,60.004 L0.001,38.907 L5.141,32.341 L11.488,32.341 C13.715,32.341 15.740,32.999 17.346,34.267 C18.928,35.514 20.048,37.304 20.501,39.587 C20.841,41.305 20.739,43.079 20.178,44.682 C19.621,46.273 18.632,47.699 17.196,48.758 L17.193,48.753 C16.386,49.357 15.482,49.809 14.511,50.106 C14.425,50.132 14.338,50.156 14.250,50.180 L22.463,59.996 L14.959,59.996 ZM60.938,59.996 L54.403,41.742 L47.719,59.996 L41.608,59.996 L51.737,32.328 L57.120,32.352 L67.017,59.996 L60.938,59.996 ZM39.473,59.996 L24.161,59.996 L24.141,39.082 L29.282,32.340 L40.017,32.340 L40.017,38.158 L32.080,38.158 L29.862,41.065 L29.864,43.516 L39.473,43.516 L39.473,49.334 L29.869,49.334 L29.874,54.177 L39.473,54.177 L39.473,59.996 ZM151.154,32.349 L141.283,60.001 L135.901,60.001 L126.063,32.349 L132.138,32.349 L138.596,50.502 L145.076,32.349 L151.154,32.349 ZM99.694,59.994 L99.694,38.144 L91.008,38.144 L91.008,32.340 L114.121,32.340 L114.121,38.144 L105.436,38.144 L105.436,59.994 L99.694,59.994 ZM117.375,59.992 L117.375,32.341 L123.117,32.341 L123.117,59.992 L117.375,59.992 ZM169.446,59.999 L151.752,59.999 L151.732,39.079 L156.899,32.340 L169.994,32.340 L169.994,38.128 L159.696,38.128 L157.453,41.054 L157.456,43.532 L169.446,43.532 L169.446,49.320 L157.461,49.320 L157.466,54.210 L169.446,54.210 L169.446,59.999 ZM115.889,5.979 L107.795,5.979 C106.298,5.979 103.601,6.732 103.620,8.593 C103.645,11.060 106.281,11.082 108.192,11.275 C109.686,11.427 111.464,11.623 113.132,12.294 C115.245,13.145 116.982,14.627 117.576,17.321 C118.274,20.479 117.264,23.409 114.756,25.464 C112.847,27.029 110.255,27.845 107.860,27.845 L99.766,27.845 L99.766,22.061 L107.860,22.061 C109.308,22.061 111.747,21.551 112.021,19.650 C112.128,18.909 111.908,18.013 111.002,17.649 C110.058,17.269 108.743,17.126 107.631,17.015 C104.104,16.657 100.180,15.881 98.570,12.249 C97.799,10.512 97.684,8.261 98.231,6.441 C99.476,2.308 103.736,0.196 107.795,0.196 L115.889,0.196 L115.889,5.979 ZM79.119,0.190 L79.120,18.940 C79.120,19.797 79.508,20.465 80.090,20.938 C81.007,21.683 82.350,22.058 83.655,22.058 L83.655,22.068 L89.336,22.067 L89.336,0.190 L95.077,0.190 L95.077,27.846 L83.655,27.850 L83.655,27.861 C81.159,27.861 78.478,27.056 76.502,25.450 C74.630,23.928 73.379,21.757 73.379,18.940 L73.379,0.190 L79.119,0.190 ZM0.000,27.846 L0.000,6.892 L5.273,0.190 L16.235,0.190 L16.235,6.001 L8.026,6.001 L5.741,8.905 L5.741,11.640 L14.981,11.640 L14.981,17.445 L5.741,17.445 L5.741,27.846 L0.000,27.846 ZM37.817,8.272 C36.360,6.798 34.346,5.887 32.122,5.887 C31.434,5.887 30.792,5.964 30.202,6.109 C29.773,6.214 29.346,6.363 28.927,6.549 L24.193,12.559 C24.108,13.074 24.068,13.494 24.068,14.029 C24.068,16.278 24.970,18.313 26.427,19.786 C27.884,21.258 29.898,22.170 32.122,22.170 C34.346,22.170 36.360,21.258 37.817,19.786 C39.275,18.313 40.176,16.277 40.176,14.029 C40.176,11.780 39.275,9.744 37.817,8.272 ZM32.122,-0.015 C35.958,-0.015 39.431,1.558 41.944,4.099 C44.458,6.640 46.014,10.151 46.014,14.029 C46.014,17.906 44.458,21.417 41.944,23.958 C39.431,26.499 35.958,28.072 32.122,28.072 C28.287,28.072 24.813,26.499 22.300,23.958 C18.427,20.043 17.520,15.093 18.838,9.903 L25.182,1.849 C26.408,1.204 27.471,0.728 28.834,0.392 C29.912,0.126 31.015,-0.015 32.122,-0.015 ZM70.110,27.844 C67.384,27.844 64.562,27.902 61.861,27.839 C58.215,27.753 54.921,26.222 52.518,23.792 C50.042,21.290 48.511,17.833 48.511,14.016 C48.511,10.198 50.042,6.741 52.518,4.237 C54.993,1.736 58.412,0.188 62.189,0.188 L70.110,0.188 L70.110,5.992 L62.189,5.992 C59.997,5.992 58.013,6.890 56.576,8.341 C55.141,9.793 54.252,11.800 54.252,14.016 C54.252,16.232 55.141,18.237 56.576,19.690 C57.675,20.799 59.094,21.585 60.681,21.895 C61.842,22.119 63.215,22.051 64.391,22.050 L70.109,22.050 L70.110,27.844 ZM87.754,59.997 C85.028,59.997 82.207,60.055 79.506,59.991 C75.859,59.906 72.565,58.375 70.162,55.945 C67.686,53.442 66.156,49.984 66.156,46.168 C66.156,42.350 67.686,38.892 70.162,36.390 C72.637,33.888 76.056,32.340 79.833,32.340 L87.754,32.340 L87.754,38.144 L79.833,38.144 C77.641,38.144 75.657,39.041 74.221,40.494 C72.785,41.945 71.897,43.951 71.897,46.168 C71.897,48.383 72.785,50.390 74.221,51.842 C75.319,52.951 76.738,53.738 78.325,54.046 C79.486,54.271 80.860,54.203 82.036,54.203 L87.754,54.202 L87.754,59.997 ZM5.742,44.703 L11.495,44.703 C12.004,44.703 12.468,44.641 12.874,44.518 C13.235,44.406 13.551,44.253 13.810,44.058 L13.832,44.036 C14.283,43.703 14.596,43.250 14.773,42.743 C14.992,42.118 15.030,41.414 14.894,40.727 C14.730,39.899 14.359,39.277 13.847,38.874 C13.268,38.417 12.452,38.181 11.488,38.181 L7.894,38.181 L5.742,40.929 L5.742,44.703 Z"
      ></path>
    </svg>
  )
}

// Header.tsx
import type { EncodeDataAttributeCallback } from '@sanity/react-loader/rsc'
import { SvgIcon } from './SvgIcon'

interface HeaderProps {
  // ...
  encodeDataAttribute?: EncodeDataAttributeCallback
}

export function Header({ encodeDataAttribute, ...props }: HeaderProps) {
  return (
    <header>
      {/* ... */}

      <SvgIcon
        data-sanity={encodeDataAttribute?.(['header', 'isLogoEnabled'])}
      />
    </header>
  )
}
Enter fullscreen mode Exit fullscreen mode

Enhancing navigation within Presentation Tool

To ensure smooth navigation within the Sanity Presentation Tool, we utilize a HistoryAdapter. This adapter maintains synchronization between the studio preview pane and your application’s routing or history state. It effectively manages navigation events and updates, ensuring that changes in your application's URL or state are accurately reflected in the Studio's preview environment and vice versa.

export default function VisualEditing() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()
  const routerRef = useRef(router)
  const navigateRef = useRef<HistoryAdapterNavigate>()

  routerRef.current = router

  const history: HistoryAdapter = useMemo(
    () => ({
            // Subscribe function to handle navigation changes
      subscribe(navigate) {
        navigateRef.current = navigate
        return () => {
          navigateRef.current = undefined
        }
      },

            // Update function to handle different types of history updates
      update(update) {
        switch (update.type) {
          case 'push':
            return routerRef.current.push(update.url)
          case 'pop':
            return routerRef.current.back()
          case 'replace':
            return routerRef.current.replace(update.url)
          default:
            throw new Error(`Unknown update type: ${update.type}`)
        }
      },
    }),
    [],
  )

  useEffect(() => {
    const disable = enableOverlays({
      allowStudioOrigin,
      history,
    })
    return () => disable()
  }, [])

  // Update navigation state when pathname or search parameters change
  useEffect(() => {
    navigateRef.current?.({
      type: 'push',
      url: `${pathname}${searchParams?.size ? `?${searchParams}` : ''}`,
    })
  }, [pathname, searchParams])

  useLiveMode({ allowStudioOrigin, client: stegaClient })

  return null
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

As we reach the end of this guide, you should now have a fully functional integration of the Sanity Presentation Tool in your Next.js application. This integration not only enhances the content management experience but also brings a new level of interactivity and efficiency to your workflow.

Reflecting on the benefits

The integration of the Presentation Tool offers numerous advantages:

  • Real-time editing: Content creators can see changes instantly, making the editing process more intuitive and efficient.
  • Visual clarity: Overlays offer a clear visual indication of editable content, simplifying content management.
  • Enhanced user experience: The seamless integration between the frontend and the CMS improves the overall user experience, both for content creators and developers.

Encouragement to experiment

With the foundational knowledge and examples provided in this guide, you're encouraged to experiment further. Each Next.js application is unique, and there may be additional ways to tailor the Presentation Tool to better suit your specific needs. If you find yourself in need of expert advice on Headless CMS and the latest web technologies, don't hesitate to get in touch with us.

Seeking further assistance

For additional resources or support, consider exploring the Sanity community forums or reaching out to fellow developers. The open-source nature of these tools means that there is a wealth of shared knowledge and experience available.

Final thoughts

Integrating the Sanity Presentation Tool into your Next.js application is a step towards a more dynamic and interactive web development experience. As technology continues to evolve, staying adaptable and open to new tools and methodologies will undoubtedly benefit your projects and workflow.


We hope this guide has been valuable in your journey to enhance your Next.js application with the Sanity Presentation Tool. Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .