Giter VIP home page Giter VIP logo

next-sanity's Introduction

next-sanity

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

Important

You're looking at the README for v9, the README for v8 is available here as well as an migration guide.

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 Visual Editing:

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

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'

export 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 {createClient, type QueryParams} 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,
})

export async function sanityFetch<QueryResponse>({
  query,
  params = {},
  tags,
}: {
  query: string
  params?: QueryParams
  tags?: string[]
}) {
  return client.fetch<QueryResponse>(query, params, {
    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/route.ts
import {revalidateTag} from 'next/cache'
import {type NextRequest, NextResponse} from 'next/server'
import {parseBody} from 'next-sanity/webhook'

type WebhookPayload = {
  _type: string
}

export async function POST(req: NextRequest) {
  try {
    const {isValidSignature, body} = await parseBody<WebhookPayload>(
      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(JSON.stringify({message, body}), {status: 400})
    }

    // If the `_type` is `page`, then all `client.fetch` calls with
    // `{next: {tags: ['page']}}` will be revalidated
    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 webhook

If you want on-demand revalidation, without using tags, 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}.

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

type WebhookPayload = {
  _type: string
  slug?: {
    current?: string
  }
}

export async function POST(req: NextRequest) {
  try {
    const {isValidSignature, body} = await parseBody<WebhookPayload>(
      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 || !body?.slug?.current) {
      const message = 'Bad Request'
      return new Response(JSON.stringify({message, body}), {status: 400})
    }

    const staleRoute = `/${body.slug.current}`
    revalidatePath(staleRoute)
    const message = `Updated route: ${staleRoute}`
    return NextResponse.json({body, message})
  } catch (err) {
    console.error(err)
    return new Response(err.message, {status: 500})
  }
}

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 = {
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
}

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 {createClient, type QueryOptions, type QueryParams} 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,
})

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

export async function sanityFetch<QueryResponse>({
  query,
  params = {},
  tags,
}: {
  query: string
  params?: QueryParams
  tags: string[]
}) {
  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',
      } satisfies QueryOptions)),
    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

Vercel Visual Editing is available on Vercel's Pro and Enterprise plans and on all Sanity plans.

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
  stega: {
    enabled: NEXT_PUBLIC_VERCEL_ENV === 'preview', // this can also be controlled in `client.fetch(query, params, {stega: boolean})`
    studioUrl: '/studio', // Or: 'https://my-cool-project.sanity.studio'
  },
})

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:

// ./sanity.config.ts
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'

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

  projectId,
  dataset,
  plugins: [structureTool()],
  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

Even if the rest of your app is using Pages Router, you should still mount the Studio on an App Router route. Next supports both routers in the same app.

// ./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, viewport} from 'next-sanity/studio'

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, Viewport} from 'next'
import {metadata as studioMetadata, viewport as studioViewport} from 'next-sanity/studio'

import {Studio} from './Studio'

// Set the right `viewport`, `robots` and `referer` meta tags
export const metadata: Metadata = {
  ...studioMetadata,
  // Overrides the title until the Studio is loaded
  title: 'Loading Studio…',
}

export const viewport: Viewport = {
  ...studioViewport,
  // Overrides the viewport to resize behavior
  interactiveWidget: 'resizes-content',
}

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

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:

'use client'

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.

next-sanity's People

Contributors

bjoerge avatar bolajiayodeji avatar codyolsendummy avatar ecospark[bot] avatar github-actions[bot] avatar hamzah-syed avatar jakubfiglak avatar jamessingleton avatar jankups avatar jfulse avatar judofyr avatar kmelve avatar marcusforsberg avatar mariuslundgard avatar mavic111 avatar mvllow avatar nyashanziramasanga avatar raffij avatar ralstonia avatar renovate[bot] avatar rexxars avatar semantic-release-bot avatar shermanhui avatar silvio-e avatar snorrees avatar stipsan avatar tegacreatives avatar tylerzey avatar valse avatar williamli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

next-sanity's Issues

Does not accept a token in the config, thus not working outside Chrome

Hello, thanks for a great library!

On non-Chrome browser, due to recent changes, we have to explicitly get the token from local storage and set it in our client (the token being __studio_auth_token_[projectId], introduced here.

However this means that the preview from createPreviewSubscriptionHook fails with "Not authenticated - preview not available", since it doesn't currently accept an explicit token.

Please allow for GraphQL query strings in createPreviewSubscriptionHook

Please correct me if I'm wrong, but I'd really love to not have to rewrite all of my queries in groq to allow for live previews of blog posts.

I'd love to just do something like:

const postQuery = `
    query($slug: String!) {
      allPost(where: { slug: { current: { eq: $slug } } }) {
        _id
        title
        publishDate
        content
        slug {
          current
        }
      }
    }
 `

const {data: post} = usePreviewSubscription(postQuery, {
    params: {slug: data.post.slug},
    initialData: data,
    enabled: preview,
  })

Upgrading to Next 13.0.3 breaks usage of client in RSC.

Description

I upgraded to Next 13.0.3 today and using the sanity client now does not work in RSC as the error presents that the component is using hooks. Everything works when using Next version 13.0.2. Next sanity version is latest, v1.0.9
image

I was under the impression based on examples that using the client in RSC would be impossible, please correct me if I am wrong :)

This bug is introduced by Next 13.0.3 so it might be better to report this there, please let me know what is most appropriate.

Temp fix

Downgrade to Next 13.0.2

Steps to reproduce

  1. Clone the following repository: https://github.com/Gawdfrey/next-sanity-13.0.3-issues
  2. Add a .env.local file with NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET variables.
  3. Run the project with pnpm run dev
  4. See the error
  5. Downgrade to Next 13.0.2 to see the example working fine.

Usage with Next.js 13 ./app

Hi, currently trying to integrate the Sanity Studio inside my site with <NextStudio ... . My project is using the new ./app directory and "sanity": "3.0.0-rc.0" + "next-sanity": "1.0.8",.

Currently getting this error.

image

I can of course install styled-components as a dependency, and that appears to let it attempt to render.
image

But that breaks all of my other pages in ./app.

image

My branch testing this is here: alvarlagerlof/portfolio#610

useGroqBeta breaks preview

Hi

We really love live preview and view it as one of the "wow" moments when showing off Sanity to clients.

Unfortunately for a lot of our projects the query size causes the browser to basically break so we've had to stop using this approach. Therefore we are really excited about his beta!

Using "next-sanity": "^0.4.1-beta.0" the preview works great as intended. But when adding:

export const usePreviewSubscription = createPreviewSubscriptionHook({
  ...config,
  useGroqBeta : true
})

It just stops working without any errors.

Api-versioning

I doesn't look like it's possible to turn on api-verisoning for the client, would love support for that :)

Especially I would like to turn it on for the createPreviewSubscriptionHook, but when I pass apiVersion to the createPreviewSubscriptionHook it still seems to be using the v1-endpoint.

export const usePreviewSubscription = createPreviewSubscriptionHook({
  dataset: "production",
  projectId: "abc123",
  apiVersion: 'X'
});

From network-tab I can see it still using the v1-endpoint, don't know if there is a smarter way to see what version the client is using.

image

https://sanity-io-land.slack.com/archives/C013NG0UF0B/p1618038372004900

Can't deploy because of dependency issue

This is likely being worked on already but this package's current preview-kit dependency is a couple minor releases behind one that addresses a missing file and directory; that repo just got updated to address it:
sanity-io/preview-kit#127

Until next-sanity is reconciled (absent other guidance), that seems to affect our ability to build out:

