Skip to content

Latest commit

 

History

History
770 lines (593 loc) · 28.8 KB

README.md

File metadata and controls

770 lines (593 loc) · 28.8 KB

next-sanity

The official Sanity.io toolkit for Next.js apps.

Features:

Table of contents

Installation

For basic functionality, run the following command in the package manager of your choice:

npm install next-sanity
yarn add next-sanity
pnpm install next-sanity
bun install next-sanity

Common dependencies

Building with Sanity and Next.js, you‘re likely to want libraries to handle On-Demand Image Transformations and block content with Portable Text:

npm install @portabletext/react @sanity/image-url
yarn add @portabletext/react @sanity/image-url
pnpm install @portabletext/react @sanity/image-url
bun install @portabletext/react @sanity/image-url

Peer dependencies for embedded Sanity Studio

When using npm newer than v7, or pnpm newer than v8, you should end up with needed dependencies like sanity and styled-components when you npm install next-sanity. It also works in yarn v1 using install-peerdeps:

npx install-peerdeps --yarn next-sanity

Usage

There are different ways to integrate Sanity with Next.js depending on your usage and needs for features like Live Preview, tag-based revalidation, and so on. It's possible to start simple and add more functionality as your project progresses.

Quick start

To start running GROQ queries with next-sanity, we recommend creating a client.ts file:

// ./src/utils/sanity/client.ts
import {createClient} from 'next-sanity'

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'

const client = createClient({
  projectId,
  dataset,
  apiVersion, // https://www.sanity.io/docs/api-versioning
  useCdn: true, // if you're using ISR or only static generation at build time then you can set this to `false` to guarantee no stale content
})

App Router Components

To fetch data in a React Server Component using the App Router:

// ./src/app/page.tsx
import {client} from '@/src/utils/sanity/client'

type Post = {
  _id: string
  title?: string
  slug?: {
    current: string
  }
}

export async function PostIndex() {
  const posts = await client.fetch<Post[]>(`*[_type == "post"]`)

  return (
    <ul>
      {posts.map((post) => (
        <li key={post._id}>
          <a href={post?.slug.current}>{post?.title}</a>
        </li>
      ))}
    </ul>
  )
}

Page Router Components

If you're using the Pages Router, then you can do the following from a page component:

// ./src/pages/index.tsx
import {client} from '@/src/utils/sanity/client'

type Post = {
  _id: string
  title?: string
  slug?: {
    current: string
  }
}

export async function getStaticProps() {
  return await client.fetch<Post[]>(`*[_type == "post"]`)
}

export async function HomePage(props) {
  const {posts} = props

  return (
    <ul>
      {posts.map((post) => (
        <li key={post._id}>
          <a href={post?.slug.current}>{post?.title}</a>
        </li>
      ))}
    </ul>
  )
}

Should useCdn be true or false?

You might notice that you have to set the useCdn to true or false in the client configuration. Sanity offers caching on a CDN for content queries. Since Next.js often comes with its own caching, it might not be necessary, but there are some exceptions.

The general rule is that useCdn should be true when:

  • Data fetching happens client-side, for example, in a useEffect hook or in response to a user interaction where the client.fetch call is made in the browser.
  • Server-Side Rendered (SSR) data fetching is dynamic and has a high number of unique requests per visitor, for example, a "For You" feed.

And it makes sense to set useCdn to false when:

  • Used in a static site generation context, for example, getStaticProps or getStaticPaths.
  • Used in an ISR on-demand webhook responder.
  • Good stale-while-revalidate caching is in place that keeps API requests on a consistent low, even if traffic to Next.js spikes.
  • For Preview or Draft modes as part of an editorial workflow, you need to ensure that the latest content is always fetched.

How does apiVersion work?

Sanity uses date-based API versioning. The tl;dr is that you can send the implementation date in a YYYY-MM-DD format, and it will automatically fall back on the latest API version of that time. Then, if a breaking change is introduced later, it won't break your application and give you time to test before upgrading (by setting the value to a date past the breaking change).

Cache revalidation

This toolkit includes the @sanity/client that fully supports Next.js’ fetch based features, including the revalidateTag API. It‘s not necessary to use the React.cache method like with many other third-party SDKs. This gives you tools to ensure great performance while preventing stale content in a way that's native to Next.js.

Note

Some hosts (like Vercel) will keep the content cache in a dedicated data layer and not part of the static app bundle, which means that it might not be revalidated from re-deploying the app like it has done earlier. We recommend reading up on caching behavior in the Next.js docs.

Time-based revalidation

Time-based revalidation is best for less complex cases and where content updates don't need to be immediately available.

// ./src/app/home/layout.tsx
import { client } from '@/src/utils/sanity/client'
import { PageProps } from '@/src/app/(page)/Page.tsx'

