next-sanity

repository·main·Indexed 21 days ago

https://github.com/sanity-io/next-sanity

An all-in-one toolkit for integrating Sanity content management into Next.js applications. It supports visual editing, real-time previews, advanced caching, and the ability to embed Sanity Studio as a route within a Next.js app. The library provides utilities like definePreview and PreviewSuspense for React Server Components and integrates with @sanity/image-url for image transformations.

Tokens
34.7K
Snippets
95
Records
132
Agent score
72%

What's inside next-sanity

  1. Apply the Three-Layer Pattern for Sanity Live

    main

    To correctly use Sanity Live with Next.js Cache Components, follow a three-layer architecture to separate dynamic API calls (like params or cookies) from cached data fetching.

    The Pattern Structure

    1. Layer 1: Page/Layout (Draft Mode Branch): The top-level component. It handles draftMode() logic. Do not use 'use cache' here.
    2. Layer 2: Dynamic Component: A component that awaits dynamic APIs (e.g., params, searchParams, or getDynamicFetchOptions). This layer should be wrapped in <Suspense> when in draft mode.
    3. Layer 3: Cached Component: The component that actually performs the data fetch using sanityFetch. This is the only layer that should contain the 'use cache' directive.

    Visual Representation

    Page/Layout (Layer 1: draftMode branch)
      ├── NOT draft mode → <CachedX perspective="published" stega={false} />  (no Suspense)
      └── draft mode → <Suspense fallback={...}>
                          <DynamicX params={params} />  (Layer 2: awaits dynamic APIs)
                            └── <CachedX perspective={p} stega={s} />  (Layer 3: 'use cache')

    Critical Rule: Adding 'use cache' to the top-level Page or Layout function is a failure mode. Dynamic APIs are forbidden inside 'use cache' boundaries. Layer 3's use of 'use cache' is sufficient to allow the route to prerender into a static shell.

  2. Handle `sanityFetch` latency in `cacheComponents: false` mode

    main

    When cacheComponents: false is enabled, sanityFetch performs two requests: one to discover syncTags and one for the actual result. The sync-tag lookup request now bypasses the Next.js fetch cache to prevent tag drift.

    For fully dynamic routes, this extra request can add latency. To mitigate this, you can:

    1. Opt in to cacheComponents: true and use the use cache: remote directive.
    2. Call client.fetch() directly and provide your own cache tags and revalidation options.
    client.fetch(query, params, {
      next: {revalidate: 15, tags: ['custom-revalidation-tag']},
    })
  3. Pattern: Shared 'use cache' helper per draft/published branch

    main

    This pattern uses a shared async helper with the 'use cache' directive to fetch data for both draft and published states. This ensures that components needing the same data don't trigger multiple independent fetches and allows for efficient streaming in draft mode.

    1. Define a shared fetcher function with 'use cache'.
    2. In the layout, check draftMode().
    3. If isDraftMode is true, wrap the dynamic component in <Suspense>.
    4. If isDraftMode is false, render the cached component directly with perspective="published" and stega={false}.
    // src/app/(website)/layout.tsx
    import {getDynamicFetchOptions, sanityFetch, type DynamicFetchOptions} from '@/sanity/lib/live'
    import {defineQuery} from 'next-sanity'
    import {draftMode} from 'next/headers'
    import {Suspense}
    from 'react'
    
    async function fetchSettings({perspective, stega}: DynamicFetchOptions) {
      'use cache'
      const settingsQuery = defineQuery(`*[_type == "settings"][0]`)
      const {data} = await sanityFetch({query: settingsQuery, perspective, stega})
      return data
    }
    
    export default async function WebsiteLayout({children}: LayoutProps<'/'>) {
      const {isEnabled: isDraftMode} = await draftMode()
      return (
        <>
          {isDraftMode ? (
            <Suspense fallback={<NavbarFallback />}>
              <DynamicNavbar />
            </Suspense>
          ) : (
            <CachedNavbar perspective="published" stega={false} />
          )}
          {children}
          {isDraftMode ? (
            <Suspense>
              <DynamicFooter />
            </Suspense>
          ) : (
            <CachedFooter perspective="published" stega={false} />
          )}
        </>
      )
    }
    
    async function DynamicNavbar() {
      const {perspective, stega} = await getDynamicFetchOptions()
      return <CachedNavbar perspective={perspective} stega={stega} />
    }
    async function CachedNavbar({perspective, stega}: DynamicFetchOptions) {
      'use cache'
      const data = await fetchSettings({perspective, stega})
      return <Navbar data={data} />
    }
    
    async function DynamicFooter() {
      const {perspective, stega} = await getDynamicFetchOptions()
      return <CachedFooter perspective={perspective} stega={stega} />
    }
    async function CachedFooter({perspective, stega}: DynamicFetchOptions) {
      'use cache'
      const data = await fetchSettings({perspective, stega})
      return <Footer data={data} />
    }
  4. Implement the Three-layer component pattern for Sanity Live Cache

    main

    The Three-layer component pattern is the core architecture for routes that can be fully statically prerendered and cached while still supporting Sanity's draft mode and live updates. It uses a tiered approach to separate static shell rendering from dynamic, real-time data fetching.

    The Pattern Structure

    1. Layer 1: Page/Layout (The Branching Layer)

      • Checks draftMode().
      • If NOT in draft mode: Renders the Layer 3 (Cached) component directly without a <Suspense> boundary to maximize the static shell.
      • If in draft mode: Renders a <Suspense> boundary wrapping the Layer 2 (Dynamic) component.
    2. Layer 2: Dynamic Component (The Resolver Layer)

      • Acts as the bridge between dynamic Next.js APIs and the cache boundary.
      • Awaits params, cookies(), and headers() (via getDynamicFetchOptions()).
      • Passes only plain, serializable props (like slug, perspective, and stega) down to Layer 3.
    3. Layer 3: Cached Component (The Data Layer)

      • Uses the 'use cache' directive.
      • Receives plain props from Layer 2.
      • Calls sanityFetch to retrieve data based on the provided perspective and stega values.

    When to use searchParams or other dynamic APIs

    If your route relies on searchParams or other dynamic APIs that are inputs to sanityFetch, you must stop branching on draftMode and always render the <Suspense> tree to allow for streaming.

    Page/Layout (Layer 1)
      ├── NOT draft mode → <CachedX perspective="published" stega={false} />  (no Suspense)
      └── draft mode → <Suspense fallback={...}>
                          <DynamicX params={params} />  (Layer 2)
                            └── <CachedX params={await params} perspective={p} stega={s} />  (Layer 3)
  5. Anti-pattern: Wrapping children in a single cached layout

    main

    Avoid wrapping {children} inside a component that is marked with 'use cache' or that awaits data. Doing so blocks the children from streaming and prevents the page itself from rendering independently, as the entire layout must wait for the data fetch to complete before any children can be sent to the client.

    // src/app/(website)/layout.tsx
    // BAD: This blocks children on the layout's data fetch
    export default async function WebsiteLayout({children}: LayoutProps<'/'>) {
      const {isEnabled: isDraftMode} = await draftMode()
      if (isDraftMode) {
        return (
          <Suspense>
            <DynamicWebsiteLayout>{children}</DynamicWebsiteLayout>
          </Suspense>
        )
      }
      return (
        <CachedWebsiteLayout perspective="published" stega={false}>
          {children}
        </CachedWebsiteLayout>
      )
    }
    
    async function CachedWebsiteLayout({
      children,
      perspective,
      stega,
    }: {children: ReactNode} & DynamicFetchOptions) {
      'use cache'
      const settingsQuery = defineQuery(`*[_type == "settings"][0]`)
      const {data} = await sanityFetch({query: settingsQuery, perspective, stega})
    
      return (
        <>
          <Navbar data={data} />
          {children}
          <Footer data={data} />
        </>
      )
    }
  6. How `getDynamicFetchOptions` works and where to call it

    main

    The getDynamicFetchOptions function resolves the current perspective (from the sanity-preview-perspective cookie) and the stega boolean required for Visual Editing.

    Usage Rules:

    • Avoid Top-Level Calls: Do not call getDynamicFetchOptions in the top-level body of a layout.tsx or page.tsx that you want to remain part of the static shell. Because it calls cookies(), it is a dynamic API.
    • Use Suspense: To prevent blocking the static shell from streaming, call getDynamicFetchOptions inside a component wrapped in a <Suspense> boundary.
    • Streaming Fallbacks: If a route has a sibling loading.tsx, you can await getDynamicFetchOptions directly in the page because loading.tsx provides the necessary streaming fallback.
  7. Migrate webhook parsing from `parseAppBody` to `parseBody`

    main

    The parseAppBody function and the ParseAppBody / ParseBody types have been removed. Use parseBody and the ParsedBody type instead.

    When using parseBody, the return type should be typed as ParsedBody<SanityDocument> from next-sanity.

    import {type NextRequest, NextResponse} from 'next/server'
    -import {parseAppBody} from 'next-sanity/webhook'
    +import {parseBody} from 'next-sanity/webhook'
    
    
    export async function POST(req: NextRequest) {
    - const {isValidSignature, body} = await parseAppBody(
    + const {isValidSignature, body} = await parseBody(
        req,
        process.env.SANITY_REVALIDATE_SECRET,
      )
    }
    
    
    // Type updates:
    -import type {ParseAppBody} from 'next-sanity/webhook'
    -import type {ParseBody} from 'next-sanity/webhook'
    +import type {ParsedBody} from 'next-sanity/webhook'
    +import type {SanityDocument} from 'next-sanity'
    
    -export async function POST(request: Request): Promise<ParseAppBody> {
    -export async function POST(request: Request): Promise<ParseBody> {
    +export async function POST(request: Request): Promise<ParsedBody<SanityDocument>> {
    
    }
  8. Handle searchParams and dynamic APIs in the Three-layer pattern

    main

    When your route uses searchParams or other dynamic APIs that influence the sanityFetch query, you cannot use the draftMode branching logic. Instead, you must always render the <Suspense> tree to allow for streaming.

    Important: Do not export the Page component as an async function in this scenario; this prevents accidentally blocking the entire render while awaiting a dynamic API. Instead, use a standard function that returns a <Suspense> boundary.

    // src/app/[slug]/page.tsx
    import {Suspense} from 'react'
    
    // Do not export an async function here, to avoid accidentally blocking render while awaiting a dynamic API
    export default function Page({params}: PageProps<'/[slug]'>) {
      return (
        <Suspense
          // not optional — no draftMode branch means a missing skeleton causes massive layout shift
          fallback={<PageFallback />}
        >
          <DynamicPage
            // do not await `params` here, it needs to be awaited in `<DynamicPage>` so the Suspense boundary works
            params={params}
          />
        </Suspense>
      )
    }
  9. Migrate next-sanity/webhook to App Router or @sanity/webhook

    main

    The next-sanity/webhook feature is now exclusive to the Next.js App Router.

    Option 1: Migrate to App Router

    If possible, migrate your existing Pages Router API Route to an App Router Route Handler.

    Option 2: Use @sanity/webhook for Pages Router

    If you are using the Pages Router for On-Demand Revalidation of ISR, you cannot use next-sanity/webhook. Instead, you must install and use @sanity/webhook directly.

    1. Install the package: npm install @sanity/webhook@4.0.2-bc --save-exact

    2. Implement a manual parseBody function to handle the request, verify the signature using isValidSignature from @sanity/webhook, and disable the default Next.js bodyParser in your route config.

    npm install @sanity/webhook@4.0.2-bc --save-exact
  10. Migrate VisualEditing import to next-sanity/visual-editing

    main

    When migrating to next-sanity v11, you must change the import path for VisualEditing. It has been moved from the root 'next-sanity' package to 'next-sanity/visual-editing'.

    This change was made to support output: 'static' builds. Because VisualEditing utilizes Next.js Server Actions, keeping it in the root export would prevent features like defineQuery from being used in static environments. Moving it to a sub-path ensures that static builds remain compatible.

    // src/app/layout.tsx
    
    // Before:
    // import {VisualEditing} from 'next-sanity'
    
    // After:
    import {VisualEditing} from 'next-sanity/visual-editing'
    import {SanityLive} from '@/sanity/lib/live'
    
    export default function RootLayout({children}: {children: React.ReactNode}) {
      return (
        <html lang="en">
          <body>
            {children}
            <SanityLive />
            {(await draftMode()).isEnabled && <VisualEditing />}
          </body>
        </html>
      )
    }
  11. Migrate from v4 to v5 in App Router

    main

    When upgrading next-sanity from v4 to v5 in a Next.js App Router project, you must implement four primary changes to your preview logic:

    1. Replace usePreview with useLiveQuery: The old usePreview hook (created via definePreview) is deprecated. Use useLiveQuery instead.
    2. Replace definePreview with PreviewProvider: Instead of using definePreview to create a custom hook, create a custom PreviewProvider component that wraps your application (or a section of it) with LiveQueryProvider from next-sanity/preview.
    3. Remove PreviewSuspense: The PreviewSuspense component is no longer used.
    4. Handle loading states with useLiveQuery: Instead of using a <Suspense fallback={...}> component, use the loading boolean returned by the useLiveQuery hook to conditionally render your fallback UI (e.g., a spinner or loading text).