Cannot find module '/vercel/path0/node_modules/.pnpm/[email protected]_r34pbuycespj3pgrjn6rawo43a/node_modules/@sanity/preview-kit/node/index.js' imported from /vercel/path0/node_modules/.pnpm/[email protected]_r34pbuycespj3pgrjn6rawo43a/node_modules/next-sanity/dist/preview.js

Copying and pasting the files resolves the issue but that won't work for a Vercel build relying on accurate versions, and deleting the existing version (or /node_modules/ itself) and reverting doesn't seem to affect things easier.

EDIT: Changed explanation for clarity

Next Studio folder Failing `next build` due to Webpack errors

Hello 👋🏼 ,

I'm using Next 13 with the appDir. I have tried this with the pages directory as well. When I try to run next build the build always fails with:

info  - Creating an optimized production build...
--
21:48:20.761 | Failed to compile.
21:48:20.761 |  
21:48:20.762 | ./node_modules/sanity/node_modules/@sanity/ui/dist/index.esm.js
21:48:20.763 | Module parse failed: 'import' and 'export' may appear only with 'sourceType: module' (61:0)
21:48:20.763 | You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
21:48:20.763 | \| function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
21:48:20.767 | \| function _toPrimitive(input, hint) { if (typeof input !== "object" \|\| input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint \|\| "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
21:48:20.767 | > import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
21:48:20.768 | \| import { useMemo, useState, useRef, useEffect, createContext, useContext, useLayoutEffect, forwardRef, useId, useCallback, cloneElement, isValidElement, createElement, Component, memo, useReducer, Children, Fragment as Fragment$1 } from 'react';
21:48:20.768 | \| import ReactIs, { isElement as isElement$1, isFragment, isValidElementType } from 'react-is';
21:48:20.768 |  
21:48:20.769 | Import trace for requested module:
21:48:20.769 | ./node_modules/sanity/node_modules/@sanity/ui/dist/index.esm.js
21:48:20.770 | ./node_modules/sanity/lib/desk.esm.js
21:48:20.770 | ./sanity.config.ts
21:48:20.770 | ./app/studio/[[...index]]/loading.tsx
21:48:20.771 |  
21:48:20.775 |  
21:48:20.776 | > Build failed because of webpack errors

Happy to give access to my repo to anyone specific. It is a fairly small repo too.

Though here are the specs atm:

app/studio/[[...index]]/head.tsx

import { NextStudioHead } from 'next-sanity/studio/head'

const CustomStudioHead = () => {
	return (
		<>
			<NextStudioHead favicons={false} />
			<link
				rel="icon"
				type="image/png"
				sizes="32x32"
				href="https://www.sanity.io/static/images/favicons/favicon-32x32.png"
			/>
		</>
	)
}

export default CustomStudioHead

app/studio/[[...index]]/loading.tsx

'use client'

import NextStudioLoading from 'next-sanity/studio/loading'
import config from '../../../sanity.config'

export default () => {
	return <NextStudioLoading config={config} />
}

app/studio/[[...index]]/page.tsx

'use client'

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

const Page = () => {
	return <NextStudio config={config} />
}

export default Page

sanity.config.ts

import { visionTool } from '@sanity/vision'
import { defineConfig } from 'sanity'
import { deskTool } from 'sanity/desk'
import { publicConfig } from './config'
import { schemaTypes } from './sanity/schemas'

export default defineConfig({
	basePath: '/studio',

	projectId: publicConfig.sanity.projectId,
	dataset: publicConfig.sanity.dataset,

	plugins: [
		deskTool(),
		visionTool(),
	],

	schema: {
		types: schemaTypes,
	},
})

publicConfig is just:

export const publicConfig = {
	sanity: {
		projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID as string,
		dataset: process.env.NEXT_PUBLIC_SANITY_DATASET as string,
	},
}

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    remotePatterns: [
      {
        hostname: 'cdn.sanity.io',
      },
    ],
  },
  redirects: async () => {
    return [
      {
        source: '/aion/schedule',
        destination: '/',
        permanent: false,
      },
      {
        source: '/aion/schedule/america',
        destination: '/aion/schedule/aion-america-schedule',
        permanent: false,
      },
    ]
  },
  experimental: {
    appDir: true,
  },
}

module.exports = nextConfig

Anything I'm missing?

Parse error with usePreviewSubscription

The Error:
I am trying to use the usePreviewSubscription hook with next-sanithy, though i'm having an issue where I am not receiving draft data.

My code:

query:

const query = groq`
  *[_type == 'product' && slug.current == $handle][0] {
    ...,
    productPage {
      ...,
      related[] -> {
        slug,
        'series': productPage.series,
        'seriesVariant': productPage.seriesVariant,
      }
    }
  }
`;

hook:

  const { data: productData, error } = usePreviewSubscription(query, {
    params: { slug: initialCmsProduct.slug.current },
    initialData: initialCmsProduct,
    enabled: true,
  });

  console.log(error)

Error message
Here is what is outputted when log the error property destructured from the hook:

    at parse (groq-js.esm.js?3052:1166)
    at Module.parse$1 (groq-js.esm.js?3052:5424)
    at eval (groq-store.js?181b:1)

Additional details
It seems to be listening to document updates because when I make a change in the respective document, the error logs once again.

Update `groq-store` dependency

This PR on groq-store fixed a bug that typically effects use of Queries in internationalisation.

For example: title[$lang]

Would be good to have this updated in next/sanity so effected queries in live preview can function correctly.

PublicAccessOnly error

I am logged in but i am still getting an error for preview mode
image

`"use client";

import { definePreview } from "next-sanity/preview";
import { projectId, dataset } from "./sanity.client";

function onPublicAccessOnly() {
throw new Error(Unable to load preview as you're not logged in);
}

if (!projectId || !dataset) {
throw new Error(
"Missing ProjectId or dataset. Check your sanity.json or .env"
);
}

export const usePreview = definePreview({
projectId,
dataset,
onPublicAccessOnly,
});
`

Not authenticated - preview not available

This issue is the same as my previous closed one: #38. Reason I create a new issue is to make it simpler to understand as I've come to find a easy way to reproduce this.

If you wrap createPreviewSubscriptionHook in a wrapper component, it will trigger this infinite "Not authenticated - preview not available" loop.

example:

// sanity.ts
const config = {
    projectId: 'xxxxxx',
    dataset: 'xxxx',
    apiVersion: '2021-03-25',
    useCdn: process.env.NODE_ENV === 'production',
    token: ''
};

export const sanityClient= ({ usePreview, locale }: any) => {
    const configCopy = JSON.parse(JSON.stringify(config));
    
    // IDEA: option to switch dataset based on locale parameter
    // configCopy.dataset = locale

    if (usePreview) {
        configCopy.useCdn = false;
        configCopy.token = process.env.SANITY_API_TOKEN as string;
    }

    return createClient(configCopy);
};

// Creates infinite loop - Simple way to reproduce 
export const usePreviewSubscription = () => createPreviewSubscriptionHook({ ...config });

// Creates infinite loop - My ideal usecase
export const usePreviewSubscription = (locale) => createPreviewSubscriptionHook({ ...config, dataset: locale });

// Works, but not for my use case (switch dataset based on locale)
export const usePreviewSubscription = createPreviewSubscriptionHook({ ...config });
// pages/mypage.tsx
    const previewSubscription = usePreviewSubscription;

    const { data: previewContents } = previewSubscription(data?.query, {
        params: data?.queryParams ?? {},
        initialData: [data?.page],
        enabled: preview,
    });

    // Client-side uses the same query, so we may need to filter it down again
    const page = filterDataToSingleItem(previewContents, preview);

Clipboard paste does not work in block content editor