type HomePageProps = {
  _id: string
  title?: string
  navItems: PageProps[]
}

export async function HomeLayout({children}) {
  const home = await client.fetch<HomePageProps>(`*[_id == "home"][0]{...,navItems[]->}`,
    next: {
      revalidate: 3600 // look for updates to revalidate cache every hour
    }
  })

  return (
    <main>
      <nav>
        <span>{home?.title}</span>
        <ul>
        {home?.navItems.map(navItem => ({
          <li key={navItem._id}><a href={navItem?.slug?.current}>{navItem?.title}</a></li>
        }))}
        </ul>
      </nav>
      {children}
    </main>
  )
}

Tag-based revalidation webhook

Tag-based or on-demand revalidation gives you more fine-grained and precise control for when to revalidate content. This is great for pulling content from the same source across components and when content freshness is important.

Below is an example configuration that ensures the client is only bundled server-side and comes with some defaults. It‘s also easier to adapt for Live Preview functionality (see below).

If you're planning to use revalidateTag, then remember to set up the webhook (see code below) as well.

// ./src/utils/sanity/client.ts
import 'server-only'

import type {QueryParams} from '@sanity/client'
import {createClient} from 'next-sanity'

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'

const client = createClient({
  projectId,
  dataset,
  apiVersion, // https://www.sanity.io/docs/api-versioning
  useCdn: false,
})

const DEFAULT_PARAMS = {} as QueryParams
const DEFAULT_TAGS = [] as string[]

export async function sanityFetch<QueryResponse>({
  query,
  params = DEFAULT_PARAMS,
  tags = DEFAULT_TAGS,
}: {
  query: string
  params?: QueryParams
  tags: string[]
}): Promise<QueryResponse> {
  return client.fetch<QueryResponse>(query, params, {
    cache: 'force-cache',
    next: {
      //revalidate: 30, // for simple, time-based revalidation
      tags, // for tag-based revalidation
    },
  })
}

Now you can import the sanityFetch() function in any component within the app folder, and specify for which document types you want it to revalidate:

// ./src/app/home/layout.tsx
import { sanityFetch } from '@/src/utils/sanity/client'
import { PageProps } from '@/src/app/(page)/Page.tsx'

type HomePageProps = {
  _id: string
  title?: string
  navItems: PageProps[]
}

export async function HomeLayout({children}) {
  // revalidate if there are changes to either the home document or to a page document (since they're referenced to in navItems)
  const home = await sanityFetch<HomePageProps>({
    query: `*[_id == "home"][0]{...,navItems[]->}`,
    tags: ['home', 'page']
    })

  return (
    <main>
      <nav>
        <span>{home?.title}</span>
        <ul>
        {home?.navItems.map(navItem => ({
          <li key={navItem._id}><a href={navItem?.slug?.current}>{navItem?.title}</a></li>
        }))}
        </ul>
      </nav>
      {children}
    </main>
  )
}

In order to get revalidateTag to work you need to set up an API route in your Next.js app that handles an incoming request, typically made by a GROQ-Powered Webhook.

You can use this template to quickly configure the webhook for your Sanity project.

The code example below uses the built-in parseBody function to validate that the request comes from your Sanity project (using a shared secret + looking at the request headers). Then it looks at the document type information in the webhook payload and matches that against the revalidation tags in your app:

// ./src/app/api/revalidate.ts
import {revalidateTag} from 'next/cache'
import {type NextRequest, NextResponse} from 'next/server'
import {parseBody} from 'next-sanity/webhook'

export async function POST(req: NextRequest) {
  try {
    const {isValidSignature, body} = await parseBody<{_type}>(
      req,
      process.env.SANITY_REVALIDATE_SECRET,
    )

    if (!isValidSignature) {
      const message = 'Invalid signature'
      return new Response(JSON.stringify({message, isValidSignature, body}), {status: 401})
    }

    if (!body?._type) {
      const message = 'Bad Request'
      return new Response({message, body}, {status: 400})
    }

    // If the `_type` is `page`, then all `client.fetch` calls with
    // `{next: {tags: ['page']}}` will be revalidated
    await revalidateTag(body._type)

    return NextResponse.json({body})
  } catch (err) {
    console.error(err)
    return new Response(err.message, {status: 500})
  }
}

You can choose to match tags based on any field or expression since GROQ-Powered Webhooks allow you to freely define the payload.

Slug-based revalidation for the Pages Router

If you are using the Pages Router and want on-demand revalidation, you'll have to do this by targeting the URLs/slugs for the pages you want to revalidate. If you have nested routes, you will need to adopt the logic to accommodate for that. For example, using _type to determine the first segment: /${body?._type}/${body?.slug.current}.

// ./pages/api/revalidate.ts
import type {NextApiRequest, NextApiResponse} from 'next'
import {parseBody} from 'next-sanity/webhook'

