next-sanity 5.5.0 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +469 -183
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,89 @@
|
|
|
1
1
|
# next-sanity<!-- omit in toc -->
|
|
2
2
|
|
|
3
|
-
[Sanity.io]
|
|
3
|
+
The official [Sanity.io][sanity] toolkit for Next.js apps.
|
|
4
4
|
|
|
5
5
|
**Features:**
|
|
6
6
|
|
|
7
|
-
- [Client-
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
7
|
+
- The [Sanity Client][sanity-client] fully compatible with [Next.js’ caching features][next-cache]
|
|
8
|
+
- [Live Preview mode][preview-kit]
|
|
9
|
+
- [Visual Editing](#visual-editing-with-content-source-maps)
|
|
10
|
+
- [GROQ syntax highlighting][groq-syntax-highlighting]
|
|
11
|
+
- Embedded Sanity Studio
|
|
10
12
|
|
|
11
13
|
## Table of contents
|
|
12
14
|
|
|
15
|
+
- [Table of contents](#table-of-contents)
|
|
13
16
|
- [Installation](#installation)
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
17
|
+
- [Common dependencies](#common-dependencies)
|
|
18
|
+
- [Peer dependencies for embedded Sanity Sudio](#peer-dependencies-for-embedded-sanity-sudio)
|
|
19
|
+
- [Usage](#usage)
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [App Router Components](#app-router-components)
|
|
22
|
+
- [Page Router Components](#page-router-components)
|
|
16
23
|
- [Should `useCdn` be `true` or `false`?](#should-usecdn-be-true-or-false)
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
24
|
+
- [How does `apiVersion` work?](#how-does-apiversionwork)
|
|
25
|
+
- [Cache revalidation](#cache-revalidation)
|
|
26
|
+
- [Time-based revalidation](#time-based-revalidation)
|
|
27
|
+
- [Tag-based revalidation webhook](#tag-based-revalidation-webhook)
|
|
28
|
+
- [Slug-based revalidation for the Pages Router](#slug-based-revalidation-for-the-pages-router)
|
|
29
|
+
- [Working example implementation](#working-example-implementation)
|
|
30
|
+
- [Debugging caching and revalidation](#debugging-caching-and-revalidation)
|
|
31
|
+
- [Preview](#preview)
|
|
32
|
+
- [Using Perspectives](#using-perspectives)
|
|
33
|
+
- [Live Preview](#live-preview)
|
|
34
|
+
- [Using `draftMode()` to de/activate previews](#using-draftmode-to-deactivate-previews)
|
|
35
|
+
- [Visual Editing with Content Source Maps](#visual-editing-with-content-source-maps)
|
|
36
|
+
- [Embedded Sanity Studio](#embedded-sanity-studio)
|
|
37
|
+
- [Configuring Sanity Studio on a route](#configuring-sanity-studio-on-a-route)
|
|
38
|
+
- [Manual installation](#manual-installation)
|
|
39
|
+
- [Studio route with App Router](#studio-route-with-app-router)
|
|
40
|
+
- [Studio Routes with Pages Router](#studio-routes-with-pages-router)
|
|
41
|
+
- [Lower level control with `StudioProvider` and `StudioLayout`](#lower-level-control-with-studioprovider-and-studiolayout)
|
|
22
42
|
- [Migration guides](#migration-guides)
|
|
23
43
|
- [Release new version](#release-new-version)
|
|
24
44
|
- [License](#license)
|
|
25
45
|
|
|
26
46
|
## Installation
|
|
27
47
|
|
|
48
|
+
For basic functionality, run the following command in the package manager of your choice:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install next-sanity
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
yarn add next-sanity
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pnpm install next-sanity
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun install next-sanity
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Common dependencies
|
|
67
|
+
|
|
68
|
+
Building with Sanity and Next.js, you‘re likely to want libraries to handle [On-Demand Image Transformations][image-url] and block content with [Portable Text][portable-text]:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install @portabletext/react @sanity/image-url
|
|
72
|
+
```
|
|
73
|
+
|
|
28
74
|
```bash
|
|
29
|
-
|
|
75
|
+
yarn add @portabletext/react @sanity/image-url
|
|
30
76
|
```
|
|
31
77
|
|
|
32
78
|
```bash
|
|
33
|
-
|
|
79
|
+
pnpm install @portabletext/react @sanity/image-url
|
|
34
80
|
```
|
|
35
81
|
|
|
36
82
|
```bash
|
|
37
|
-
|
|
83
|
+
bun install @portabletext/react @sanity/image-url
|
|
38
84
|
```
|
|
39
85
|
|
|
40
|
-
###
|
|
86
|
+
### Peer dependencies for embedded Sanity Studio
|
|
41
87
|
|
|
42
88
|
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`:
|
|
43
89
|
|
|
@@ -45,14 +91,21 @@ When using `npm` newer than `v7`, or `pnpm` newer than `v8`, you should end up w
|
|
|
45
91
|
npx install-peerdeps --yarn next-sanity
|
|
46
92
|
```
|
|
47
93
|
|
|
48
|
-
##
|
|
94
|
+
## Usage
|
|
95
|
+
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
### Quick start
|
|
99
|
+
|
|
100
|
+
To start running GROQ queries with `next-sanity`, we recommend creating a `client.ts` file:
|
|
49
101
|
|
|
50
102
|
```ts
|
|
51
|
-
|
|
103
|
+
// ./src/utils/sanity/client.ts
|
|
104
|
+
import {createClient} from 'next-sanity'
|
|
52
105
|
|
|
53
106
|
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
54
107
|
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
55
|
-
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION
|
|
108
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
56
109
|
|
|
57
110
|
const client = createClient({
|
|
58
111
|
projectId,
|
|
@@ -60,47 +113,356 @@ const client = createClient({
|
|
|
60
113
|
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
61
114
|
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
|
|
62
115
|
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### App Router Components
|
|
119
|
+
|
|
120
|
+
To fetch data in a React Server Component using the [App Router][app-router]:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// ./src/components/PostIndex.tsx
|
|
124
|
+
import {client} from '@/src/utils/sanity/client'
|
|
125
|
+
|
|
126
|
+
type Post = {
|
|
127
|
+
_id: string
|
|
128
|
+
title?: string
|
|
129
|
+
slug?: {
|
|
130
|
+
current: string
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function PostIndex() {
|
|
135
|
+
const posts = await client.fetch<Post[]>(`*[_type == "post"]`)
|
|
63
136
|
|
|
64
|
-
|
|
137
|
+
return (
|
|
138
|
+
<ul>
|
|
139
|
+
{posts.map((post) => (
|
|
140
|
+
<li key={post._id}>
|
|
141
|
+
<a href={post?.slug.current}>{post?.title}</a>
|
|
142
|
+
</li>
|
|
143
|
+
))}
|
|
144
|
+
</ul>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
65
147
|
```
|
|
66
148
|
|
|
67
|
-
###
|
|
149
|
+
### Page Router Components
|
|
150
|
+
|
|
151
|
+
If you're using the [Pages Router][pages-router], then you can do the following from a page component:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
// ./src/pages/Index.tsx
|
|
155
|
+
import {client} from '@/src/utils/sanity/client'
|
|
156
|
+
|
|
157
|
+
type Post = {
|
|
158
|
+
_id: string
|
|
159
|
+
title?: string
|
|
160
|
+
slug?: {
|
|
161
|
+
current: string
|
|
162
|
+
}
|
|
163
|
+
}
|
|
68
164
|
|
|
69
|
-
|
|
165
|
+
export async function getStaticProps() {
|
|
166
|
+
return await client.fetch<Post[]>(`*[_type == "post"]`)
|
|
167
|
+
}
|
|
70
168
|
|
|
71
|
-
|
|
72
|
-
|
|
169
|
+
export async function HomePage(props) {
|
|
170
|
+
const {posts} = props
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<ul>
|
|
174
|
+
{posts.map((post) => (
|
|
175
|
+
<li key={post._id}>
|
|
176
|
+
<a href={post?.slug.current}>{post?.title}</a>
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
</ul>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
```
|
|
73
183
|
|
|
74
184
|
### Should `useCdn` be `true` or `false`?
|
|
75
185
|
|
|
186
|
+
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][cdn]. Since Next.js often comes with its own caching, it might not be necessary, but there are some exceptions.
|
|
187
|
+
|
|
76
188
|
The general rule is that `useCdn` should be `true` when:
|
|
77
189
|
|
|
78
|
-
- Data fetching happens client-side,
|
|
79
|
-
- SSR data fetching is dynamic and
|
|
190
|
+
- 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.
|
|
191
|
+
- Server-Side Rendered (SSR) data fetching is dynamic and has a high number of unique requests per visitor, for example, a "For You" feed.
|
|
80
192
|
|
|
81
193
|
And it makes sense to set `useCdn` to `false` when:
|
|
82
194
|
|
|
83
|
-
- Used in a static site generation context,
|
|
84
|
-
- Used in
|
|
195
|
+
- Used in a static site generation context, for example, `getStaticProps` or `getStaticPaths`.
|
|
196
|
+
- Used in an ISR on-demand webhook responder.
|
|
85
197
|
- Good `stale-while-revalidate` caching is in place that keeps API requests on a consistent low, even if traffic to Next.js spikes.
|
|
86
|
-
-
|
|
198
|
+
- For Preview or Draft modes as part of an editorial workflow, you need to ensure that the latest content is always fetched.
|
|
199
|
+
|
|
200
|
+
### How does `apiVersion` work?
|
|
87
201
|
|
|
88
|
-
|
|
202
|
+
Sanity uses [date-based API versioning][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).
|
|
203
|
+
|
|
204
|
+
## Cache revalidation
|
|
205
|
+
|
|
206
|
+
This toolkit includes the [`@sanity/client`][sanity-client] that fully supports Next.js’ `fetch` based features, [including the `revalidateTag` API][revalidate-tag]. 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.
|
|
89
207
|
|
|
90
208
|
> **Note**
|
|
91
209
|
>
|
|
92
|
-
>
|
|
210
|
+
> 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][next-cache].
|
|
211
|
+
|
|
212
|
+
### Time-based revalidation
|
|
213
|
+
|
|
214
|
+
Time-based revalidation is best for less complex cases and where content updates don't need to be immediately available.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// ./src/utils/sanity/client.ts
|
|
218
|
+
import {createClient} from 'next-sanity'
|
|
219
|
+
|
|
220
|
+
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
221
|
+
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
222
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
223
|
+
|
|
224
|
+
const client = createClient({
|
|
225
|
+
projectId,
|
|
226
|
+
dataset,
|
|
227
|
+
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
228
|
+
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
|
|
229
|
+
next: {
|
|
230
|
+
revalidate: 3600, // look for updates to revalidate cache every hour
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Tag-based revalidation webhook
|
|
236
|
+
|
|
237
|
+
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.
|
|
238
|
+
|
|
239
|
+
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).
|
|
240
|
+
|
|
241
|
+
If you're planning to use `revalidateTag`, then remember to set up the webhook (see code below) as well.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// ./src/utils/sanity/client.ts
|
|
245
|
+
import 'server-only'
|
|
246
|
+
|
|
247
|
+
import type {QueryParams} from '@sanity/client'
|
|
248
|
+
import {createClient} from 'next-sanity'
|
|
249
|
+
|
|
250
|
+
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
251
|
+
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
252
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
253
|
+
|
|
254
|
+
const client = createClient({
|
|
255
|
+
projectId,
|
|
256
|
+
dataset,
|
|
257
|
+
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
258
|
+
useCdn: false,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const DEFAULT_PARAMS = {} as QueryParams
|
|
262
|
+
const DEFAULT_TAGS = [] as string[]
|
|
263
|
+
|
|
264
|
+
export async function sanityFetch<QueryResponse>({
|
|
265
|
+
query,
|
|
266
|
+
params = DEFAULT_PARAMS,
|
|
267
|
+
tags = DEFAULT_TAGS,
|
|
268
|
+
}: {
|
|
269
|
+
query: string
|
|
270
|
+
params?: QueryParams
|
|
271
|
+
tags: string[]
|
|
272
|
+
}): Promise<QueryResponse> {
|
|
273
|
+
return client.fetch<QueryResponse>(query, params, {
|
|
274
|
+
cache: 'force-cache',
|
|
275
|
+
next: {
|
|
276
|
+
//revalidate: 30, // for simple, time-based revalidation
|
|
277
|
+
tags, // for tag-based revalidation
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
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:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
// ./src/app/home/layout.tsx
|
|
287
|
+
import { sanityFetch } from '@/src/utils/sanity/client'
|
|
288
|
+
import { PageProps } from '@/src/app/(page)/Page.tsx'
|
|
289
|
+
|
|
290
|
+
type HomePageProps = {
|
|
291
|
+
_id: string
|
|
292
|
+
title?: string
|
|
293
|
+
navItems: PageProps[]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function HomeLayout({children}) {
|
|
297
|
+
// revalidate if there are changes to either the home document or to a page document (since they're referenced to in navItems)
|
|
298
|
+
const home = await sanityFetch<HomePageProps>({
|
|
299
|
+
query: `*[_id == "home"][0]{...,navItems[]->}`,
|
|
300
|
+
tags: ['home', 'page']
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<main>
|
|
305
|
+
<nav>
|
|
306
|
+
<span>{home?.title}</span>
|
|
307
|
+
<ul>
|
|
308
|
+
{home?.navItems.map(navItem => ({
|
|
309
|
+
<li key={navItem._id}><a href={navItem?.slug?.current}>{navItem?.title}</a></li>
|
|
310
|
+
}))}
|
|
311
|
+
</ul>
|
|
312
|
+
</nav>
|
|
313
|
+
{children}
|
|
314
|
+
</main>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
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][groq-webhook].
|
|
320
|
+
|
|
321
|
+
You can use this [template][webhook-template] to quickly configure the webhook for your Sanity project.
|
|
322
|
+
|
|
323
|
+
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:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
// ./src/app/api/revalidate.ts
|
|
327
|
+
import {revalidateTag} from 'next/cache'
|
|
328
|
+
import {type NextRequest, NextResponse} from 'next/server'
|
|
329
|
+
import {parseBody} from 'next-sanity/webhook'
|
|
330
|
+
|
|
331
|
+
export async function POST(req: NextRequest) {
|
|
332
|
+
try {
|
|
333
|
+
const {isValidSignature, body} = await parseBody<{_type}>(
|
|
334
|
+
req,
|
|
335
|
+
process.env.SANITY_REVALIDATE_SECRET,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if (!isValidSignature) {
|
|
339
|
+
const message = 'Invalid signature'
|
|
340
|
+
return new Response(JSON.stringify({message, isValidSignature, body}), {status: 401})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!body?._type) {
|
|
344
|
+
const message = 'Bad Request'
|
|
345
|
+
return new Response({message, body}, {status: 400})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If the `_type` is `page`, then all `client.fetch` calls with
|
|
349
|
+
// `{next: {tags: ['page']}}` will be revalidated
|
|
350
|
+
await revalidateTag(body._type)
|
|
351
|
+
|
|
352
|
+
return NextResponse.json({body})
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error(err)
|
|
355
|
+
return new Response(err.message, {status: 500})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
You can choose to match tags based on any field or expression since GROQ-Powered Webhooks allow you to freely define the payload.
|
|
361
|
+
|
|
362
|
+
### Slug-based revalidation for the Pages Router
|
|
363
|
+
|
|
364
|
+
If you are using the Pages Router and want on-demand revalidation, you'll have to do this by targeting the URLs/slugs for the pages you want to revalidate. If you have nested routes, you will need to adopt the logic to accommodate for that. For example, using `_type` to determine the first segment: `/${body?._type}/${body?.slug.current}`.
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
// ./pages/api/revalidate.ts
|
|
368
|
+
import type {NextApiRequest, NextApiResponse} from 'next'
|
|
369
|
+
import {parseBody} from 'next-sanity/webhook'
|
|
370
|
+
|
|
371
|
+
// Export the config from next-sanity to enable validating the request body signature properly
|
|
372
|
+
export {config} from 'next-sanity/webhook'
|
|
373
|
+
|
|
374
|
+
export default async function revalidate(req: NextApiRequest, res: NextApiResponse) {
|
|
375
|
+
try {
|
|
376
|
+
const {isValidSignature, body} = await parseBody(req, process.env.SANITY_REVALIDATE_SECRET)
|
|
377
|
+
|
|
378
|
+
if (!isValidSignature) {
|
|
379
|
+
const message = 'Invalid signature'
|
|
380
|
+
return res.status(401).json({message, isValidSignature, body})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const staleRoute = `/${body.slug.current}`
|
|
384
|
+
await res.revalidate(staleRoute)
|
|
385
|
+
const message = `Updated route: ${staleRoute}`
|
|
386
|
+
return res.status(200).json({message, body})
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error(err)
|
|
389
|
+
return res.status(500).json({message: err.message})
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Working example implementation
|
|
395
|
+
|
|
396
|
+
Check out our [Personal website template][personal-website-template] to see a feature-complete example of how `revalidateTag` is used together with Live Previews.
|
|
397
|
+
|
|
398
|
+
### Debugging caching and revalidation
|
|
399
|
+
|
|
400
|
+
To aid in debugging and understanding what's in the cache, revalidated, skipped, and more, add the following to your Next.js configuration file:
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
// ./next.config.js
|
|
404
|
+
module.exports = {
|
|
405
|
+
experimental: {
|
|
406
|
+
logging: 'verbose',
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Preview
|
|
412
|
+
|
|
413
|
+
There are different ways to set up content previews with Sanity and Next.js.
|
|
414
|
+
|
|
415
|
+
### Using Perspectives
|
|
416
|
+
|
|
417
|
+
[Perspectives][perspectives-docs] 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.
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
// ./src/utils/sanity/client.ts
|
|
421
|
+
import {createClient} from 'next-sanity'
|
|
422
|
+
|
|
423
|
+
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
424
|
+
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
425
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
426
|
+
const token = process.env.SECRET_SANITY_VIEW_TOKEN
|
|
427
|
+
|
|
428
|
+
const client = createClient({
|
|
429
|
+
projectId,
|
|
430
|
+
dataset,
|
|
431
|
+
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
432
|
+
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
|
|
433
|
+
token,
|
|
434
|
+
perspective: 'published', // prevent drafts from leaking through even though requests are authenticated
|
|
435
|
+
})
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Live Preview
|
|
439
|
+
|
|
440
|
+
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.
|
|
441
|
+
|
|
442
|
+
Router-specific setup guides for Live Preview:
|
|
443
|
+
|
|
444
|
+
- [`app-router`][preivew-app-router]
|
|
445
|
+
- [`pages-router`][preview-pages-router]
|
|
446
|
+
|
|
447
|
+
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][preview-kit-documentation].
|
|
448
|
+
The [same is true][preview-kit-livequery] for `next-sanity/preview/live-query`.
|
|
449
|
+
|
|
450
|
+
### Using `draftMode()` to de/activate previews
|
|
451
|
+
|
|
452
|
+
Next.js gives you [a built-in `draftMode` variable][draft-mode] that can be used to activate features like Visual Edit or any preview implementation.
|
|
93
453
|
|
|
94
454
|
```ts
|
|
455
|
+
// ./src/utils/sanity/client.ts
|
|
95
456
|
import 'server-only'
|
|
96
457
|
|
|
458
|
+
import {draftMode} from 'next/headers'
|
|
97
459
|
import type {QueryParams} from '@sanity/client'
|
|
98
460
|
import {createClient, groq} from 'next-sanity'
|
|
99
461
|
import {draftMode} from 'next/headers'
|
|
100
462
|
|
|
101
463
|
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
102
464
|
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
103
|
-
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION
|
|
465
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
104
466
|
|
|
105
467
|
const client = createClient({
|
|
106
468
|
projectId,
|
|
@@ -142,37 +504,9 @@ export async function sanityFetch<QueryResponse>({
|
|
|
142
504
|
},
|
|
143
505
|
})
|
|
144
506
|
}
|
|
145
|
-
|
|
146
|
-
// Inside a Server Component data is easily fetched and tagged
|
|
147
|
-
async function HomePageLayout() {
|
|
148
|
-
const data = await sanityFetch<HomePageData>({
|
|
149
|
-
query: groq`*[_type == "home"][0]`,
|
|
150
|
-
// Now calling `revalidateTag('home')` will revalidate this query, and could be done with a simple GROQ webhook
|
|
151
|
-
tags: ['home'],
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<div>
|
|
156
|
-
<h1>{data.title}</h1>
|
|
157
|
-
<PortableText blocks={data.body} />
|
|
158
|
-
</div>
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
[Checkout our Personal website template to see a feature complete example of how `revalidateTag` is used together with Live Previews.](https://github.com/sanity-io/sanity-template-nextjs-app-router-personal-website)
|
|
164
|
-
|
|
165
|
-
To aid in debugging and understanding what's in the cache, revalidated, skipped and more, add this to your `next.config.js`:
|
|
166
|
-
|
|
167
|
-
```js
|
|
168
|
-
module.exports = {
|
|
169
|
-
experimental: {
|
|
170
|
-
logging: 'verbose',
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
507
|
```
|
|
174
508
|
|
|
175
|
-
##
|
|
509
|
+
## Visual Editing with Content Source Maps
|
|
176
510
|
|
|
177
511
|
> **Note**
|
|
178
512
|
>
|
|
@@ -191,68 +525,69 @@ const client = createClient({
|
|
|
191
525
|
projectId,
|
|
192
526
|
dataset,
|
|
193
527
|
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
194
|
-
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
|
|
528
|
+
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
|
|
195
529
|
studioUrl: '/studio', // Or: 'https://my-cool-project.sanity.studio'
|
|
196
530
|
encodeSourceMap: true, // Optional. Default to: process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview',
|
|
197
531
|
})
|
|
198
532
|
```
|
|
199
533
|
|
|
200
|
-
[
|
|
534
|
+
Go to our [setup guide][visual-editing] for a walkthrough on how to customize the experience.
|
|
201
535
|
|
|
202
|
-
##
|
|
536
|
+
## Embedded Sanity Studio
|
|
203
537
|
|
|
204
|
-
|
|
538
|
+
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.
|
|
205
539
|
|
|
206
|
-
|
|
207
|
-
- [`pages-router`](./PREVIEW-pages-router.md)
|
|
540
|
+
This opens up many possibilities:
|
|
208
541
|
|
|
209
|
-
|
|
210
|
-
|
|
542
|
+
- Any service that hosts Next.js apps can now host your Studio.
|
|
543
|
+
- Building previews for your content is easier as your Studio lives in the same environment.
|
|
544
|
+
- Use [Data Fetching][next-data-fetching] to configure your Studio.
|
|
545
|
+
- Easy setup of [Preview Mode][next-preview-mode].
|
|
211
546
|
|
|
212
|
-
|
|
547
|
+
> [See it live][embedded-studio-demo]
|
|
213
548
|
|
|
214
|
-
|
|
549
|
+
### Configuring Sanity Studio on a route
|
|
215
550
|
|
|
216
|
-
The
|
|
551
|
+
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.
|
|
217
552
|
|
|
218
|
-
|
|
219
|
-
- Building previews for your content is easier as your Studio lives in the same environment.
|
|
220
|
-
- Use [Data Fetching](https://nextjs.org/docs/basic-features/data-fetching/overview) to configure your Studio.
|
|
221
|
-
- Easy setup of [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode).
|
|
553
|
+
To quickly scaffold the embedded studio and a Sanity project, you can run the following command in your project folder:
|
|
222
554
|
|
|
223
|
-
|
|
555
|
+
```bash
|
|
556
|
+
npx sanity@latest init
|
|
557
|
+
```
|
|
224
558
|
|
|
225
|
-
|
|
559
|
+
### Manual installation
|
|
226
560
|
|
|
227
|
-
Both the Next `/app` and `/pages` examples
|
|
228
|
-
`sanity.config.ts`:
|
|
561
|
+
Make a file called `sanity.config.ts` (or `.js` for non-TypeScript projects) in the project's root (same place as `next.config.ts`) and copy the example below. Both the Next `/app` and `/pages` examples use this config file:
|
|
229
562
|
|
|
230
563
|
```ts
|
|
564
|
+
// ./sanity.config.ts
|
|
231
565
|
import {defineConfig} from 'sanity'
|
|
232
566
|
import {deskTool} from 'sanity/desk'
|
|
233
567
|
|
|
234
|
-
import {schemaTypes} from './
|
|
568
|
+
import {schemaTypes} from './src/schema'
|
|
235
569
|
|
|
236
570
|
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
|
|
237
571
|
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
|
|
238
572
|
|
|
239
573
|
export default defineConfig({
|
|
240
|
-
basePath: '/
|
|
574
|
+
basePath: '/admin', // <-- important that `basePath` matches the route you're mounting your studio from, it applies to both `/pages` and `/app`
|
|
241
575
|
|
|
242
576
|
projectId,
|
|
243
577
|
dataset,
|
|
244
|
-
|
|
245
578
|
plugins: [deskTool()],
|
|
246
|
-
|
|
247
579
|
schema: {
|
|
248
580
|
types: schemaTypes,
|
|
249
581
|
},
|
|
250
582
|
})
|
|
251
583
|
```
|
|
252
584
|
|
|
253
|
-
|
|
585
|
+
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.
|
|
586
|
+
|
|
587
|
+
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:
|
|
254
588
|
|
|
255
589
|
```ts
|
|
590
|
+
// ./sanity.cli.ts
|
|
256
591
|
/* eslint-disable no-process-env */
|
|
257
592
|
import {loadEnvConfig} from '@next/env'
|
|
258
593
|
import {defineCliConfig} from 'sanity/cli'
|
|
@@ -266,13 +601,12 @@ const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
|
|
|
266
601
|
export default defineCliConfig({api: {projectId, dataset}})
|
|
267
602
|
```
|
|
268
603
|
|
|
269
|
-
Now you can run commands like `npx sanity cors add`.
|
|
270
|
-
|
|
271
|
-
#### Using `app-router`
|
|
604
|
+
Now you can run commands like `npx sanity cors add`. Run `npx sanity help` for a full list of what you can do.
|
|
272
605
|
|
|
273
|
-
|
|
606
|
+
### Studio route with App Router
|
|
274
607
|
|
|
275
608
|
```tsx
|
|
609
|
+
// ./src/app/studio/[[...index]]/page.tsx
|
|
276
610
|
import {Studio} from './Studio'
|
|
277
611
|
|
|
278
612
|
// Ensures the Studio route is statically generated
|
|
@@ -286,9 +620,8 @@ export default function StudioPage() {
|
|
|
286
620
|
}
|
|
287
621
|
```
|
|
288
622
|
|
|
289
|
-
`app/studio/[[...index]]/Studio.tsx`:
|
|
290
|
-
|
|
291
623
|
```tsx
|
|
624
|
+
// ./src/app/studio/[[...index]]/Studio.tsx
|
|
292
625
|
'use client'
|
|
293
626
|
|
|
294
627
|
import {NextStudio} from 'next-sanity/studio'
|
|
@@ -301,10 +634,10 @@ export function Studio() {
|
|
|
301
634
|
}
|
|
302
635
|
```
|
|
303
636
|
|
|
304
|
-
|
|
305
|
-
`app/studio/[[...index]]/page.tsx`:
|
|
637
|
+
How to customize meta tags:
|
|
306
638
|
|
|
307
639
|
```tsx
|
|
640
|
+
// ./src/app/studio/[[...index]]/page.tsx
|
|
308
641
|
import type {Metadata} from 'next'
|
|
309
642
|
import {metadata as studioMetadata} from 'next-sanity/studio/metadata'
|
|
310
643
|
|
|
@@ -322,11 +655,10 @@ export default function StudioPage() {
|
|
|
322
655
|
}
|
|
323
656
|
```
|
|
324
657
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
`/pages/studio/[[...index]].tsx`:
|
|
658
|
+
### Studio Routes with Pages Router
|
|
328
659
|
|
|
329
660
|
```tsx
|
|
661
|
+
// ./pages/studio/[[...index]].tsx
|
|
330
662
|
import Head from 'next/head'
|
|
331
663
|
import {NextStudio} from 'next-sanity/studio'
|
|
332
664
|
import {metadata} from 'next-sanity/studio/metadata'
|
|
@@ -347,9 +679,9 @@ export default function StudioPage() {
|
|
|
347
679
|
}
|
|
348
680
|
```
|
|
349
681
|
|
|
350
|
-
###
|
|
682
|
+
### Lower level control with `StudioProvider` and `StudioLayout`
|
|
351
683
|
|
|
352
|
-
If you want to go lower level and have more control over the
|
|
684
|
+
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`:
|
|
353
685
|
|
|
354
686
|
```tsx
|
|
355
687
|
import {NextStudio} from 'next-sanity/studio'
|
|
@@ -369,96 +701,50 @@ function StudioPage() {
|
|
|
369
701
|
}
|
|
370
702
|
```
|
|
371
703
|
|
|
372
|
-
## `next-sanity/webhook`
|
|
373
|
-
|
|
374
|
-
Implements [`@sanity/webhook`](https://github.com/sanity-io/webhook-toolkit) to parse and verify that a [Webhook](https://www.sanity.io/docs/webhooks) is indeed coming from Sanity infrastructure.
|
|
375
|
-
|
|
376
|
-
### App Router
|
|
377
|
-
|
|
378
|
-
`app/api/revalidate/route.ts`:
|
|
379
|
-
|
|
380
|
-
```ts
|
|
381
|
-
import {revalidateTag} from 'next/cache'
|
|
382
|
-
import {type NextRequest, NextResponse} from 'next/server'
|
|
383
|
-
import {parseBody} from 'next-sanity/webhook'
|
|
384
|
-
|
|
385
|
-
export async function POST(req: NextRequest) {
|
|
386
|
-
try {
|
|
387
|
-
const {isValidSignature, body} = await parseBody<{_type}>(
|
|
388
|
-
req,
|
|
389
|
-
process.env.SANITY_REVALIDATE_SECRET,
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
if (!isValidSignature) {
|
|
393
|
-
const message = 'Invalid signature'
|
|
394
|
-
return new Response(JSON.stringify({message, isValidSignature, body}), {status: 401})
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (!body?._type) {
|
|
398
|
-
const message = 'Bad Request'
|
|
399
|
-
return new Response({message, body}, {status: 400})
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// If the `_type` is `page`, then all `client.fetch` calls with
|
|
403
|
-
// `{next: {tags: ['page']}}` will be revalidated
|
|
404
|
-
await revalidateTag(body._type)
|
|
405
|
-
|
|
406
|
-
return NextResponse.json({body})
|
|
407
|
-
} catch (err) {
|
|
408
|
-
console.error(err)
|
|
409
|
-
return new Response(err.message, {status: 500})
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### Pages Router
|
|
415
|
-
|
|
416
|
-
`pages/api/revalidate.ts`:
|
|
417
|
-
|
|
418
|
-
```ts
|
|
419
|
-
import type {NextApiRequest, NextApiResponse} from 'next'
|
|
420
|
-
import {parseBody} from 'next-sanity/webhook'
|
|
421
|
-
|
|
422
|
-
// Export the config from next-sanity to enable validating the request body signature properly
|
|
423
|
-
export {config} from 'next-sanity/webhook'
|
|
424
|
-
|
|
425
|
-
export default async function revalidate(req: NextApiRequest, res: NextApiResponse) {
|
|
426
|
-
try {
|
|
427
|
-
const {isValidSignature, body} = await parseBody(req, process.env.SANITY_REVALIDATE_SECRET)
|
|
428
|
-
|
|
429
|
-
if (!isValidSignature) {
|
|
430
|
-
const message = 'Invalid signature'
|
|
431
|
-
return res.status(401).json({message, isValidSignature, body})
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const staleRoute = `/${body.slug.current}`
|
|
435
|
-
await res.revalidate(staleRoute)
|
|
436
|
-
const message = `Updated route: ${staleRoute}`
|
|
437
|
-
return res.status(200).json({message, body})
|
|
438
|
-
} catch (err) {
|
|
439
|
-
console.error(err)
|
|
440
|
-
return res.status(500).json({message: err.message})
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
```
|
|
444
|
-
|
|
445
704
|
## Migration guides
|
|
446
705
|
|
|
447
706
|
- From `v4` to `v5`
|
|
448
|
-
- [`app-router`]
|
|
449
|
-
- [`pages-router`]
|
|
450
|
-
- [From `<0.4` to `v4`]
|
|
707
|
+
- [`app-router`][migrate-v4-to-v5-app]
|
|
708
|
+
- [`pages-router`][migrate-v4-to-v5-pages]
|
|
709
|
+
- [From `<0.4` to `v4`][migrate-v1-to-v4]
|
|
451
710
|
|
|
452
711
|
## License
|
|
453
712
|
|
|
454
|
-
MIT-licensed. See [LICENSE]
|
|
455
|
-
|
|
456
|
-
[
|
|
457
|
-
[
|
|
458
|
-
[
|
|
459
|
-
[
|
|
713
|
+
MIT-licensed. See [LICENSE][LICENSE].
|
|
714
|
+
|
|
715
|
+
[api-versioning]: https://www.sanity.io/docs/api-versioning?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
716
|
+
[app-router]: https://nextjs.org/docs/app/building-your-application/routing
|
|
717
|
+
[cdn]: https://www.sanity.io/docs/asset-cdn?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
718
|
+
[ci-workflow]: https://github.com/sanity-io/next-sanity/actions/workflows/ci.yml
|
|
719
|
+
[content-source-maps-intro]: https://www.sanity.io/blog/content-source-maps-announce?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
720
|
+
[content-source-maps]: https://www.sanity.io/docs/content-source-maps?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
721
|
+
[draft-mode]: https://nextjs.org/docs/app/building-your-application/configuring/draft-mode
|
|
722
|
+
[embedded-studio-demo]: https://next.sanity.build/studio
|
|
723
|
+
[enterprise-cta]: https://www.sanity.io/enterprise?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
724
|
+
[groq-syntax-highlighting]: https://marketplace.visualstudio.com/items?itemName=sanity-io.vscode-sanity
|
|
725
|
+
[groq-webhook]: https://www.sanity.io/docs/webhooks?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
726
|
+
[image-url]: https://www.sanity.io/docs/presenting-images?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
727
|
+
[LICENSE]: LICENSE
|
|
728
|
+
[migrate-v1-to-v4]: ./MIGRATE-v1-to-v4.md
|
|
729
|
+
[migrate-v4-to-v5-app]: ./MIGRATE-v4-to-v5-pages-app-router.md
|
|
730
|
+
[migrate-v4-to-v5-pages]: ./MIGRATE-v4-to-v5-pages-pages-router.md
|
|
731
|
+
[next-cache]: https://nextjs.org/docs/app/building-your-application/caching
|
|
732
|
+
[next-data-fetching]: https://nextjs.org/docs/basic-features/data-fetching/overview
|
|
733
|
+
[next-preview-mode]: https://nextjs.org/docs/advanced-features/preview-mode
|
|
734
|
+
[pages-router]: https://nextjs.org/docs/pages/building-your-application/routing
|
|
735
|
+
[personal-website-template]: https://github.com/sanity-io/sanity-template-nextjs-app-router-personal-website
|
|
736
|
+
[perspectives-docs]: https://www.sanity.io/docs/perspectives?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
737
|
+
[portable-text]: https://portabletext.org
|
|
738
|
+
[preivew-app-router]: ./PREVIEW-app-router.md
|
|
460
739
|
[preview-kit-client]: https://github.com/sanity-io/preview-kit#sanitypreview-kitclient
|
|
461
|
-
[
|
|
462
|
-
[
|
|
463
|
-
[
|
|
464
|
-
[
|
|
740
|
+
[preview-kit-documentation]: https://github.com/sanity-io/preview-kit#sanitypreview-kit-1
|
|
741
|
+
[preview-kit-livequery]: https://github.com/sanity-io/preview-kit#using-the-livequery-wrapper-component-instead-of-the-uselivequery-hook
|
|
742
|
+
[preview-kit]: https://github.com/sanity-io/preview-kit
|
|
743
|
+
[preview-pages-router]: ./PREVIEW-pages-router.md
|
|
744
|
+
[revalidate-tag]: https://nextjs.org/docs/app/api-reference/functions/revalidateTag
|
|
745
|
+
[sales-cta]: https://www.sanity.io/contact/sales?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
746
|
+
[sanity-client]: https://www.sanity.io/docs/js-client?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
747
|
+
[sanity]: https://www.sanity.io?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
748
|
+
[visual-editing-intro]: https://www.sanity.io/blog/visual-editing-sanity-vercel?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
749
|
+
[visual-editing]: https://www.sanity.io/docs/vercel-visual-editing?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
750
|
+
[webhook-template]: https://www.sanity.io/manage/webhooks/share?name=Tag-based+Revalidation+Hook+for+Next.js+13%2B&description=%5B%C2%A0%5D+Replace+URL+with+the+production*+URL+for+your+revalidation+handler+in+your+Next.js+app%0A%5B+%5D%C2%A0Insert%2Freplace+the+document+types+you+want+to+be+able+to+make+tags+for+in+the+Filter+array%0A%5B+%5D%C2%A0Make+a+Secret+that+you+also+add+to+your+app%27s+environment+variables+%28SANITY_REVALIDATE_SECRET%29%0A%0A*Or+preview+URL+for+preliminary+testing%0A%0AFor+complete+instructions%2C+see+the+README+on%3A%0Ahttps%3A%2F%2Fgithub.com%2Fsanity-io%2Fnext-sanity&url=https%3A%2F%2FYOUR-PRODUCTION-URL.TLD%2Fapi%2Frevalidate&on=create&on=update&on=delete&filter=_type+in+%5B%22post%22%2C+%22home%22%2C+%22OTHER_DOCUMENT_TYPE%22%5D&projection=%7B_type%7D&httpMethod=POST&apiVersion=v2021-03-25&includeDrafts=&headers=%7B%7D
|