When I tried to paste something into the block content editor, it tells me this:

Unhandled Runtime Error
TypeError: undefined is not a function

Call Stack
Array.find
<anonymous>
blockContentFeatures
../../node_modules/.pnpm/@[email protected]/node_modules/@sanity/block-tools/lib/index.js (24:0)
createRuleOptions
../../node_modules/.pnpm/@[email protected]/node_modules/@sanity/block-tools/lib/index.js (370:0)
new HtmlDeserializer
../../node_modules/.pnpm/@[email protected]/node_modules/@sanity/block-tools/lib/index.js (918:0)
htmlToBlocks
../../node_modules/.pnpm/@[email protected]/node_modules/@sanity/block-tools/lib/index.js (1218:0)
editor.insertTextOrHTMLData
../../node_modules/.pnpm/@[email protected]_3zqndmw22mpijhrn6grpsril3e/node_modules/@sanity/portable-text-editor/lib/index.js (3413:47)
editor.insertData
../../node_modules/.pnpm/@[email protected]_3zqndmw22mpijhrn6grpsril3e/node_modules/@sanity/portable-text-editor/lib/index.js (3446:0)
eval
../../node_modules/.pnpm/@[email protected]_3zqndmw22mpijhrn6grpsril3e/node_modules/@sanity/portable-text-editor/lib/index.js (4719:0)
isEventHandled
../../node_modules/.pnpm/@[email protected]_ve2o5usd253mp6cjwaeru2n3py/node_modules/@sanity/slate-react/dist/index.es.js (2306:0)
eval
../../node_modules/.pnpm/@[email protected]_ve2o5usd253mp6cjwaeru2n3py/node_modules/@sanity/slate-react/dist/index.es.js (2217:0)
HTMLUnknownElement.callCallback
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (3945:0)
Object.invokeGuardedCallbackDev
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (3994:0)
invokeGuardedCallback
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (4056:0)
invokeGuardedCallbackAndCatchFirstError
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (4070:0)
executeDispatch
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (8243:0)
processDispatchQueueItemsInOrder
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (8275:0)
processDispatchQueue
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (8288:0)
dispatchEventsForPlugins
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (8299:0)
eval
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (8508:0)
batchedEventUpdates$1
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (22396:0)
batchedEventUpdates
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (3745:0)
dispatchEventForPluginEventSystem
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (8507:0)
attemptToDispatchEvent
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (6005:0)
dispatchEvent
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (5924:0)
unstable_runWithPriority
../../node_modules/.pnpm/[email protected]/node_modules/scheduler/cjs/scheduler.development.js (468:0)
runWithPriority$1
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (11276:0)
discreteUpdates$1
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (22413:0)
discreteUpdates
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (3756:0)
dispatchDiscreteEvent
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.development.js (5889:0)

Google Chrome 2022-09-27 at 17 06 10@2x

How do I fix it 🤔 ?

Previews in Safari Version 15.1 MacOS do not authenticate

I am unable to get Safari to display previews, Chrome renders them fine.

the usePreviewSubscription hook returns this error object within it's response

Error: Not authenticated - preview not available
column: 22
line: 180
message: "Not authenticated - preview not available"
stack: "@↵promiseReactionJob@[native code]"

My code

  const data = usePreviewSubscription(getGuide, {
    params: { slug: slug },
    initialData: content,
    enabled: preview
  })

Also this console.warn message is shown in the console "Not authenticated - preview not available" within the function below from the next-sanity.esm.js file

useDeepCompareEffectNoCheck(function () {
    if (!enabled) {
      return function () {
        /* intentional noop */
      };
    }

    setLoading(true);
    var aborter = getAborter();
    var subscription;
    getCurrentUser(projectId, aborter).then(function (user) {
      if (user) {
        return;
      } // eslint-disable-next-line no-console


      console.warn('Not authenticated - preview not available');
      throw new Error('Not authenticated - preview not available');
    }).then(getStore).then(function (store) {
      subscription = store.subscribe(query, params, function (err, result) {
        if (err) {
          setError(err);
        } else {
          setData(result);
        }
      });
    }).catch(setError).finally(function () {
      return setLoading(false);
    });
    return function () {
      if (subscription) {
        subscription.unsubscribe();
      }

      aborter.abort();
    };
  }, [getStore, query, params, enabled]);
  return {
    data: typeof data === 'undefined' ? initialData : data,
    loading: loading,
    error: error
  };
}

Versions
Safari is Version 15.1 (16612.2.9.1.30, 16612)

next app

"@sanity/client": "^2.21.7",
"next": "^12.0.1",
"next-sanity": "^0.4.0", 

studio app

"@sanity/base": "^2.21.9",
"@sanity/components": "^2.14.0",
"@sanity/core": "^2.21.8",
"@sanity/default-layout": "^2.21.9",
"@sanity/default-login": "^2.21.9",
"@sanity/desk-tool": "^2.21.9",
"@sanity/production-preview": "^2.15.0",
"@sanity/vision": "^2.21.9",

Optimizing Bundle Size

I'm currently trying to optimize my bundle size and this module has become a sticking point. I've follow the instructions in the README but @sanity/client is still being included in the bundle.

If I remove the import import { createPreviewSubscriptionHook } from "next-sanity"; from my sanity.ts and add

export const usePreviewSubscription = (query: string, options: PreviewOptions) => { //createPreviewSubscriptionHook(config);
  return {
    data: options.initialData,
  };
}

in place of export const usePreviewSubscription = createPreviewSubscriptionHook(config);

I no longer see @sanity/client included in the bundle.

I'm testing this with nextjs-12.2.3-canary.10 using webpack5 configuration, unmodified from the default and @next/bundle-analyzer-12.1.6

React Hook "usePreviewSubscription" is called conditionally. React Hooks must be called in the exact same order in every component render

Next.js 11 now supports ESLint out of the box.

After upgrading to Next.js 11 and installing/running ESLint, there is one issue related to next-sanity that I'm not quite sure how to solve:

Error: React Hook "usePreviewSubscription" is called conditionally. React Hooks must be called in the exact same order in every component render.  react-hooks/rules-of-hooks

This is happening because of this return ErrorPage we have here:

  if (!router.isFallback && !data.post?.slug) {
    return <ErrorPage statusCode={404} />
  }

  const {data: post} = usePreviewSubscription(postQuery, {
    params: {slug: data.post.slug},
    initialData: data.post,
    enabled: preview,
  })

One possible solution that I found is to make usePreviewSubscription to accept a function like useEffect does so it's possible to call usePreviewSubscription sooner in the component and inside the passed function, use the condition !router.isFallback && !data.post?.slug.

But this is a guess. I would like to hear from experienced developers so we know how to make this code 100% compatible with React standards.

Websocket doesnt get closed in Firefox for the usePreviewSubscription hook

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.


Using a curated preset maintained by


Sanity: The Composable Content Cloud

Pending Approval

These branches will be created by Renovate only once you click their checkbox below.

  • chore(deps): update dependency eslint-config-next to v14.3.0-canary.39
  • chore(deps): update dependency next to v14.3.0-canary.39
  • chore(deps): update nextjs monorepo to v14.3.0-canary.39 (@next/env, next)
  • chore(deps): update vitest monorepo to ^1.6.0 (@vitest/coverage-v8, vitest)
  • chore(deps): lock file maintenance
  • 🔐 Create all pending approval PRs at once 🔐

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

github-actions
.github/workflows/browserslist.yml
  • actions/checkout v4
  • pnpm/action-setup v3
  • actions/setup-node v4
  • actions/create-github-app-token v1
  • peter-evans/create-pull-request v6@6d6857d36972b65feb161a90e484f2984215f83e