// Export the config from next-sanity to enable validating the request body signature properly
export {config} from 'next-sanity/webhook'

export default async function revalidate(req: NextApiRequest, res: NextApiResponse) {
  try {
    const {isValidSignature, body} = await parseBody(req, process.env.SANITY_REVALIDATE_SECRET)

    if (!isValidSignature) {
      const message = 'Invalid signature'
      return res.status(401).json({message, isValidSignature, body})
    }

    const staleRoute = `/${body.slug.current}`
    await res.revalidate(staleRoute)
    const message = `Updated route: ${staleRoute}`
    return res.status(200).json({message, body})
  } catch (err) {
    console.error(err)
    return res.status(500).json({message: err.message})
  }
}

Working example implementation

Check out our Personal website template to see a feature-complete example of how revalidateTag is used together with Live Previews.

Debugging caching and revalidation

To aid in debugging and understanding what's in the cache, revalidated, skipped, and more, add the following to your Next.js configuration file:

// ./next.config.js
module.exports = {
  experimental: {
    logging: 'verbose',
  },
}

Preview

There are different ways to set up content previews with Sanity and Next.js.

Using Perspectives

Perspectives is a feature for Sanity Content Lake that lets you run the same queries but pull the right content variations for any given experience. The default value is raw, which means no special filtering is applied, while published and previewDrafts can be used to optimize for preview and ensure that no draft data leaks into production for authenticated requests.

// ./src/utils/sanity/client.ts
import {createClient} from 'next-sanity'

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
const token = process.env.SECRET_SANITY_VIEW_TOKEN

const client = createClient({
  projectId,
  dataset,
  apiVersion, // https://www.sanity.io/docs/api-versioning
  useCdn: true, // if you're using ISR or only static generation at build time then you can set this to `false` to guarantee no stale content
  token,
  perspective: 'published', // prevent drafts from leaking through even though requests are authenticated
})

Live Preview

Live Preview gives you real-time preview across your whole app for your Sanity project members. The Live Preview can be set up to give the preview experience across the whole app. Live Preview works on the data layer and doesn't require specialized components or data attributes. However, it needs a thin component wrapper to load server-side components into client-side, in order to rehydrate on changes.

Router-specific setup guides for Live Preview:

Since next-sanity/preview is simply re-exporting LiveQueryProvider and useLiveQuery from @sanity/preview-kit, you'll find advanced usage and comprehensive docs in its README. The same is true for next-sanity/preview/live-query.

Using draftMode() to de/activate previews

Next.js gives you a built-in draftMode variable that can activate features like Visual Edit or any preview implementation.

// ./src/utils/sanity/client.ts
import 'server-only'

import {draftMode} from 'next/headers'
import type {QueryParams} from '@sanity/client'
import {createClient, groq} from 'next-sanity'
import {draftMode} from 'next/headers'

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'

const client = createClient({
  projectId,
  dataset,
  apiVersion, // https://www.sanity.io/docs/api-versioning
  useCdn: false,
})

// Used by `PreviewProvider`
export const token = process.env.SANITY_API_READ_TOKEN

const DEFAULT_PARAMS = {} as QueryParams
const DEFAULT_TAGS = [] as string[]

export async function sanityFetch<QueryResponse>({
  query,
  params = DEFAULT_PARAMS,
  tags = DEFAULT_TAGS,
}: {
  query: string
  params?: QueryParams
  tags: string[]
}): Promise<QueryResponse> {
  const isDraftMode = draftMode().isEnabled
  if (isDraftMode && !token) {
    throw new Error('The `SANITY_API_READ_TOKEN` environment variable is required.')
  }

  const REVALIDATE_SKIP_CACHE = 0
  const REVALIDATE_CACHE_FOREVER = false

  return client.fetch<QueryResponse>(query, params, {
    ...(isDraftMode && {
      token: token,
      perspective: 'previewDrafts',
    }),
    next: {
      revalidate: isDraftMode ? REVALIDATE_SKIP_CACHE : REVALIDATE_CACHE_FOREVER,
      tags,
    },
  })
}

Using cache and revalidation at the same time

Be aware that you can get errors if you use the cache and the revalidate configurations for Next.js cache at the same time. Go to the Next.js docs to learn more.

Visual Editing with Content Source Maps

Note

Content Source Maps are available as an API for select Sanity enterprise customers. Contact our sales team for more information.

The createClient method in next-sanity supports visual editing, it supports all the same options as @sanity/preview-kit/client. Add studioUrl to your client configuration and it'll automatically show up on Vercel Preview Deployments:

import {createClient, groq} from 'next-sanity'

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION // "2023-05-03"

