├── README.md ├── apps ├── static │ ├── .gitignore │ ├── postcss.config.cjs │ ├── app │ │ ├── layout.tsx │ │ ├── studio │ │ │ └── page.tsx │ │ ├── sanity.fetch.ts │ │ ├── sanity.client.ts │ │ ├── globals.css │ │ ├── page.tsx │ │ ├── Image.tsx │ │ └── PostsLayout.tsx │ ├── next.config.ts │ ├── turbo.json │ ├── groqd-client.ts │ ├── .env.local.example │ ├── tsconfig.json │ ├── sanity.cli.ts │ ├── sanity.config.ts │ └── package.json └── mvp │ ├── .gitignore │ ├── app │ ├── (sanity) │ │ ├── studio │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (website) │ │ ├── FormStatus.tsx │ │ ├── live.ts │ │ ├── actions.ts │ │ ├── RefreshButton.tsx │ │ ├── DebugStatus.tsx │ │ ├── layout.tsx │ │ ├── Image.tsx │ │ ├── only-production │ │ │ └── page.tsx │ │ ├── no-resolve-perspective │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── PostsLayout.tsx │ ├── api │ │ ├── draft-mode │ │ │ └── enable │ │ │ │ └── route.ts │ │ └── revalidate-tag │ │ │ └── route.ts │ ├── sanity.client.ts │ └── globals.css │ ├── postcss.config.cjs │ ├── groqd-client.ts │ ├── next.config.ts │ ├── .env.local.example │ ├── turbo.json │ ├── tsconfig.json │ ├── sanity.cli.ts │ ├── sanity.config.ts │ └── package.json ├── packages ├── next-sanity │ ├── src │ │ ├── visual-editing │ │ │ ├── index.ts │ │ │ ├── client-component │ │ │ │ ├── index.ts │ │ │ │ ├── VisualEditingLazy.tsx │ │ │ │ ├── utils.ts │ │ │ │ └── VisualEditing.tsx │ │ │ ├── server-actions │ │ │ │ └── index.ts │ │ │ └── VisualEditing.tsx │ │ ├── draft-mode │ │ │ ├── index.ts │ │ │ └── define-enable-draft-mode.ts │ │ ├── experimental │ │ │ ├── constants.ts │ │ │ ├── types.ts │ │ │ └── client-components │ │ │ │ └── PresentationComlink.tsx │ │ ├── image │ │ │ ├── index.ts │ │ │ ├── imageLoader.ts │ │ │ └── Image.tsx │ │ ├── hooks │ │ │ └── index.ts │ │ ├── live │ │ │ ├── client-components │ │ │ │ ├── live │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── RefreshOnReconnect.tsx │ │ │ │ │ ├── RefreshOnMount.tsx │ │ │ │ │ ├── RefreshOnFocus.tsx │ │ │ │ │ ├── PresentationComlink.tsx │ │ │ │ │ └── SanityLive.tsx │ │ │ │ └── live-stream │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── SanityLiveStreamLazy.tsx │ │ │ │ │ └── SanityLiveStream.tsx │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useIsPresentationTool.ts │ │ │ │ ├── useIsLivePreview.ts │ │ │ │ ├── useDraftMode.ts │ │ │ │ ├── context.ts │ │ │ │ └── usePresentationQuery.ts │ │ │ ├── resolveCookiePerspective.ts │ │ │ ├── utils.ts │ │ │ └── server-actions │ │ │ │ └── index.ts │ │ ├── client.ts │ │ ├── create-data-attribute.ts │ │ ├── studio │ │ │ ├── client-component │ │ │ │ ├── index.ts │ │ │ │ ├── useIsMounted.ts │ │ │ │ ├── NextStudioLazy.tsx │ │ │ │ ├── createHashHistoryForStudio.ts │ │ │ │ └── NextStudio.tsx │ │ │ ├── index.ts │ │ │ ├── NextStudioLayout.tsx │ │ │ ├── NextStudioWithBridge.tsx │ │ │ ├── NextStudioNoScript.tsx │ │ │ └── head.tsx │ │ ├── index.ts │ │ ├── live.ts │ │ ├── isCorsOriginError.ts │ │ ├── live.server-only.ts │ │ └── webhook │ │ │ └── index.ts │ ├── tsconfig.build.json │ ├── turbo.json │ ├── vite.config.ts │ ├── tsconfig.json │ ├── tsconfig.base.json │ ├── test │ │ ├── verifyHistoryVersion.test.ts │ │ └── imageLoader.test.ts │ ├── MIGRATE-v11-to-v12.md │ ├── MIGRATE-v6-to-v7.md │ ├── MIGRATE-v5-to-v6.md │ ├── tsdown.config.ts │ ├── MIGRATE-v10-to-v11.md │ ├── MIGRATE-v4-to-v5-app-router.md │ ├── EXPERIMENTAL-CACHE-COMPONENTS.md │ ├── MIGRATE-v4-to-v5-pages-router.md │ ├── MIGRATE-v7-to-v8.md │ ├── package.json │ ├── MIGRATE-v8-to-v9.md │ ├── MIGRATE-v1-to-v4.md │ └── MIGRATE-v9-to-v10.md ├── sanity-config │ ├── tsconfig.json │ ├── src │ │ ├── schemas │ │ │ ├── index.ts │ │ │ ├── category.ts │ │ │ ├── author.ts │ │ │ ├── post.ts │ │ │ └── blockContent.ts │ │ └── index.tsx │ └── package.json └── typescript-config │ ├── package.json │ └── base.json ├── fixtures ├── fail │ └── server-only-live │ │ ├── next.config.ts │ │ ├── src │ │ └── app │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── package.json │ │ ├── .gitignore │ │ └── tsconfig.json └── pass │ └── server-only-live │ ├── next.config.ts │ ├── src │ └── app │ │ ├── layout.tsx │ │ └── page.tsx │ ├── package.json │ ├── .gitignore │ └── tsconfig.json ├── .changeset └── config.json ├── .oxfmtrc.json ├── .github ├── workflows │ ├── format-if-needed.yml │ ├── renovate.yml │ ├── release.yml │ ├── lock.yml │ └── ci.yml └── renovate.json ├── turbo.json ├── .oxlintrc.json ├── package.json ├── LICENSE ├── pnpm-workspace.yaml └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | ./packages/next-sanity/README.md -------------------------------------------------------------------------------- /apps/static/.gitignore: -------------------------------------------------------------------------------- 1 | public/analyze 2 | next-env.d.ts 3 | -------------------------------------------------------------------------------- /packages/next-sanity/src/visual-editing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './VisualEditing' 2 | -------------------------------------------------------------------------------- /packages/next-sanity/src/draft-mode/index.ts: -------------------------------------------------------------------------------- 1 | export * from './define-enable-draft-mode' 2 | -------------------------------------------------------------------------------- /apps/mvp/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .env*.local 3 | public/studio/static 4 | public/analyze 5 | next-env.d.ts 6 | -------------------------------------------------------------------------------- /packages/next-sanity/src/experimental/constants.ts: -------------------------------------------------------------------------------- 1 | export const PUBLISHED_SYNC_TAG_PREFIX = 'sp:' 2 | export const DRAFT_SYNC_TAG_PREFIX = 'sd:' 3 | -------------------------------------------------------------------------------- /packages/next-sanity/src/image/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | export {Image, type ImageProps} from './Image' 3 | export {imageLoader} from './imageLoader' 4 | -------------------------------------------------------------------------------- /packages/next-sanity/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export * from '../live/hooks' 4 | export {useOptimistic} from '@sanity/visual-editing/react' 5 | -------------------------------------------------------------------------------- /packages/sanity-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json", 3 | "include": ["src/**/*.ts", "src/**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live/client-components/live/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export type {SanityLiveProps} from './SanityLive' 4 | export {SanityLive as default} from './SanityLive' 5 | -------------------------------------------------------------------------------- /fixtures/fail/server-only-live/next.config.ts: -------------------------------------------------------------------------------- 1 | import type {NextConfig} from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | cacheComponents: true, 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /fixtures/pass/server-only-live/next.config.ts: -------------------------------------------------------------------------------- 1 | import type {NextConfig} from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | cacheComponents: true, 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /packages/next-sanity/src/client.ts: -------------------------------------------------------------------------------- 1 | export type * from '@sanity/client' 2 | export {createClient, unstable__adapter, unstable__environment} from '@sanity/client' 3 | export {stegaClean} from '@sanity/client/stega' 4 | -------------------------------------------------------------------------------- /packages/next-sanity/src/create-data-attribute.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type CreateDataAttribute, 3 | createDataAttribute, 4 | type CreateDataAttributeProps, 5 | } from '@sanity/visual-editing/create-data-attribute' 6 | -------------------------------------------------------------------------------- /packages/next-sanity/src/studio/client-component/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export type {NextStudioProps} from './NextStudio' 4 | export {NextStudioLazyClientComponent as NextStudio} from './NextStudioLazy' 5 | -------------------------------------------------------------------------------- /packages/next-sanity/src/visual-editing/client-component/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export type {VisualEditingProps} from './VisualEditing' 4 | export {VisualEditingLazyClientComponent as default} from './VisualEditingLazy' 5 | -------------------------------------------------------------------------------- /apps/mvp/app/(sanity)/studio/page.tsx: -------------------------------------------------------------------------------- 1 | import {NextStudio} from 'next-sanity/studio' 2 | 3 | import config from '@/sanity.config' 4 | 5 | export default function StudioPage() { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /apps/mvp/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | '@tailwindcss/postcss': {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live/client-components/live-stream/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export type {SanityLiveStreamProps} from './SanityLiveStream' 4 | export {SanityLiveStreamLazyClientComponent as default} from './SanityLiveStreamLazy' 5 | -------------------------------------------------------------------------------- /apps/static/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | '@tailwindcss/postcss': {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/next-sanity/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client' 2 | export * from './create-data-attribute' 3 | export * from '@portabletext/react' 4 | export {defineQuery, default as groq} from 'groq' 5 | export {isCorsOriginError} from './isCorsOriginError' 6 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "private": true, 4 | "exports": { 5 | "./base.json": "./base.json" 6 | }, 7 | "dependencies": { 8 | "typescript": "catalog:" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/sanity-config/src/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import author from './author' 2 | import blockContent from './blockContent' 3 | import category from './category' 4 | import post from './post' 5 | 6 | export const schemaTypes = [post, author, category, blockContent] 7 | -------------------------------------------------------------------------------- /packages/next-sanity/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./tsconfig.base", "@sanity/tsconfig/isolated-declarations"], 3 | "include": ["src/**/*.ts", "src/**/*.tsx"], 4 | "exclude": ["dist", "node_modules", "./src/**/*.test.ts", "./src/**/*.test.tsx"] 5 | } 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config/schema.json", 3 | "changelog": ["@changesets/changelog-github", {"repo": "sanity-io/next-sanity"}], 4 | "access": "public", 5 | "baseBranch": "main", 6 | "privatePackages": false 7 | } 8 | -------------------------------------------------------------------------------- /apps/static/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | 3 | export default function RootLayout({children}: {children: React.ReactNode}) { 4 | return ( 5 | 6 | 7 | {children} 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/fail/server-only-live/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode 5 | }>) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/pass/server-only-live/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode 5 | }>) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/next-sanity/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "build": { 6 | "outputs": ["dist/**"] 7 | }, 8 | "test": { 9 | "env": ["GITHUB_ACTIONS"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/next-sanity/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {configDefaults, defineConfig} from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | // don't use vitest to run Bun and Deno tests 6 | exclude: [...configDefaults.exclude, 'test.cjs', 'test.mjs'], 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type DefineSanityLiveOptions, 3 | type DefinedSanityFetchType, 4 | type DefinedSanityLiveProps, 5 | type DefinedSanityLiveStreamType, 6 | defineLive, 7 | } from './live/defineLive' 8 | export {isCorsOriginError} from './isCorsOriginError' 9 | -------------------------------------------------------------------------------- /packages/next-sanity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["dist", "node_modules"], 5 | "compilerOptions": { 6 | "plugins": [ 7 | { 8 | "name": "next" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/mvp/app/(website)/FormStatus.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useFormStatus} from 'react-dom' 4 | 5 | export function FormStatusLabel({idle, pending}: {idle: string; pending: string}) { 6 | const status = useFormStatus() 7 | 8 | return status.pending ? pending : idle 9 | } 10 | -------------------------------------------------------------------------------- /apps/static/app/studio/page.tsx: -------------------------------------------------------------------------------- 1 | import {NextStudio} from 'next-sanity/studio' 2 | 3 | import config from '@/sanity.config' 4 | 5 | export {metadata, viewport} from 'next-sanity/studio' 6 | 7 | export default function Studio() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /apps/static/next.config.ts: -------------------------------------------------------------------------------- 1 | import type {NextConfig} from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | output: 'export', 5 | logging: { 6 | fetches: { 7 | fullUrl: false, 8 | }, 9 | }, 10 | productionBrowserSourceMaps: true, 11 | } 12 | 13 | export default nextConfig 14 | -------------------------------------------------------------------------------- /packages/next-sanity/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/tsconfig/strictest", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "noUncheckedIndexedAccess": false, 7 | "exactOptionalPropertyTypes": false, 8 | "jsx": "react-jsx" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/static/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "build": { 6 | "env": ["NEXT_PUBLIC_SANITY_PROJECT_ID", "NEXT_PUBLIC_SANITY_DATASET"], 7 | "outputs": [".next/**", "!.next/cache/**", "out/**"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/next-sanity/src/studio/index.ts: -------------------------------------------------------------------------------- 1 | export {metadata, viewport} from './head' 2 | export * from './NextStudioLayout' 3 | export * from './NextStudioNoScript' 4 | export {NextStudioWithBridge as NextStudio} from './NextStudioWithBridge' 5 | export {type NextStudioProps} from 'next-sanity/studio/client-component' 6 | -------------------------------------------------------------------------------- /apps/mvp/app/api/draft-mode/enable/route.ts: -------------------------------------------------------------------------------- 1 | import {defineEnableDraftMode} from 'next-sanity/draft-mode' 2 | 3 | import {client} from '@/app/sanity.client' 4 | 5 | export const {GET} = defineEnableDraftMode({ 6 | client: client.withConfig({ 7 | token: process.env.SANITY_API_READ_TOKEN, 8 | }), 9 | }) 10 | -------------------------------------------------------------------------------- /packages/next-sanity/src/isCorsOriginError.ts: -------------------------------------------------------------------------------- 1 | import type {CorsOriginError} from '@sanity/client' 2 | 3 | /** @public */ 4 | export function isCorsOriginError(error: unknown): error is CorsOriginError { 5 | return error instanceof Error && error.name === 'CorsOriginError' 6 | } 7 | 8 | export type {CorsOriginError} 9 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDraftMode' 2 | export type {DraftPerspective, DraftEnvironment} from './context' 3 | export type {ClientPerspective} from '@sanity/client' 4 | export * from './useIsPresentationTool' 5 | export * from './useIsLivePreview' 6 | export * from './usePresentationQuery' 7 | -------------------------------------------------------------------------------- /apps/mvp/app/(sanity)/layout.tsx: -------------------------------------------------------------------------------- 1 | import '../globals.css' 2 | 3 | export {metadata, viewport} from 'next-sanity/studio' 4 | 5 | export default function RootLayout({children}: {children: React.ReactNode}) { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /apps/mvp/app/(website)/live.ts: -------------------------------------------------------------------------------- 1 | import {defineLive} from 'next-sanity/experimental/live' 2 | 3 | import {client} from '@/app/sanity.client' 4 | 5 | const token = process.env.SANITY_API_READ_TOKEN! 6 | 7 | export const {sanityFetch, SanityLive} = defineLive({ 8 | client, 9 | serverToken: token, 10 | browserToken: token, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/next-sanity/src/studio/client-component/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import {useSyncExternalStore} from 'react' 2 | 3 | /** @internal */ 4 | export function useIsMounted(): boolean { 5 | return useSyncExternalStore( 6 | emptySubscribe, 7 | () => true, 8 | () => false, 9 | ) 10 | } 11 | const emptySubscribe = () => () => {} 12 | -------------------------------------------------------------------------------- /.oxfmtrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 | "printWidth": 100, 4 | "semi": false, 5 | "singleQuote": true, 6 | "bracketSpacing": false, 7 | "quoteProps": "consistent", 8 | "experimentalSortImports": {}, 9 | "ignorePatterns": ["dist/**", "pnpm-lock.yaml", ".changeset/*.md", "CHANGELOG.md"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/mvp/groqd-client.ts: -------------------------------------------------------------------------------- 1 | import {createGroqBuilder} from 'groqd' 2 | 3 | import type {AllSanitySchemaTypes, internalGroqTypeReferenceTo} from './sanity.types.ts' 4 | 5 | type SchemaConfig = { 6 | schemaTypes: AllSanitySchemaTypes 7 | referenceSymbol: typeof internalGroqTypeReferenceTo 8 | } 9 | export const q = createGroqBuilder() 10 | -------------------------------------------------------------------------------- /.github/workflows/format-if-needed.yml: -------------------------------------------------------------------------------- 1 | name: Auto format 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | permissions: 10 | contents: read # for checkout 11 | 12 | jobs: 13 | if-needed: 14 | uses: sanity-io/.github/.github/workflows/format.yml@main 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /apps/static/groqd-client.ts: -------------------------------------------------------------------------------- 1 | import {createGroqBuilder} from 'groqd' 2 | 3 | import type {AllSanitySchemaTypes, internalGroqTypeReferenceTo} from './sanity.types.ts' 4 | 5 | type SchemaConfig = { 6 | schemaTypes: AllSanitySchemaTypes 7 | referenceSymbol: typeof internalGroqTypeReferenceTo 8 | } 9 | export const q = createGroqBuilder() 10 | -------------------------------------------------------------------------------- /packages/next-sanity/src/experimental/types.ts: -------------------------------------------------------------------------------- 1 | import type {InitializedClientConfig} from '@sanity/client' 2 | 3 | export interface SanityClientConfig extends Pick< 4 | InitializedClientConfig, 5 | | 'projectId' 6 | | 'dataset' 7 | | 'apiHost' 8 | | 'apiVersion' 9 | | 'useProjectHostname' 10 | | 'token' 11 | | 'requestTagPrefix' 12 | > {} 13 | -------------------------------------------------------------------------------- /apps/mvp/app/(website)/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import {draftMode} from 'next/headers' 4 | 5 | export async function disableDraftMode() { 6 | 'use server' 7 | await Promise.allSettled([ 8 | (await draftMode()).disable(), 9 | // Simulate a delay to show the loading state 10 | new Promise((resolve) => setTimeout(resolve, 1000)), 11 | ]) 12 | } 13 | -------------------------------------------------------------------------------- /apps/mvp/app/(website)/RefreshButton.tsx: -------------------------------------------------------------------------------- 1 | import {refresh} from 'next/cache' 2 | 3 | export function RefreshButton() { 4 | return ( 5 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /apps/mvp/next.config.ts: -------------------------------------------------------------------------------- 1 | import type {NextConfig} from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | // basePath: process.env.NEXT_PUBLIC_TEST_BASE_PATH, 5 | // trailingSlash: true, 6 | cacheComponents: true, 7 | logging: { 8 | fetches: { 9 | fullUrl: false, 10 | }, 11 | }, 12 | productionBrowserSourceMaps: true, 13 | } 14 | 15 | export default nextConfig 16 | -------------------------------------------------------------------------------- /apps/static/app/sanity.fetch.ts: -------------------------------------------------------------------------------- 1 | import {client, type QueryParams} from './sanity.client' 2 | 3 | export const token = process.env.SANITY_API_READ_TOKEN! 4 | 5 | export function sanityFetch({ 6 | query, 7 | params = {}, 8 | }: { 9 | query: string 10 | params?: QueryParams 11 | }) { 12 | return client.fetch(query, params, {cache: 'no-store'}) 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/pass/server-only-live/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import {createClient} from 'next-sanity' 2 | import {defineLive} from 'next-sanity/live' 3 | 4 | const {SanityLive} = defineLive({ 5 | client: createClient({ 6 | projectId: 'pv8y60vp', 7 | dataset: 'production', 8 | apiVersion: '2025-10-24', 9 | useCdn: true, 10 | }), 11 | }) 12 | 13 | export default function Home() { 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /apps/mvp/.env.local.example: -------------------------------------------------------------------------------- 1 | # Setup env by running `npx vercel link && npx vercel env pull` in your cli 2 | # If you don't have access to the Vercel deploy then `cp .env.local.example .env.local` and set these up manually 3 | NEXT_PUBLIC_SANITY_PROJECT_ID="pv8y60vp" 4 | NEXT_PUBLIC_SANITY_DATASET="production" 5 | # Used for Preview Mode, it's exposed clientside for those who launch the preview pages/api/preview 6 | # SANITY_API_READ_TOKEN=... -------------------------------------------------------------------------------- /apps/static/.env.local.example: -------------------------------------------------------------------------------- 1 | # Setup env by running `npx vercel link && npx vercel env pull` in your cli 2 | # If you don't have access to the Vercel deploy then `cp .env.local.example .env.local` and set these up manually 3 | NEXT_PUBLIC_SANITY_PROJECT_ID="pv8y60vp" 4 | NEXT_PUBLIC_SANITY_DATASET="production" 5 | # Used for Preview Mode, it's exposed clientside for those who launch the preview pages/api/preview 6 | # SANITY_API_READ_TOKEN=... -------------------------------------------------------------------------------- /packages/next-sanity/src/visual-editing/server-actions/index.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | import {revalidatePath} from 'next/cache' 3 | import {draftMode} from 'next/headers' 4 | 5 | export async function revalidateRootLayout(): Promise { 6 | if (!(await draftMode()).isEnabled) { 7 | console.warn('Skipped revalidatePath request because draft mode is not enabled') 8 | return 9 | } 10 | revalidatePath('/', 'layout') 11 | } 12 | -------------------------------------------------------------------------------- /packages/sanity-config/src/schemas/category.ts: -------------------------------------------------------------------------------- 1 | import {defineType} from 'sanity' 2 | 3 | export default defineType({ 4 | name: 'category', 5 | title: 'Category', 6 | type: 'document', 7 | fields: [ 8 | { 9 | name: 'title', 10 | title: 'Title', 11 | type: 'string', 12 | }, 13 | { 14 | name: 'description', 15 | title: 'Description', 16 | type: 'text', 17 | }, 18 | ], 19 | }) 20 | -------------------------------------------------------------------------------- /apps/mvp/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "build": { 6 | "env": [ 7 | "NEXT_PUBLIC_SANITY_PROJECT_ID", 8 | "NEXT_PUBLIC_SANITY_DATASET", 9 | "NEXT_PUBLIC_TEST_BASE_PATH", 10 | "SANITY_API_READ_TOKEN" 11 | ], 12 | "outputs": [".next/**", "!.next/cache/**", "public/studio/static/**"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ], 9 | "module": "preserve", 10 | "paths": { 11 | "@/*": ["./*"] 12 | }, 13 | "jsx": "react-jsx" 14 | }, 15 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /apps/static/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import {loadEnvConfig} from '@next/env' 2 | import {defineCliConfig} from 'sanity/cli' 3 | 4 | const dev = process.env.NODE_ENV !== 'production' 5 | loadEnvConfig(__dirname, dev, {info: () => null, error: console.error}) 6 | 7 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID 8 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET 9 | 10 | export default defineCliConfig({api: {projectId, dataset}}) 11 | -------------------------------------------------------------------------------- /fixtures/fail/server-only-live/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {createClient} from 'next-sanity' 4 | import {defineLive} from 'next-sanity/live' 5 | 6 | const {SanityLive} = defineLive({ 7 | client: createClient({ 8 | projectId: 'pv8y60vp', 9 | dataset: 'production', 10 | apiVersion: '2025-10-24', 11 | useCdn: true, 12 | }), 13 | }) 14 | 15 | export default function Home() { 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /apps/mvp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ], 9 | "module": "preserve", 10 | "paths": { 11 | "@/*": ["./*"] 12 | }, 13 | "jsx": "react-jsx" 14 | }, 15 | "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next-env.d.ts", "**/*.mts"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /apps/static/sanity.config.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import sharedConfig from '@repo/sanity-config' 4 | import {defineConfig} from 'sanity' 5 | 6 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID! 7 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET! 8 | 9 | export default defineConfig({ 10 | projectId, 11 | dataset, 12 | 13 | plugins: [sharedConfig()], 14 | 15 | scheduledPublishing: { 16 | enabled: false, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /packages/sanity-config/src/index.tsx: -------------------------------------------------------------------------------- 1 | import {assist} from '@sanity/assist' 2 | import {visionTool} from '@sanity/vision' 3 | import {definePlugin} from 'sanity' 4 | import {structureTool} from 'sanity/structure' 5 | 6 | import {schemaTypes} from './schemas' 7 | 8 | export default definePlugin({ 9 | name: '@repo/sanity-config', 10 | plugins: [assist(), structureTool(), visionTool()], 11 | schema: { 12 | types: schemaTypes, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /.github/workflows/renovate.yml: -------------------------------------------------------------------------------- 1 | name: Add changeset to Renovate updates 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | permissions: 10 | contents: read # for checkout 11 | 12 | jobs: 13 | call: 14 | uses: sanity-io/.github/.github/workflows/changesets-from-conventional-commits.yml@main 15 | if: github.event.pull_request.user.login == 'renovate[bot]' 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /packages/sanity-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/sanity-config", 3 | "private": true, 4 | "exports": { 5 | ".": "./src/index.tsx" 6 | }, 7 | "dependencies": { 8 | "@sanity/assist": "^5.0.3" 9 | }, 10 | "devDependencies": { 11 | "@repo/typescript-config": "workspace:*", 12 | "@sanity/vision": "catalog:", 13 | "sanity": "catalog:" 14 | }, 15 | "peerDependencies": { 16 | "@sanity/vision": "catalog:", 17 | "sanity": "catalog:" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/mvp/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import {loadEnvConfig} from '@next/env' 2 | import {defineCliConfig} from 'sanity/cli' 3 | 4 | const dev = process.env.NODE_ENV !== 'production' 5 | loadEnvConfig(__dirname, dev, {info: () => null, error: console.error}) 6 | 7 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID 8 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET 9 | 10 | export default defineCliConfig({ 11 | api: {projectId, dataset}, 12 | studioHost: `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}/studio`, 13 | }) 14 | -------------------------------------------------------------------------------- /apps/static/app/sanity.client.ts: -------------------------------------------------------------------------------- 1 | import {createClient} from 'next-sanity' 2 | 3 | export const client = createClient({ 4 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, 5 | dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, 6 | apiVersion: '2025-03-04', 7 | useCdn: false, 8 | perspective: 'published', 9 | resultSourceMap: 'withKeyArraySelector', 10 | stega: { 11 | enabled: true, 12 | studioUrl: '/studio/#', 13 | logger: console, 14 | }, 15 | }) 16 | 17 | export type {QueryOptions, QueryParams} from 'next-sanity' 18 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": [".env", ".env.local"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"] 7 | }, 8 | "typegen": {}, 9 | "test": { 10 | "dependsOn": ["^build"], 11 | "cache": false, 12 | "persistent": true 13 | }, 14 | "dev": { 15 | "cache": false, 16 | "persistent": true 17 | }, 18 | "start": { 19 | "dependsOn": ["build"], 20 | "cache": false, 21 | "persistent": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - cache-components 8 | 9 | concurrency: ${{ github.workflow }}-${{ github.ref }} 10 | 11 | permissions: 12 | contents: read # for checkout 13 | 14 | jobs: 15 | release: 16 | uses: sanity-io/.github/.github/workflows/changesets.yml@main 17 | permissions: 18 | contents: read # for checkout 19 | id-token: write # to enable use of OIDC for npm provenance 20 | with: 21 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 22 | secrets: inherit 23 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "strictNullChecks": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "preserve", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/next-sanity/test/verifyHistoryVersion.test.ts: -------------------------------------------------------------------------------- 1 | import sanityJson from 'sanity/package.json' assert {type: 'json'} 2 | import {expect, test} from 'vitest' 3 | 4 | import nextSanityJson from '../package.json' assert {type: 'json'} 5 | 6 | /** 7 | * It's important that the `history` package used by `sanity` to underpin its router is the same we use to implement hash history support 8 | */ 9 | 10 | test('verify that next-sanity requires the same history version as sanity', () => { 11 | expect(nextSanityJson.dependencies.history).toBe(sanityJson.dependencies.history) 12 | }) 13 | -------------------------------------------------------------------------------- /apps/mvp/app/sanity.client.ts: -------------------------------------------------------------------------------- 1 | import {createClient} from 'next-sanity' 2 | 3 | export const client = createClient({ 4 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, 5 | dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, 6 | apiVersion: '2025-03-04', 7 | useCdn: false, 8 | perspective: 'published', 9 | resultSourceMap: 'withKeyArraySelector', 10 | stega: { 11 | enabled: true, 12 | studioUrl: `${process.env.NEXT_PUBLIC_TEST_BASE_PATH || ''}/studio#`, 13 | // logger: console, 14 | }, 15 | }) 16 | 17 | export type {QueryOptions, QueryParams} from 'next-sanity' 18 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live/client-components/live/RefreshOnReconnect.tsx: -------------------------------------------------------------------------------- 1 | import {useRouter} from 'next/navigation' 2 | import {useEffect} from 'react' 3 | 4 | export default function RefreshOnReconnect(): null { 5 | const router = useRouter() 6 | 7 | useEffect(() => { 8 | const controller = new AbortController() 9 | const {signal} = controller 10 | window.addEventListener('online', () => router.refresh(), {passive: true, signal}) 11 | return () => controller.abort() 12 | }, [router]) 13 | 14 | return null 15 | } 16 | RefreshOnReconnect.displayName = 'RefreshOnReconnect' 17 | -------------------------------------------------------------------------------- /apps/mvp/app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* 4 | The default border color has changed to `currentcolor` in Tailwind CSS v4, 5 | so we've added these compatibility styles to make sure everything still 6 | looks the same as it did with Tailwind CSS v3. 7 | 8 | If we ever want to remove these styles, we need to add an explicit border 9 | color utility to any element that depends on these defaults. 10 | */ 11 | @layer base { 12 | *, 13 | ::after, 14 | ::before, 15 | ::backdrop, 16 | ::file-selector-button { 17 | border-color: var(--color-gray-200, currentcolor); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/static/app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* 4 | The default border color has changed to `currentcolor` in Tailwind CSS v4, 5 | so we've added these compatibility styles to make sure everything still 6 | looks the same as it did with Tailwind CSS v3. 7 | 8 | If we ever want to remove these styles, we need to add an explicit border 9 | color utility to any element that depends on these defaults. 10 | */ 11 | @layer base { 12 | *, 13 | ::after, 14 | ::before, 15 | ::backdrop, 16 | ::file-selector-button { 17 | border-color: var(--color-gray-200, currentcolor); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/next-sanity/src/studio/NextStudioLayout.tsx: -------------------------------------------------------------------------------- 1 | /** @public */ 2 | export interface NextStudioLayoutProps { 3 | children: React.ReactNode 4 | } 5 | 6 | const style = { 7 | height: '100vh', 8 | maxHeight: '100dvh', 9 | overscrollBehavior: 'none', 10 | WebkitFontSmoothing: 'antialiased', 11 | overflow: 'auto', 12 | } satisfies React.CSSProperties 13 | 14 | /** @public */ 15 | export const NextStudioLayout = ({children}: NextStudioLayoutProps): React.JSX.Element => { 16 | return ( 17 |
18 | {children} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lock Threads 3 | 4 | on: 5 | issues: 6 | types: [closed] 7 | pull_request: 8 | types: [closed] 9 | schedule: 10 | - cron: '0 0 * * *' 11 | workflow_dispatch: 12 | 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | concurrency: 18 | group: ${{ github.workflow }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | action: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6 26 | with: 27 | issue-inactive-days: 0 28 | pr-inactive-days: 7 29 | -------------------------------------------------------------------------------- /fixtures/pass/server-only-live/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fixtures/pass/server-only-live", 3 | "private": true, 4 | "description": "defineLive can only be called from a server component", 5 | "scripts": { 6 | "test": "next build", 7 | "typegen": "next typegen" 8 | }, 9 | "dependencies": { 10 | "next": "catalog:", 11 | "next-sanity": "workspace:*", 12 | "react": "catalog:", 13 | "react-dom": "catalog:" 14 | }, 15 | "devDependencies": { 16 | "@repo/typescript-config": "workspace:*", 17 | "@types/node": "catalog:", 18 | "@types/react": "catalog:", 19 | "@types/react-dom": "catalog:" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /fixtures/fail/server-only-live/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fixtures/fail/server-only-live", 3 | "private": true, 4 | "description": "defineLive can not be called from a client component", 5 | "scripts": { 6 | "test": "next build && exit 1 || exit 0", 7 | "typegen": "next typegen" 8 | }, 9 | "dependencies": { 10 | "next": "catalog:", 11 | "next-sanity": "workspace:*", 12 | "react": "catalog:", 13 | "react-dom": "catalog:" 14 | }, 15 | "devDependencies": { 16 | "@repo/typescript-config": "workspace:*", 17 | "@types/node": "catalog:", 18 | "@types/react": "catalog:", 19 | "@types/react-dom": "catalog:" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live/resolveCookiePerspective.ts: -------------------------------------------------------------------------------- 1 | import type {ClientPerspective} from '@sanity/client' 2 | 3 | import {perspectiveCookieName} from '@sanity/preview-url-secret/constants' 4 | import {cookies, draftMode} from 'next/headers' 5 | 6 | import {sanitizePerspective} from './utils' 7 | 8 | /** 9 | * @internal 10 | */ 11 | export async function resolveCookiePerspective(): Promise> { 12 | return (await draftMode()).isEnabled 13 | ? (await cookies()).has(perspectiveCookieName) 14 | ? sanitizePerspective((await cookies()).get(perspectiveCookieName)?.value, 'drafts') 15 | : 'drafts' 16 | : 'published' 17 | } 18 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live/client-components/live-stream/SanityLiveStreamLazy.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This file works around a new restriction in Next v15 where server components are not allowed 3 | * to use dynamic(() => import('...), {ssr: false}) 4 | * only Client Components can set ssr: false. 5 | */ 6 | 7 | import dynamic from 'next/dynamic' 8 | 9 | import type {SanityLiveStreamProps} from './SanityLiveStream' 10 | 11 | const SanityLiveStreamClientComponent = dynamic(() => import('./SanityLiveStream'), {ssr: false}) 12 | 13 | export function SanityLiveStreamLazyClientComponent(props: SanityLiveStreamProps): React.ReactNode { 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/fail/server-only-live/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /fixtures/pass/server-only-live/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /packages/next-sanity/src/live/utils.ts: -------------------------------------------------------------------------------- 1 | import {validateApiPerspective, type ClientPerspective} from '@sanity/client' 2 | 3 | /** @internal */ 4 | export function sanitizePerspective( 5 | _perspective: unknown, 6 | fallback: 'drafts' | 'published', 7 | ): Exclude { 8 | const perspective = 9 | typeof _perspective === 'string' && _perspective.includes(',') 10 | ? _perspective.split(',') 11 | : _perspective 12 | try { 13 | validateApiPerspective(perspective) 14 | return perspective === 'raw' ? fallback : perspective 15 | } catch (err) { 16 | console.warn(`Invalid perspective:`, _perspective, perspective, err) 17 | return fallback 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/next-sanity/MIGRATE-v11-to-v12.md: -------------------------------------------------------------------------------- 1 | ## Migrate 2 | 3 | ### Minimum required `sanity` is now `5.0.0` 4 | 5 | Upgrade to the latest v5 stable using the following command: 6 | 7 | ```bash 8 | npm install sanity@latest --save-exact 9 | ``` 10 | 11 | [Read the changelog for v5](https://www.sanity.io/docs/changelog/fd3ab62e-9264-4e7b-825a-fd4f99abd481) 12 | 13 | ### Minimum required `react` and `react-dom` is now `19.2.3` 14 | 15 | [If you're still on React 18, you need to upgrade to React 19.2.3 or later.](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) 16 | 17 | ### Minimum required `next` is now `16` 18 | 19 | v15 is no longer supported. [You need to upgrade to v16.](https://nextjs.org/docs/app/guides/upgrading/version-16) 20 | -------------------------------------------------------------------------------- /packages/next-sanity/src/studio/NextStudioWithBridge.tsx: -------------------------------------------------------------------------------- 1 | import {NextStudio, type NextStudioProps} from 'next-sanity/studio/client-component' 2 | import {preloadModule} from 'react-dom' 3 | 4 | /** 5 | * Loads the bridge script the same way Sanity Studio does: 6 | * https://github.com/sanity-io/sanity/blob/bd5b1acb5015baaddd8d96c2abd1eaf579b3c904/packages/sanity/src/_internal/cli/server/renderDocument.tsx#L124-L139 7 | */ 8 | 9 | const bridgeScript = 'https://core.sanity-cdn.com/bridge.js' 10 | 11 | export function NextStudioWithBridge(props: NextStudioProps): React.JSX.Element { 12 | preloadModule(bridgeScript, {as: 'script'}) 13 | 14 | return ( 15 | <> 16 |