.github/workflows/ci.yml
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
.github/workflows/lock.yml
  • dessant/lock-threads v5@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771
.github/workflows/prettier.yml
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
  • actions/cache v4
  • actions/create-github-app-token v1
  • peter-evans/create-pull-request v6@6d6857d36972b65feb161a90e484f2984215f83e
.github/workflows/release-please-workaround.yml
  • actions/create-github-app-token v1
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
  • EndBug/add-and-commit v9@a94899bca583c204427a224a7af87c02f9b325d5
.github/workflows/release-please.yml
  • actions/create-github-app-token v1
  • google-github-actions/release-please-action v3
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
npm
apps/mvp/package.json
  • @sanity/assist ^3.0.0
  • @sanity/image-url ^1.0.2
  • @sanity/presentation 1.13.0
  • @sanity/preview-url-secret ^1.6.4
  • @sanity/vision 3.40.0
  • groqd ^0.15.10
  • next 14.3.0-canary.37
  • react ^18.2.0
  • react-dom ^18.2.0
  • sanity 3.40.0
  • @next/bundle-analyzer 14.2.3
  • @next/env 14.2.3
  • @types/react ^18.3.1
  • @typescript-eslint/eslint-plugin ^7.8.0
  • autoprefixer ^10.4.19
  • eslint ^8.57.0
  • eslint-config-next 14.2.3
  • eslint-config-prettier ^9.1.0
  • eslint-gitignore ^0.1.0
  • eslint-plugin-simple-import-sort ^12.1.0
  • postcss ^8.4.38
  • server-only ^0.0.1
  • tailwindcss ^3.4.3
  • typescript 5.4.5
  • node 20
apps/static/package.json
  • @sanity/assist 3.0.3
  • @sanity/client ^6.15.7
  • @sanity/image-url ^1.0.2
  • @sanity/vision 3.40.0
  • groqd ^0.15.10
  • next 14.3.0-canary.37
  • react ^18.2.0
  • react-dom ^18.2.0
  • sanity 3.40.0
  • @next/bundle-analyzer 14.2.3
  • @next/env 14.2.3
  • @types/react ^18.3.1
  • @typescript-eslint/eslint-plugin ^7.8.0
  • autoprefixer ^10.4.19
  • eslint ^8.57.0
  • eslint-config-next 14.2.3
  • eslint-config-prettier ^9.1.0
  • eslint-gitignore ^0.1.0
  • eslint-plugin-simple-import-sort ^12.1.0
  • postcss ^8.4.38
  • server-only ^0.0.1
  • tailwindcss ^3.4.3
  • typescript 5.4.5
package.json
  • @next/bundle-analyzer 14.2.3
  • @next/env 14.3.0-canary.37
  • eslint-config-next 14.3.0-canary.37
  • next 14.3.0-canary.37
  • prettier ^3.2.5
  • prettier-plugin-packagejson ^2.5.0
  • prettier-plugin-tailwindcss ^0.5.14
  • sanity 3.40.0
  • turbo 1.13.3
  • pnpm 9.0.6
packages/next-sanity/package.json
  • @portabletext/react ^3.0.18
  • @sanity/client ^6.17.2
  • @sanity/preview-kit 5.0.51
  • @sanity/visual-editing 1.8.17
  • groq ^3.37.1
  • history ^5.3.0
  • @sanity/browserslist-config ^1.0.3
  • @sanity/eslint-config-studio ^4.0.0
  • @sanity/pkg-utils ^6.8.11
  • @sanity/webhook 4.0.4
  • @types/react ^18.3.1
  • @typescript-eslint/eslint-plugin ^7.8.0
  • @vitest/coverage-v8 ^1.5.3
  • eslint ^8.57.0
  • eslint-config-prettier ^9.1.0
  • eslint-config-sanity ^7.1.2
  • eslint-gitignore ^0.1.0
  • eslint-plugin-simple-import-sort ^12.1.0
  • ls-engines ^0.9.1
  • next 14.2.3
  • react ^18.3.1
  • styled-components ^6.1.9
  • typescript 5.4.5
  • vitest ^1.5.3
  • vitest-github-actions-reporter ^0.11.1
  • @sanity/client ^6.17.2
  • @sanity/icons ^2.11.3
  • @sanity/types ^3.37.1
  • @sanity/ui ^2.0.11
  • next ^14.1 || >=14.3.0-canary.0 <14.3.0
  • react ^18.2
  • sanity ^3.37.1
  • styled-components ^6.1
  • node >=18.17

  • Check this box to trigger a request for Renovate to run again on this repository

Exclude document types from dataset streamed to browser

Hi!

We use the usePreviewSubscription a lot, but as we use Sanity more and more we have document types that doesn't really need preview. Example is like a "comment" document type. Removing these would make the dataset much lighter as these document types are several thousand but the "previewable" documents are usually a lot fewer.

I understand that this would mean that this content could not be shown in the preview at all.

`createPreviewSubscriptionHook` doesn't work with combined query

Hi folks,

I found that if I use a combined query like this:

const query = groq`
  {
    "page": *[_type == "page" && slug.current == $slug][0] { ... }
    "meta": *[_id == "site_config"] { ... }
  }
`

const Page = ({ data: initialData, preview }) => {
  /*... other stuff ... */
  const { data } = usePreviewSubscription(postQuery, {
    params: { slug: initialData.slug },
    initialData,
    enabled: preview
  })
  
  return ( ... )
}

export default Page

export const getStaticProps: GetStaticProps = async ({ params, preview = false }) => {
  const { slug = "" } = params
  const data = await getClient(preview).fetch(postQuery, { slug })
  return {
    revalidate: 60,
    props: {
      preview,
      data,
    }
  }
}

The hook created from createPreviewSubscriptionHook no longer return preview data . When I separate my query into two, it works as expected. This is how I get the subscription hook:

export const usePreviewSubscription = createPreviewSubscriptionHook({
  projectId: '<projectId>',
  dataset: 'development',
  useCdn: false
})

I have no reproduction to show at the moment, just a quick report in case it actually is a bug & not something I missed. Thanks!

Preview Subscription Connection Interruption

Seeing this error in the console which is preventing the client side preview mode:

The connection to https://6u24pg6u.api.sanity.io/v1/data/listen/production?query=*&effectFormat=mendoza was interrupted while the page was loading.

I'm currently forcing preview to be true (not sure how relevant this variable is when using the client side preview feature).

import ErrorPage from 'next/error'
import { useRouter } from 'next/router'
import { groq } from 'next-sanity'
import {
  getClient,
  usePreviewSubscription,
  urlFor,
  PortableText,
} from '../../../lib/sanity'

const postQuery = groq`
  *[_type == "post" && _id == $postId][0] {
    _id,
    title,
    body,
    mainImage,
    categories[]->{
      _id,
      title
    },
    "slug": slug.current
  }
`

export default function Post({ data, preview }) {
  const router = useRouter()
  if (!router.isFallback && !data.post?.slug) {
    return <ErrorPage statusCode={404} />
  }

  console.log(data)

  const { data: { post } } = usePreviewSubscription(postQuery, {
    params: { postId: data?.post?._id },
    initialData: data,
    enabled: true,
  })

  console.log(post)

  const { title, mainImage, body } = post
  return (
    <article>
      <h2>{title}</h2>
      <figure>
        <img src={urlFor(mainImage).url()} />
      </figure>
      <PortableText
        blocks={body}
        serializers={{
          types: {
            code: (props) => (
              <pre data-language={props.node.language}>
                <code>{props.node.code}</code>
              </pre>
            ),
          },
        }}
      />
      <aside></aside>
    </article>
  )
}