const client = createClient({
  projectId,
  dataset,
  apiVersion, // https://www.sanity.io/docs/api-versioning
  useCdn: true, // if you're using ISR or only static generation at build time, then you can set this to `false` to guarantee no stale content
  studioUrl: '/studio', // Or: 'https://my-cool-project.sanity.studio'
  encodeSourceMap: true, // Optional. Default to: process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview',
})

Go to our setup guide for a walkthrough on how to customize the experience.

Embedded Sanity Studio

Sanity Studio allows you to embed a near-infinitely configurable content editing interface into any React application. For Next.js, you can embed the Studio on a route (like /admin). The Studio will still require authentication and be available only for members of your Sanity project.

This opens up many possibilities:

  • Any service that hosts Next.js apps can now host your Studio.
  • Building previews for your content is easier as your Studio lives in the same environment.
  • Use Data Fetching to configure your Studio.
  • Easy setup of Preview Mode.

See it live

Configuring Sanity Studio on a route

The NextStudio component loads up the import {Studio} from 'sanity' component for you and wraps it in a Next-friendly layout. metadata specifies the necessary <meta> tags for making the Studio adapt to mobile devices, and prevents the route from being indexed by search engines.

To quickly scaffold the embedded studio and a Sanity project, you can run the following command in your project folder:

npx sanity@latest init

Manual installation

Make a file called sanity.config.ts (or .js for non-TypeScript projects) in the project's root (same place as next.config.ts) and copy the example below. Both the Next /app and /pages examples use this config file:

// ./sanity.config.ts
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'

import {schemaTypes} from './src/schema'

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!

export default defineConfig({
  basePath: '/admin', // <-- important that `basePath` matches the route you're mounting your studio from, it applies to both `/pages` and `/app`

  projectId,
  dataset,
  plugins: [deskTool()],
  schema: {
    types: schemaTypes,
  },
})

This example assumes that there is a src/schema/index.ts file that exports the schema definitions for Sanity Studio. However, you are free to structure Studio files as you see fit.

To run Sanity CLI commands, add a sanity.cli.ts with the same projectId and dataset as your sanity.config.ts to the project root:

// ./sanity.cli.ts
/* eslint-disable no-process-env */
import {loadEnvConfig} from '@next/env'
import {defineCliConfig} from 'sanity/cli'

const dev = process.env.NODE_ENV !== 'production'
loadEnvConfig(__dirname, dev, {info: () => null, error: console.error})

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET

export default defineCliConfig({api: {projectId, dataset}})

Now you can run commands like npx sanity cors add. Run npx sanity help for a full list of what you can do.

Studio route with App Router

// ./src/app/studio/[[...index]]/page.tsx
import {Studio} from './Studio'

// Ensures the Studio route is statically generated
export const dynamic = 'force-static'

// Set the right `viewport`, `robots` and `referer` meta tags
export {metadata} from 'next-sanity/studio/metadata'

export default function StudioPage() {
  return <Studio />
}
// ./src/app/studio/[[...index]]/Studio.tsx
'use client'

import {NextStudio} from 'next-sanity/studio'

import config from '../../../sanity.config'

export function Studio() {
  //  Supports the same props as `import {Studio} from 'sanity'`, `config` is required
  return <NextStudio config={config} />
}

How to customize meta tags:

// ./src/app/studio/[[...index]]/page.tsx
import type {Metadata} from 'next'
import {metadata as studioMetadata} from 'next-sanity/studio/metadata'

import {Studio} from './Studio'

// Set the right `viewport`, `robots` and `referer` meta tags
export const metadata: Metadata = {
  ...studioMetadata,
  // Overrides the viewport to resize behavior
  viewport: `${studioMetadata.viewport}, interactive-widget=resizes-content`,
}

export default function StudioPage() {
  return <Studio />
}

Studio Routes with Pages Router

// ./pages/studio/[[...index]].tsx
import Head from 'next/head'
import {NextStudio} from 'next-sanity/studio'
import {metadata} from 'next-sanity/studio/metadata'

import config from '../../sanity.config'

export default function StudioPage() {
  return (
    <>
      <Head>
        {Object.entries(metadata).map(([key, value]) => (
          <meta key={key} name={key} content={value} />
        ))}
      </Head>
      <NextStudio config={config} />
    </>
  )
}

Lower level control with StudioProvider and StudioLayout

If you want to go to a lower level and have more control over the Studio, you can pass StudioProvider and StudioLayout from sanity as children:

import {NextStudio} from 'next-sanity/studio'
import {StudioProvider, StudioLayout} from 'sanity'

import config from '../../../sanity.config'

function StudioPage() {
  return (
    <NextStudio config={config}>
      <StudioProvider config={config}>
        {/* Put components here and you'll have access to the same React hooks as Studio gives you when writing plugins */}
        <StudioLayout />
      </StudioProvider>
    </NextStudio>
  )
}

Migration guides

License

MIT-licensed. See LICENSE.