export async function getStaticProps({ params, preview = false }) {
  const post = await getClient(true).fetch(postQuery, {
    postId: params.postId,
  })

  if (!post) {
    return {
      notFound: true,
    }
  }

  return {
    props: {
      preview: true,
      data: { post },
    },
  }
}

export async function getStaticPaths() {
  const paths = await getClient().fetch(
    groq`*[_type == "post" && defined(slug.current)]{ 'slug': slug.current, 'postId': _id }`
  )

  return {
    paths: paths.map(({ slug, postId }) => ({ params: { slug, postId } })),
    fallback: true,
  }
}

Document that usePreviewSubscription live preview has 3000 document dataset limit

We wasted a lot of time setting up live preview only to find out that it needs to stream the entire dataset to the client and because of that there's a default 3000 document limit. You should say that up front in the guides and docs. It's not a great developer experience to find that out the hard way. Imagine, too, folks who do this for their whole codebase from the start only to find out it stops working when they hit 3000?

We would not have bothered experimenting with live preview at all if we knew this feature needed to stream the entire dataset.

Real time update fails silently when using pt::text

When using the latest version ([email protected]) the usePreviewSubscription hook does not return live draft data if the query utilizes the pt::text function.

The query is successful and returns fresh data in vision and also works fine through the client when building the next page. The hook however returns the data passed to the initialData property of the hook. I can see that the subscription is set up correctly, and the page is re-rendered when I do changes in sanity studio, but the data from the preview subscription hook is still returning the initialData. No errors are logged to the console.

This will return initialData 👇

const { data } = usePreviewSubscription(groq`
  *[_type == 'frontpage']{
    ...,
    content[]{
      ...,
      articles[]->{
        _id,
        slug,
        title,
        "preamble": pt::text(preamble)
      }
    }
  }
`, { initialData, enabled })

...while this one will return live updates once the dataset is downloaded and listener is set up👇

const { data } = usePreviewSubscription(groq`
  *[_type == 'frontpage']{
    ...,
    content[]{
      ...,
      articles[]->{
        _id,
        slug,
        title,
        "preamble": "blabla" // 👈 no pt::text(...)
      }
    }
  }
`, { initialData, enabled })

Preview on a route with multiple dynamic segments breaking due to params leaking from other routes

Preview mode is breaking on a route with multiple dynamic segments learn/[courseSlug]/[sectionSlug]/[postSlug].

Is this a flawed implementation of the bottom up approach? When I console.log(params) I am getting the params for every post.

export async function generateStaticParams() {
  const paths = await getAllPostsSlugs();

  function loopParams(posts: Post[]) {
    const params: { courseSlug: string; sectionSlug: string; postSlug: string }[] = [];

    posts.forEach((post) => {
      const postSlug = post?.slug;
      post?.sections?.forEach((section) => {
        const sectionSlug = section?.slug;
        section?.courses?.forEach((course) => {
          const courseSlug = course?.slug;

          params.push({ courseSlug, sectionSlug, postSlug });
        });
      });
    });

    return params;
  }

  return loopParams(paths);
}

Let's say I have a post 1 and 2.
If I visit /course/section/1, I get:

{
  courseSlug: 'course',
  sectionSlug: 'section',
  postSlug: '1'
}
{
  courseSlug: 'course',
  sectionSlug: 'section',
  postSlug: '2'
}

Instead of just:

{
  courseSlug: 'course',
  sectionSlug: 'section',
  postSlug: '1'
}

export 'PreviewSuspense' (reexported as 'PreviewSuspense') was not found in '@sanity/preview-kit'

./node_modules/next-sanity/dist/preview.js
export 'PreviewSuspense' (reexported as 'PreviewSuspense') was not found in '@sanity/preview-kit' (module has no exports)

Import trace for requested module:
./node_modules/next-sanity/dist/preview.js
./app/(learn)/learn/[courseSlug]/[sectionSlug]/[postSlug]/page.tsx

./node_modules/next-sanity/dist/preview.js
export 'definePreview' (reexported as 'definePreview') was not found in '@sanity/preview-kit' (module has no exports)

Import trace for requested module:
./node_modules/next-sanity/dist/preview.js
./app/(learn)/learn/[courseSlug]/[sectionSlug]/[postSlug]/page.tsx

I am not getting this error with nextjs-blog-cms-sanity-v3. Is this error just telling me that something is wrong with the way I set up preview mode? Will keep looking over my files.

Thanks! ❤️

{
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next",
    "lint": "next lint",
    "prepare": "husky install",
    "prettier": "prettier --write .",
    "start": "next start"
  },
  "dependencies": {
    "@docsearch/css": "^3.3.0",
    "@docsearch/react": "^3.3.0",
    "@headlessui/react": "^1.7.5",
    "@heroicons/react": "^2.0.13",
    "@next/env": "^13.0.6",
    "@next/font": "^13.0.6",
    "@portabletext/react": "^2.0.0",
    "@sanity/icons": "^2.1.0",
    "@sanity/image-url": "^1.0.1",
    "@sanity/orderable-document-list": "^1.0.2",
    "@sanity/ui": "^1.0.1",
    "@sanity/vision": "^3.0.6",
    "@sanity/webhook": "^2.0.0",
    "@tabler/icons": "^1.116.1",
    "@tailwindcss/line-clamp": "^0.4.2",
    "@vercel/og": "^0.0.21",
    "@web3forms/react": "^1.1.3",
    "class-variance-authority": "^0.4.0",
    "clsx": "^1.2.1",
    "intl-segmenter-polyfill": "^0.4.4",
    "next": "^13.0.6",
    "next-sanity": "^3.1.3",
    "overlayscrollbars": "^2.0.1",
    "overlayscrollbars-react": "^0.5.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-hook-form-latest": "npm:react-hook-form@^7.40.0",
    "react-is": "^18.2.0",
    "sanitize-html": "^2.7.3",
    "sanity": "^3.0.6",
    "sanity-plugin-asset-source-unsplash": "^1.0.1",
    "sanity-plugin-media": "^2.0.2",
    "satori": "^0.0.44",
    "server-only": "^0.0.1",
    "styled-components": "^5.3.6",
    "suspend-react": "^0.0.8"
  },
  "devDependencies": {
    "@commitlint/cli": "^17.3.0",
    "@commitlint/config-conventional": "^17.3.0",
    "@tailwindcss/forms": "^0.5.3",
    "@types/node": "18.11.12",
    "@types/react": "^18.0.26",
    "@types/react-dom": "^18.0.9",
    "@types/sanitize-html": "^2.6.2",
    "@types/styled-components": "^5.1.26",
    "@typescript-eslint/eslint-plugin": "^5.46.0",
    "@typescript-eslint/parser": "^5.46.0",
    "autoprefixer": "^10.4.13",
    "eslint": "^8.29.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-airbnb-typescript": "^17.0.0",
    "eslint-config-next": "^13.0.6",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-jsx-a11y": "^6.6.1",
    "eslint-plugin-react": "^7.31.11",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-storybook": "^0.6.8",
    "husky": "^8.0.2",
    "postcss": "^8.4.19",
    "prettier": "^2.8.1",
    "prettier-plugin-packagejson": "^2.3.0",
    "prettier-plugin-tailwindcss": "^0.2.1",
    "tailwindcss": "^3.2.4",
    "typescript": "^4.9.4"
  },
  "engines": {
    "node": ">=16.0.0",
    "npm": ">=8.3.0",
    "pnpm": "please-use-npm",
    "yarn": "please-use-npm"
  }
}

v3 ready version of code-input causing schema errors

I've been trying a few different ways of getting this to cooperate, but I keep hitting schema errors in Sanity.

// sanity.config.ts

import dynamic from "next/dynamic"
import { createConfig, Plugin } from "sanity"
import { deskTool } from "sanity/desk"
import structure from "./deskStructure"
import types from "./schemas"
const codeInput = () =>
  dynamic<Plugin<void>>(
    () =>
      import("@sanity/code-input").then(
        (mod) => mod.default.codeInput
      ) as Promise<any>,
    { ssr: false }
  )

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

const config = createConfig({
  name: "default",
  title: "strangelove-ventures",
  projectId,
  dataset,
  plugins: [deskTool({ structure }), codeInput()],
  schema: {
    types,
  },
  basePath: "/studio",
})

export default config

I'm having to use next/dynamic because when I pass the codeInput() function into the config using the usual ES6 import pattern, I get a 'window undefined' error.

I'm getting the schema error when I attempt to add the code type to my blockContent schema:

// blockContent.ts

const blockContent = {
  title: "Block Content",
  name: "blockContent",
  type: "array",
  of: [
    {
      title: "Block",
      type: "block",
      styles: [
        { title: "Normal", value: "normal" },
        { title: "H1", value: "h1" },
        { title: "H2", value: "h2" },
        { title: "H3", value: "h3" },
        { title: "H4", value: "h4" },
        { title: "Quote", value: "blockquote" },
      ],
      marks: {
        decorators: [
          { title: "Strong", value: "strong" },
          { title: "Emphasis", value: "em" },
        ],
        annotations: [
          {
            title: "URL",
            name: "link",
            type: "object",
            fields: [
              {
                title: "URL",
                name: "href",
                type: "url",
              },
            ],
          },
        ],
      },
    },
    {
      type: "image",
      options: { hotspot: true },
    },
    {
      type: "code",
    },
  ],
}

export default blockContent

Relevant deps from my package.json:

{
  "dependencies": {
    "@portabletext/react": "1.0.6",
    "@sanity/code-input": "^3.0.0-v3-studio.2",
    "@sanity/image-url": "^1.0.1",
    "@sanity/next-studio-layout": "^2.0.0-v3-studio.8",
    "next": "12.2.4",
    "next-sanity": "0.5.2",
    "react": "18.1.0",
    "react-dom": "18.1.0",
  },
  "devDependencies": {
    "@types/node": "17.0.41",
    "@types/styled-components": "^5.1.25",
    "eslint": "8.17.0",
    "eslint-config-next": "12.1.6",
    "prettier": "2.6.2",
    "sanity": "^3.0.0-dev-preview.12",
    "sanity-codegen": "^0.9.8",
    "styled-components": "^5.3.5",
    "typescript": "4.7.3"
  }
}

Not authenticated - preview not available

I'm trying to add the preview to our Sanity setup but when navigating to the preview url with cookies set, both sanity studio tab and the newly opened preview tab freezes. With further investigation it seems it freezes due to having an infinite loop trying to fetch /me:

image

It seems to break on this line:

.catch((err: Error) => (err.name === 'AbortError' ? null : Promise.reject(err)))

image

I've tested to navigate in the same browser to the url the function tries to fetch:

return fetch(`https://${projectId}.api.sanity.io/v1/users/me`, {

And this returns me successfully:
image

What is causing this, and might it be related to #35? ( FYI: I'm using Google Chrome on Windows )

User authentication in NextJs 13.

I created a blog using sanity.
Now I want to create a user registration and like system (user can like a post and it will be saved). Can I do it with sanity?

I thought to create a schema for user, and then just add the liked posts to the array.

ESM isn't published correctly

As there's no type field set in package.json, Node will assume anything with a .js file extension to be CommonJS and treats the exported ESM as CommonJS.

Reproduction:
> npm i next-sanity
> echo 'import { groq } from "next-sanity";' > test.mjs
node test.mjs

Results in:

import { groq } from "next-sanity";
         ^^^^
SyntaxError: Named export 'groq' not found. The requested module 'next-sanity' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'next-sanity';
const { groq } = pkg;

Solution:

  1. Rename all ESM exports to .mjs, and CommonJS exports to .cjs (Recommended).
    or
  2. Set package.json type as "module" (so Node then treats .js as ESM) and then rename all extensions of commonjs exports to .cjs.

Also, if the package is to remain publishing dual bundles, you need to ensure that there's no dual package hazard.

For more information see the Node.js docs: https://nodejs.org/api/packages.html#dual-commonjses-module-packages

Something is missing in next-sanity package?

I've done a "yarn add next-sanity" and as soon as I add:

import { groq } from 'next-sanity'

I get the following errors:

error - ./node_modules/@sanity/client/lib/sanityClient.js:27:0
Module not found: Can't resolve './assets/assetsClient'

Not sure what step I might be missing.

`groq-js` may be incorrectly included in the client bundle

This issue stems from a community question regarding the client bundle size.

@chris-erickson pointed out that their next.js client build included groq-js.

groq-js is expected to show up as a dependency to @sanity/groq-store however it should be code-split and imported dynamically only when a sanity user is logged in.

This issue is an item to investigate that

Huge bundle size with next-sanity/studio

I'm experiencing a massive increase in bundle size in my front-end routes even though they're not connected to the studio/[[...index]].tsx by imports or anything else. It seems that the blog/[slug].tsx route is bundling (almost) all the sanity packages. See following build output:

Route (pages)                              Size     First Load JS
┌ ● / (7935 ms)                            451 B          76.9 kB
├   /_app                                  0 B            74.4 kB
├ ○ /404                                   183 B          74.5 kB
├ ● /blog/[slug] (6103 ms)                 8.52 kB         787 kB
├   └ /blog/hello-world (6103 ms)
└ ○ /studio/[[...index]] (486 ms)          38.7 kB         815 kB
+ First Load JS shared by all              78.1 kB
  ├ chunks/framework-cd882fbc9dfcb48f.js   45.4 kB
  ├ chunks/main-8ad9c38724b57341.js        26.8 kB
  ├ chunks/pages/_app-1560d8dcaf02a188.js  301 B
  ├ chunks/webpack-6d204e4d68f3163f.js     1.84 kB
  └ css/5e77330589c885d9.css               3.75 kB

○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

Following source is used:

studio/[[...index]].tsx

import { studioConfig } from "lib/sanity.config";
import { NextStudio } from "next-sanity/studio";

export default function StudioPage() {
  return <NextStudio config={studioConfig} />;
}

lib/sanity.config.ts

import { codeInput } from "@sanity/code-input";
import { defineConfig } from "sanity";
import { deskTool } from "sanity/desk";
import post from "./schemas/post";

export const config = {
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: "2022-11-16",
  useCdn: typeof document !== "undefined",
};

export const studioConfig = defineConfig({
  basePath: "/studio",
  title: "John Schmidt",
  projectId: config.projectId,
  dataset: config.dataset,
  schema: {
    types: [post],
  },
  plugins: [deskTool(), codeInput()],
});

lib/sanity.server.ts

import { createClient } from "next-sanity";
import { config } from "./sanity.config";

export const client = createClient(config);

blog/[slug].tsx

import type { GetStaticPaths, GetStaticProps } from "next";
import Link from "next/link";
import { ArrowLeft } from "phosphor-react";
import { PortableText } from "@portabletext/react";

import { client } from "lib/sanity.server";
import SanityImage from "components/SanityImage";

export const getStaticPaths: GetStaticPaths = async () => {
  const data = await client.fetch(`*[_type == "post"] { "slug":slug.current }`);
  const paths = data.map((post: any) => ({ params: { slug: post.slug } }));
  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const data = await client.fetch(
    `*[_type == "post" && slug.current == $slug][0] {
    title,
    body[] {
        ...,
        _type == "image" => {
          ...,
          ...(asset -> {
            "lqip": metadata.lqip,
            "dimensions": metadata.dimensions,
          })
        },
      },
  }`,
    { slug: params?.slug }
  );
  return { props: { data } };
};

type PostPageProps = {
  data: any;
};

const PostPage = ({ data }: PostPageProps) => {
  return (
    <>
      <nav className="p-6">
        <Link
          href="/"
          className="flex items-center space-x-3 text-blue-700 hover:underline"
        >
          <ArrowLeft />
          <span>Back home</span>
        </Link>
      </nav>
      <main className="px-6">
        <article className="prose md:prose-lg porse-zinc dark:prose-invert mx-auto my-12 md:prose-pre:rounded-none prose-pre:rounded-none">
          <h1>{data.title}</h1>
          <PortableText
            value={data.body}
            components={{
              types: {
                image: ({ value }) => (
                  <figure className="-mx-6 lg:-mx-20">
                    <SanityImage
                      src={value}
                      sizes="(min-width: 780px) 780px, 100vw"
                    />
                  </figure>
                ),
                code: ({ value }) => (
                  <pre className="-mx-6 lg:-mx-20">
                    <code>{value.code}</code>
                  </pre>
                ),
              },
            }}
          />
        </article>
      </main>
    </>
  );
};

export default PostPage;

Supply Image component using next/image

It would be great to have an Image component built-in the library, using next/image behind the scenes. It should be possible to use Sanity with it as since [email protected], there is a new loader prop (see next/image documentation).

At least, if not supplying a component leveraging next/image, it would be great to have a code example on how to implement one in the docs or in an example project.

I started developing one myself as a small POC (supports both crop and hotspot features if layout !== 'fill'):

import React, { useCallback } from 'react'
import NextImage, { ImageProps as NextImageProps, ImageLoader } from 'next/image'
import { urlFor } from '../../../clients/sanity'
import { SanityAsset } from '../../../utils/sanity'

// Needed to mostly copy ImageProps from next/image as otherwise Typescript types were all messy
export type ImageProps = Omit<
  JSX.IntrinsicElements['img'],
  'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style'
> & {
  src: {
    alt?: string
    asset: SanityAsset & { url?: string }
  }
  quality?: number | string
  priority?: boolean
  loading?: NextImageProps['loading']
  unoptimized?: boolean
  objectFit?: NextImageProps['objectFit']
  objectPosition?: NextImageProps['objectPosition']
} & (
    | {
        width?: never
        height?: never
        /** @deprecated Use `layout="fill"` instead */
        unsized: true
      }
    | {
        width?: never
        height?: never
        layout: 'fill'
      }
    | {
        width: number | string
        height: number | string
        layout?: 'fixed' | 'intrinsic' | 'responsive'
      }
  )

/**
 * Basic component displaying an Image coming from Sanity using next/image.
 *
 * Supports Crop/Hotspot if height and width are specified (if using a `layout` prop different of "fill")
 *
 * @see https://nextjs.org/docs/api-reference/next/image
 */
export const Image: React.FC<ImageProps> = ({ src, ...props }) => {
  if (!src?.asset?._ref && !src?.asset?.url) {
    console.warn('No Reference passed to image. Make sure the image is correctly set.')
    return null
  }

  // Loader for Sanity. Unfortunately, as we need crop/hotspot properties here, along with the height provided, 
  // We need to have this function defined in the component.
  const sanityLoader = useCallback<ImageLoader>(
    ({ width, quality = 75 }) => {
      const renderWidthInt = _getInt(props.width)
      const renderHeightInt = _getInt(props.height)
      const imageRatio =
        renderWidthInt && renderHeightInt ? renderWidthInt / renderHeightInt : undefined

      let urlBuilder = urlFor(src)
        .auto('format') // Load webp if supported by browser
        .fit('max') // Don't scale up images of lower resolutions
        .width(width)
        .quality(quality)

      if (renderHeightInt && imageRatio) {
        urlBuilder = urlBuilder.height(width / imageRatio)
      }

      return urlBuilder.url() || ''
    },
    [src, props.width, props.height],
  )

  return (
    <NextImage
      {...props}
      alt={src.alt || ''}
      src={src.asset._ref || src.asset.url || ''}
      loader={sanityLoader}
    />
  )
}

const _getInt = (x: string | number | undefined): number | undefined => {
  if (typeof x === 'number') {
    return x
  }
  if (typeof x === 'string') {
    return parseInt(x, 10)
  }
  return undefined
}

Why it doesn't work on linux?

I tried do the npm install next-sanity @portabletext/react @sanity/image-url and the terminal output for me was like that:

npm WARN read-shrinkwrap This version of npm is compatible with lockfileVersion@1, but package-lock.json was generated for lockfileVersion@2. I'll try to do my best with it!
npm ERR! code EBADPLATFORM
npm ERR! notsup Unsupported platform for [email protected]: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm ERR! notsup Valid OS:    darwin
npm ERR! notsup Valid Arch:  any
npm ERR! notsup Actual OS:   linux
npm ERR! notsup Actual Arch: x64

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/remessaonline/.npm/_logs/2022-03-04T20_57_21_096Z-debug.log

So I can't continue the project I was doing because of this

Typescript Build Error: property token does not exist

Hi guys!

I have been trying to fix this for some time, but I could not find any solution.. I did follow the documentation but I still get an annoying typeerror on previewData().token. I do use the appDir and this is an example component:

export default async function IndexRoute(props: any) {
  // Fetch queries in parallel
  const [home, navigation] = await Promise.all([
    getHomePage(),
    getNavigation(),
  ]);

  if (previewData()) {
    const token = previewData()?.token || null;

    return (
      <PreviewSuspense
        fallback={
          <IndexPage home={home} navigation={navigation} loading preview />
        }
      >
        <PreviewIndexPage home={home} navigation={navigation} token={token} />
      </PreviewSuspense>
    );
  }

  return <IndexPage home={home} navigation={navigation} />;
}

const token = previewData()?.token || null; will give the following erorr:

Property 'token' does not exist on type 'string | false | object'.
  Property 'token' does not exist on type 'string'.ts(2339)

Anyone else experienced this and what could be a solution?

Documentation proposal: Remove unecessary context.preview from getStaticProps code example

In the README.md it says:

export async function getStaticProps({params, preview = false}) {}

The README.md mentions that NextJS preview-mode can be used. But I would argue that it's confusing to include this context.preview variable in the example because it will never be true unless the user sets up the necessary API endpoint.

It really seems like Sanity does not need NextJS's preview mode because it checks to see if the user is logged into Sanity, and relies on that login session to fetch preview data.

So, I propose:

  • Removing unused example code related to NextJS's preview mode.
  • Expand the documentation to emphasise the difference between this way of previewing vs. NextJS's preview mode.
  • Optionally create a separate NextJS preview mode example, and explain when one should consider using it. If there's no need to use it the docs should be explicit about that.

Finally, I'd like to say thanks for building a great product. That's why I took the time to write this up because I think this issue might be a step towards making Sanity previews easier to implement.

Support React 18

Currently this project has dependencies preventing users to update to the latest version of React and Next.

This is especially an issue when using npm workspaces as seen below.

npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: [email protected]
npm WARN Found: [email protected]
npm WARN apps/web/node_modules/react
npm WARN   peer react@"17.0.2" from [email protected]
npm WARN   apps/web/node_modules/react-dom
npm WARN     peer react-dom@"^17.0.2 || ^18.0.0-0" from [email protected]
npm WARN     apps/web/node_modules/next
npm WARN   1 more (next)
npm WARN
npm WARN Could not resolve dependency:
npm WARN peer react@"17.0.2" from [email protected]
npm WARN apps/web/node_modules/react-dom
npm WARN   peer react-dom@"^17.0.2 || ^18.0.0-0" from [email protected]
npm WARN   apps/web/node_modules/next
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR!   peer react@"^16.14.0" from @hot-loader/[email protected]
npm ERR!   node_modules/@hot-loader/react-dom
npm ERR!     @hot-loader/react-dom@"^16.9.0-4.12.11" from @sanity/[email protected]
npm ERR!     node_modules/@sanity/server
npm ERR!       @sanity/server@"2.29.5" from @sanity/[email protected]
npm ERR!       node_modules/@sanity/core
npm ERR!         @sanity/core@"^2.29.5" from sanity@undefined
npm ERR!         apps/sanity
npm ERR!   peer react@"^16.8.0 || 17.x" from @reach/[email protected]
npm ERR!   node_modules/@reach/auto-id
npm ERR!     @reach/auto-id@"^0.13.2" from @sanity/[email protected]
npm ERR!     node_modules/@sanity/base
npm ERR!       @sanity/base@"2.29.8" from @sanity/[email protected]
npm ERR!       node_modules/@sanity/default-layout
npm ERR!         @sanity/default-layout@"^2.29.8" from sanity@undefined
npm ERR!         apps/sanity
npm ERR!       7 more (@sanity/default-login, @sanity/desk-tool, ...)
npm ERR!     @reach/auto-id@"^0.13.2" from @sanity/[email protected]
npm ERR!     node_modules/@sanity/desk-tool
npm ERR!       @sanity/desk-tool@"^2.29.8" from sanity@undefined
npm ERR!       apps/sanity
npm ERR!         sanity@undefined
npm ERR!         node_modules/sanity
npm ERR!     1 more (@sanity/form-builder)
npm ERR!   50 more (@reach/utils, @sanity/base, @sanity/default-layout, ...)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^17.0.2 || ^18.0.0-0" from [email protected]
npm ERR! node_modules/next
npm ERR!   peer next@">=10.2.0" from [email protected]
npm ERR!   node_modules/eslint-config-next
npm ERR!     eslint-config-next@"^12.0.8" from [email protected]
npm ERR!     packages/config
npm ERR!       [email protected]
npm ERR!       node_modules/config
npm ERR!
npm ERR! Conflicting peer dependency: [email protected]
npm ERR! node_modules/react
npm ERR!   peer react@"^17.0.2 || ^18.0.0-0" from [email protected]
npm ERR!   node_modules/next
npm ERR!     peer next@">=10.2.0" from [email protected]
npm ERR!     node_modules/eslint-config-next
npm ERR!       eslint-config-next@"^12.0.8" from [email protected]
npm ERR!       packages/config
npm ERR!         [email protected]
npm ERR!         node_modules/config
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /Users/danewilson/.npm/eresolve-report.txt for a full report.

I suspect supporting react 18 will be a big push, since you'll need to also update dependencies within the Sanity ecosystem.

PortableText: Unknown block type errors

I have a few custom blocks:

export default {
  name: 'block.basic',
  title: 'Basic Block',
  type: 'block',
  styles: [
    { title: 'Normal', value: 'normal' },
    { title: 'Normal', value: 'normal' },
  ],
  lists: [],
  marks: {
    decorators: [
      { title: 'Strong', value: 'strong' },
      { title: 'Emphasis', value: 'em' },
      { title: 'Code', value: 'code' },
    ],
    // annotations: [{ type: 'link' }, { type: 'internalLink' }],
  },
};

export default {
  name: 'block.content',
  title: 'Block',
  type: 'block',
  // Styles let you set what your user can mark up blocks with. These
  // correspond with HTML tags, but you can set any title or value
  // you want and decide how you want to deal with it where you want to
  // use your content.
  styles: [
    { title: 'Normal', value: 'normal' },
    { title: 'H1', value: 'h1' },
    { title: 'H2', value: 'h2' },
    { title: 'H3', value: 'h3' },
    { title: 'H4', value: 'h4' },
    { title: 'Quote', value: 'blockquote' },
  ],
  lists: [{ title: 'Bullet', value: 'bullet' }],
  // Marks let you mark up inline text in the block editor.
  marks: {
    // Decorators usually describe a single property – e.g. a typographic
    // preference or highlighting by editors.
    decorators: [
      { title: 'Strong', value: 'strong' },
      { title: 'Emphasis', value: 'em' },
    ],
    // Annotations can be any object structure – e.g. a link or a footnote.
    annotations: [
      {
        title: 'URL',
        name: 'link',
        type: 'object',
        fields: [
          {
            title: 'URL',
            name: 'href',
            type: 'url',
          },
        ],
      },
    ],
  },
};

Out of the box, I am getting Error: Unknown block type "block.content", please specify a serializer for it in the serializers.types prop, while this scenario should be handled gracefully, and fall back to the default serializers automatically.

In order to get it to work, I had to specify @sanity/block-content-to-react as a dependency and implement the following:

import BlockContent from '@sanity/block-content-to-react';

const BlockRenderer = props => {
  return BlockContent.defaultSerializers.types.block(props);
};

const types = {};

types['block.basic'] = BlockRenderer;
types['block.content'] = BlockRenderer;

export default { types };

Feature suggestion: update-events-only subscription hook

Since calls to Next.js' getStaticProps and getServerSideProps must often do further processing on fetched data, it's potentially a false assumption that the user actually wants the data to come through the subscription: rather, it may be more useful to listen for the update event, and then re-trigger the feeding of data from getStaticProps and/or getServerSideProps. A classic example of this is when the CMS data contains raw MDX code, and this needs to be compiled.

It turns out this can be done my using the Next router, and simply calling router.replace(router.asPath), which will cause all those prop-getting functions to re-run.

Since I just ran in to this issue with datocms' implementation of the same functionality, I offer this simplified hook based on their internal datocms-listen library, which is capable of returning the data from the query but for which I just use it to get the events:

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { subscribeToQuery } from 'datocms-listen';

export function useDatoCmsUpdate({
  query,
  variables = {},
  preview = false,
  updateRef = false, // pass data from gSP in here, to track `isUpdating` state
} = {}) {
  const [error, setError] = useState(null);
  const [status, setStatus] = useState(query ? 'connecting' : 'closed');
  const [isUpdating, setIsUpdating] = useState(false);
  const router = useRouter();
  useEffect(() => {
    if (!updateRef) return;
    setIsUpdating(false);
  }, [updateRef]);
  useEffect(() => {
    if (!query) return () => false; // return early if there's no query, preview mode is inactive
    let unsubscribe;
    async function subscribe() {
      unsubscribe = await subscribeToQuery({
        query,
        variables,
        preview,
        token: process.env.DATOCMS_TOKEN,
        onStatusChange: (s) => {
          setStatus(s);
        },
        onChannelError: (e) => {
          setError(e);
        },
        onUpdate: (updateData) => {
          setError(null);
          if (status !== 'closed') {
            // hat tip: https://www.joshwcomeau.com/nextjs/refreshing-server-side-props/
            router.replace(router.asPath);
            if (updateRef) setIsUpdating(true);
          }
        },
      });
    }
    subscribe();
    return () => {
      unsubscribe && unsubscribe();
    };
  }, []);
  return { error, status, isUpdating };
}

Example imports getClient twice

The example says
import { getClient usePreviewSubscription, urlFor, PortableText } from "../lib/sanity";

but it's not actually exported from there

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.