next-sanity 5.5.0 → 5.5.2
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 +484 -184
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,88 @@
|
|
|
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 Studio](#peer-dependencies-for-embedded-sanity-studio)
|
|
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
|
-
- [Release new version](#release-new-version)
|
|
24
43
|
- [License](#license)
|
|
25
44
|
|
|
26
45
|
## Installation
|
|
27
46
|
|
|
47
|
+
For basic functionality, run the following command in the package manager of your choice:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install next-sanity
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
yarn add next-sanity
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm install next-sanity
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bun install next-sanity
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Common dependencies
|
|
66
|
+
|
|
67
|
+
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]:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install @portabletext/react @sanity/image-url
|
|
71
|
+
```
|
|
72
|
+
|
|
28
73
|
```bash
|
|
29
|
-
|
|
74
|
+
yarn add @portabletext/react @sanity/image-url
|
|
30
75
|
```
|
|
31
76
|
|
|
32
77
|
```bash
|
|
33
|
-
|
|
78
|
+
pnpm install @portabletext/react @sanity/image-url
|
|
34
79
|
```
|
|
35
80
|
|
|
36
81
|
```bash
|
|
37
|
-
|
|
82
|
+
bun install @portabletext/react @sanity/image-url
|
|
38
83
|
```
|
|
39
84
|
|
|
40
|
-
###
|
|
85
|
+
### Peer dependencies for embedded Sanity Studio
|
|
41
86
|
|
|
42
87
|
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
88
|
|
|
@@ -45,14 +90,21 @@ When using `npm` newer than `v7`, or `pnpm` newer than `v8`, you should end up w
|
|
|
45
90
|
npx install-peerdeps --yarn next-sanity
|
|
46
91
|
```
|
|
47
92
|
|
|
48
|
-
##
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
### Quick start
|
|
98
|
+
|
|
99
|
+
To start running GROQ queries with `next-sanity`, we recommend creating a `client.ts` file:
|
|
49
100
|
|
|
50
101
|
```ts
|
|
51
|
-
|
|
102
|
+
// ./src/utils/sanity/client.ts
|
|
103
|
+
import {createClient} from 'next-sanity'
|
|
52
104
|
|
|
53
105
|
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
54
106
|
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
55
|
-
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION
|
|
107
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
56
108
|
|
|
57
109
|
const client = createClient({
|
|
58
110
|
projectId,
|
|
@@ -60,47 +112,371 @@ const client = createClient({
|
|
|
60
112
|
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
61
113
|
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
114
|
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### App Router Components
|
|
118
|
+
|
|
119
|
+
To fetch data in a React Server Component using the [App Router][app-router]:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// ./src/app/page.tsx
|
|
123
|
+
import {client} from '@/src/utils/sanity/client'
|
|
124
|
+
|
|
125
|
+
type Post = {
|
|
126
|
+
_id: string
|
|
127
|
+
title?: string
|
|
128
|
+
slug?: {
|
|
129
|
+
current: string
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function PostIndex() {
|
|
134
|
+
const posts = await client.fetch<Post[]>(`*[_type == "post"]`)
|
|
63
135
|
|
|
64
|
-
|
|
136
|
+
return (
|
|
137
|
+
<ul>
|
|
138
|
+
{posts.map((post) => (
|
|
139
|
+
<li key={post._id}>
|
|
140
|
+
<a href={post?.slug.current}>{post?.title}</a>
|
|
141
|
+
</li>
|
|
142
|
+
))}
|
|
143
|
+
</ul>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
65
146
|
```
|
|
66
147
|
|
|
67
|
-
###
|
|
148
|
+
### Page Router Components
|
|
149
|
+
|
|
150
|
+
If you're using the [Pages Router][pages-router], then you can do the following from a page component:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
// ./src/pages/index.tsx
|
|
154
|
+
import {client} from '@/src/utils/sanity/client'
|
|
155
|
+
|
|
156
|
+
type Post = {
|
|
157
|
+
_id: string
|
|
158
|
+
title?: string
|
|
159
|
+
slug?: {
|
|
160
|
+
current: string
|
|
161
|
+
}
|
|
162
|
+
}
|
|
68
163
|
|
|
69
|
-
|
|
164
|
+
export async function getStaticProps() {
|
|
165
|
+
return await client.fetch<Post[]>(`*[_type == "post"]`)
|
|
166
|
+
}
|
|
70
167
|
|
|
71
|
-
|
|
72
|
-
|
|
168
|
+
export async function HomePage(props) {
|
|
169
|
+
const {posts} = props
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<ul>
|
|
173
|
+
{posts.map((post) => (
|
|
174
|
+
<li key={post._id}>
|
|
175
|
+
<a href={post?.slug.current}>{post?.title}</a>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
178
|
+
</ul>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
```
|
|
73
182
|
|
|
74
183
|
### Should `useCdn` be `true` or `false`?
|
|
75
184
|
|
|
185
|
+
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.
|
|
186
|
+
|
|
76
187
|
The general rule is that `useCdn` should be `true` when:
|
|
77
188
|
|
|
78
|
-
- Data fetching happens client-side,
|
|
79
|
-
- SSR data fetching is dynamic and
|
|
189
|
+
- 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.
|
|
190
|
+
- 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
191
|
|
|
81
192
|
And it makes sense to set `useCdn` to `false` when:
|
|
82
193
|
|
|
83
|
-
- Used in a static site generation context,
|
|
84
|
-
- Used in
|
|
194
|
+
- Used in a static site generation context, for example, `getStaticProps` or `getStaticPaths`.
|
|
195
|
+
- Used in an ISR on-demand webhook responder.
|
|
85
196
|
- Good `stale-while-revalidate` caching is in place that keeps API requests on a consistent low, even if traffic to Next.js spikes.
|
|
86
|
-
-
|
|
197
|
+
- For Preview or Draft modes as part of an editorial workflow, you need to ensure that the latest content is always fetched.
|
|
198
|
+
|
|
199
|
+
### How does `apiVersion` work?
|
|
87
200
|
|
|
88
|
-
|
|
201
|
+
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).
|
|
202
|
+
|
|
203
|
+
## Cache revalidation
|
|
204
|
+
|
|
205
|
+
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
206
|
|
|
90
207
|
> **Note**
|
|
91
208
|
>
|
|
92
|
-
>
|
|
209
|
+
> 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].
|
|
210
|
+
|
|
211
|
+
### Time-based revalidation
|
|
212
|
+
|
|
213
|
+
Time-based revalidation is best for less complex cases and where content updates don't need to be immediately available.
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// ./src/app/home/layout.tsx
|
|
217
|
+
import { client } from '@/src/utils/sanity/client'
|
|
218
|
+
import { PageProps } from '@/src/app/(page)/Page.tsx'
|
|
219
|
+
|
|
220
|
+
type HomePageProps = {
|
|
221
|
+
_id: string
|
|
222
|
+
title?: string
|
|
223
|
+
navItems: PageProps[]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function HomeLayout({children}) {
|
|
227
|
+
const home = await client.fetch<HomePageProps>(`*[_id == "home"][0]{...,navItems[]->}`,
|
|
228
|
+
next: {
|
|
229
|
+
revalidate: 3600 // look for updates to revalidate cache every hour
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<main>
|
|
235
|
+
<nav>
|
|
236
|
+
<span>{home?.title}</span>
|
|
237
|
+
<ul>
|
|
238
|
+
{home?.navItems.map(navItem => ({
|
|
239
|
+
<li key={navItem._id}><a href={navItem?.slug?.current}>{navItem?.title}</a></li>
|
|
240
|
+
}))}
|
|
241
|
+
</ul>
|
|
242
|
+
</nav>
|
|
243
|
+
{children}
|
|
244
|
+
</main>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Tag-based revalidation webhook
|
|
250
|
+
|
|
251
|
+
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.
|
|
252
|
+
|
|
253
|
+
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).
|
|
254
|
+
|
|
255
|
+
If you're planning to use `revalidateTag`, then remember to set up the webhook (see code below) as well.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
// ./src/utils/sanity/client.ts
|
|
259
|
+
import 'server-only'
|
|
260
|
+
|
|
261
|
+
import type {QueryParams} from '@sanity/client'
|
|
262
|
+
import {createClient} from 'next-sanity'
|
|
263
|
+
|
|
264
|
+
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
265
|
+
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
266
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
267
|
+
|
|
268
|
+
const client = createClient({
|
|
269
|
+
projectId,
|
|
270
|
+
dataset,
|
|
271
|
+
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
272
|
+
useCdn: false,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const DEFAULT_PARAMS = {} as QueryParams
|
|
276
|
+
const DEFAULT_TAGS = [] as string[]
|
|
277
|
+
|
|
278
|
+
export async function sanityFetch<QueryResponse>({
|
|
279
|
+
query,
|
|
280
|
+
params = DEFAULT_PARAMS,
|
|
281
|
+
tags = DEFAULT_TAGS,
|
|
282
|
+
}: {
|
|
283
|
+
query: string
|
|
284
|
+
params?: QueryParams
|
|
285
|
+
tags: string[]
|
|
286
|
+
}): Promise<QueryResponse> {
|
|
287
|
+
return client.fetch<QueryResponse>(query, params, {
|
|
288
|
+
cache: 'force-cache',
|
|
289
|
+
next: {
|
|
290
|
+
//revalidate: 30, // for simple, time-based revalidation
|
|
291
|
+
tags, // for tag-based revalidation
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
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:
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// ./src/app/home/layout.tsx
|
|
301
|
+
import { sanityFetch } from '@/src/utils/sanity/client'
|
|
302
|
+
import { PageProps } from '@/src/app/(page)/Page.tsx'
|
|
303
|
+
|
|
304
|
+
type HomePageProps = {
|
|
305
|
+
_id: string
|
|
306
|
+
title?: string
|
|
307
|
+
navItems: PageProps[]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function HomeLayout({children}) {
|
|
311
|
+
// revalidate if there are changes to either the home document or to a page document (since they're referenced to in navItems)
|
|
312
|
+
const home = await sanityFetch<HomePageProps>({
|
|
313
|
+
query: `*[_id == "home"][0]{...,navItems[]->}`,
|
|
314
|
+
tags: ['home', 'page']
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<main>
|
|
319
|
+
<nav>
|
|
320
|
+
<span>{home?.title}</span>
|
|
321
|
+
<ul>
|
|
322
|
+
{home?.navItems.map(navItem => ({
|
|
323
|
+
<li key={navItem._id}><a href={navItem?.slug?.current}>{navItem?.title}</a></li>
|
|
324
|
+
}))}
|
|
325
|
+
</ul>
|
|
326
|
+
</nav>
|
|
327
|
+
{children}
|
|
328
|
+
</main>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
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].
|
|
334
|
+
|
|
335
|
+
You can use this [template][webhook-template] to quickly configure the webhook for your Sanity project.
|
|
336
|
+
|
|
337
|
+
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:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
// ./src/app/api/revalidate.ts
|
|
341
|
+
import {revalidateTag} from 'next/cache'
|
|
342
|
+
import {type NextRequest, NextResponse} from 'next/server'
|
|
343
|
+
import {parseBody} from 'next-sanity/webhook'
|
|
344
|
+
|
|
345
|
+
export async function POST(req: NextRequest) {
|
|
346
|
+
try {
|
|
347
|
+
const {isValidSignature, body} = await parseBody<{_type}>(
|
|
348
|
+
req,
|
|
349
|
+
process.env.SANITY_REVALIDATE_SECRET,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if (!isValidSignature) {
|
|
353
|
+
const message = 'Invalid signature'
|
|
354
|
+
return new Response(JSON.stringify({message, isValidSignature, body}), {status: 401})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!body?._type) {
|
|
358
|
+
const message = 'Bad Request'
|
|
359
|
+
return new Response({message, body}, {status: 400})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// If the `_type` is `page`, then all `client.fetch` calls with
|
|
363
|
+
// `{next: {tags: ['page']}}` will be revalidated
|
|
364
|
+
await revalidateTag(body._type)
|
|
365
|
+
|
|
366
|
+
return NextResponse.json({body})
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.error(err)
|
|
369
|
+
return new Response(err.message, {status: 500})
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
You can choose to match tags based on any field or expression since GROQ-Powered Webhooks allow you to freely define the payload.
|
|
375
|
+
|
|
376
|
+
### Slug-based revalidation for the Pages Router
|
|
377
|
+
|
|
378
|
+
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}`.
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
// ./pages/api/revalidate.ts
|
|
382
|
+
import type {NextApiRequest, NextApiResponse} from 'next'
|
|
383
|
+
import {parseBody} from 'next-sanity/webhook'
|
|
384
|
+
|
|
385
|
+
// Export the config from next-sanity to enable validating the request body signature properly
|
|
386
|
+
export {config} from 'next-sanity/webhook'
|
|
387
|
+
|
|
388
|
+
export default async function revalidate(req: NextApiRequest, res: NextApiResponse) {
|
|
389
|
+
try {
|
|
390
|
+
const {isValidSignature, body} = await parseBody(req, process.env.SANITY_REVALIDATE_SECRET)
|
|
391
|
+
|
|
392
|
+
if (!isValidSignature) {
|
|
393
|
+
const message = 'Invalid signature'
|
|
394
|
+
return res.status(401).json({message, isValidSignature, body})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const staleRoute = `/${body.slug.current}`
|
|
398
|
+
await res.revalidate(staleRoute)
|
|
399
|
+
const message = `Updated route: ${staleRoute}`
|
|
400
|
+
return res.status(200).json({message, body})
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.error(err)
|
|
403
|
+
return res.status(500).json({message: err.message})
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Working example implementation
|
|
409
|
+
|
|
410
|
+
Check out our [Personal website template][personal-website-template] to see a feature-complete example of how `revalidateTag` is used together with Live Previews.
|
|
411
|
+
|
|
412
|
+
### Debugging caching and revalidation
|
|
413
|
+
|
|
414
|
+
To aid in debugging and understanding what's in the cache, revalidated, skipped, and more, add the following to your Next.js configuration file:
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
// ./next.config.js
|
|
418
|
+
module.exports = {
|
|
419
|
+
experimental: {
|
|
420
|
+
logging: 'verbose',
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Preview
|
|
426
|
+
|
|
427
|
+
There are different ways to set up content previews with Sanity and Next.js.
|
|
428
|
+
|
|
429
|
+
### Using Perspectives
|
|
430
|
+
|
|
431
|
+
[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.
|
|
93
432
|
|
|
94
433
|
```ts
|
|
434
|
+
// ./src/utils/sanity/client.ts
|
|
435
|
+
import {createClient} from 'next-sanity'
|
|
436
|
+
|
|
437
|
+
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
438
|
+
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
439
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
440
|
+
const token = process.env.SECRET_SANITY_VIEW_TOKEN
|
|
441
|
+
|
|
442
|
+
const client = createClient({
|
|
443
|
+
projectId,
|
|
444
|
+
dataset,
|
|
445
|
+
apiVersion, // https://www.sanity.io/docs/api-versioning
|
|
446
|
+
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
|
|
447
|
+
token,
|
|
448
|
+
perspective: 'published', // prevent drafts from leaking through even though requests are authenticated
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Live Preview
|
|
453
|
+
|
|
454
|
+
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.
|
|
455
|
+
|
|
456
|
+
Router-specific setup guides for Live Preview:
|
|
457
|
+
|
|
458
|
+
- [`app-router`][preivew-app-router]
|
|
459
|
+
- [`pages-router`][preview-pages-router]
|
|
460
|
+
|
|
461
|
+
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].
|
|
462
|
+
The [same is true][preview-kit-livequery] for `next-sanity/preview/live-query`.
|
|
463
|
+
|
|
464
|
+
### Using `draftMode()` to de/activate previews
|
|
465
|
+
|
|
466
|
+
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.
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
// ./src/utils/sanity/client.ts
|
|
95
470
|
import 'server-only'
|
|
96
471
|
|
|
472
|
+
import {draftMode} from 'next/headers'
|
|
97
473
|
import type {QueryParams} from '@sanity/client'
|
|
98
474
|
import {createClient, groq} from 'next-sanity'
|
|
99
475
|
import {draftMode} from 'next/headers'
|
|
100
476
|
|
|
101
477
|
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
|
|
102
478
|
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
|
|
103
|
-
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION
|
|
479
|
+
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
|
|
104
480
|
|
|
105
481
|
const client = createClient({
|
|
106
482
|
projectId,
|
|
@@ -142,37 +518,9 @@ export async function sanityFetch<QueryResponse>({
|
|
|
142
518
|
},
|
|
143
519
|
})
|
|
144
520
|
}
|
|
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
521
|
```
|
|
174
522
|
|
|
175
|
-
##
|
|
523
|
+
## Visual Editing with Content Source Maps
|
|
176
524
|
|
|
177
525
|
> **Note**
|
|
178
526
|
>
|
|
@@ -191,68 +539,69 @@ const client = createClient({
|
|
|
191
539
|
projectId,
|
|
192
540
|
dataset,
|
|
193
541
|
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
|
|
542
|
+
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
543
|
studioUrl: '/studio', // Or: 'https://my-cool-project.sanity.studio'
|
|
196
544
|
encodeSourceMap: true, // Optional. Default to: process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview',
|
|
197
545
|
})
|
|
198
546
|
```
|
|
199
547
|
|
|
200
|
-
[
|
|
548
|
+
Go to our [setup guide][visual-editing] for a walkthrough on how to customize the experience.
|
|
201
549
|
|
|
202
|
-
##
|
|
550
|
+
## Embedded Sanity Studio
|
|
203
551
|
|
|
204
|
-
|
|
552
|
+
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
553
|
|
|
206
|
-
|
|
207
|
-
- [`pages-router`](./PREVIEW-pages-router.md)
|
|
554
|
+
This opens up many possibilities:
|
|
208
555
|
|
|
209
|
-
|
|
210
|
-
|
|
556
|
+
- Any service that hosts Next.js apps can now host your Studio.
|
|
557
|
+
- Building previews for your content is easier as your Studio lives in the same environment.
|
|
558
|
+
- Use [Data Fetching][next-data-fetching] to configure your Studio.
|
|
559
|
+
- Easy setup of [Preview Mode][next-preview-mode].
|
|
211
560
|
|
|
212
|
-
|
|
561
|
+
> [See it live][embedded-studio-demo]
|
|
213
562
|
|
|
214
|
-
|
|
563
|
+
### Configuring Sanity Studio on a route
|
|
215
564
|
|
|
216
|
-
The
|
|
565
|
+
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
566
|
|
|
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).
|
|
567
|
+
To quickly scaffold the embedded studio and a Sanity project, you can run the following command in your project folder:
|
|
222
568
|
|
|
223
|
-
|
|
569
|
+
```bash
|
|
570
|
+
npx sanity@latest init
|
|
571
|
+
```
|
|
224
572
|
|
|
225
|
-
|
|
573
|
+
### Manual installation
|
|
226
574
|
|
|
227
|
-
Both the Next `/app` and `/pages` examples
|
|
228
|
-
`sanity.config.ts`:
|
|
575
|
+
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
576
|
|
|
230
577
|
```ts
|
|
578
|
+
// ./sanity.config.ts
|
|
231
579
|
import {defineConfig} from 'sanity'
|
|
232
580
|
import {deskTool} from 'sanity/desk'
|
|
233
581
|
|
|
234
|
-
import {schemaTypes} from './
|
|
582
|
+
import {schemaTypes} from './src/schema'
|
|
235
583
|
|
|
236
584
|
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
|
|
237
585
|
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
|
|
238
586
|
|
|
239
587
|
export default defineConfig({
|
|
240
|
-
basePath: '/
|
|
588
|
+
basePath: '/admin', // <-- important that `basePath` matches the route you're mounting your studio from, it applies to both `/pages` and `/app`
|
|
241
589
|
|
|
242
590
|
projectId,
|
|
243
591
|
dataset,
|
|
244
|
-
|
|
245
592
|
plugins: [deskTool()],
|
|
246
|
-
|
|
247
593
|
schema: {
|
|
248
594
|
types: schemaTypes,
|
|
249
595
|
},
|
|
250
596
|
})
|
|
251
597
|
```
|
|
252
598
|
|
|
253
|
-
|
|
599
|
+
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.
|
|
600
|
+
|
|
601
|
+
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
602
|
|
|
255
603
|
```ts
|
|
604
|
+
// ./sanity.cli.ts
|
|
256
605
|
/* eslint-disable no-process-env */
|
|
257
606
|
import {loadEnvConfig} from '@next/env'
|
|
258
607
|
import {defineCliConfig} from 'sanity/cli'
|
|
@@ -266,13 +615,12 @@ const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
|
|
|
266
615
|
export default defineCliConfig({api: {projectId, dataset}})
|
|
267
616
|
```
|
|
268
617
|
|
|
269
|
-
Now you can run commands like `npx sanity cors add`.
|
|
618
|
+
Now you can run commands like `npx sanity cors add`. Run `npx sanity help` for a full list of what you can do.
|
|
270
619
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
`app/studio/[[...index]]/page.tsx`:
|
|
620
|
+
### Studio route with App Router
|
|
274
621
|
|
|
275
622
|
```tsx
|
|
623
|
+
// ./src/app/studio/[[...index]]/page.tsx
|
|
276
624
|
import {Studio} from './Studio'
|
|
277
625
|
|
|
278
626
|
// Ensures the Studio route is statically generated
|
|
@@ -286,9 +634,8 @@ export default function StudioPage() {
|
|
|
286
634
|
}
|
|
287
635
|
```
|
|
288
636
|
|
|
289
|
-
`app/studio/[[...index]]/Studio.tsx`:
|
|
290
|
-
|
|
291
637
|
```tsx
|
|
638
|
+
// ./src/app/studio/[[...index]]/Studio.tsx
|
|
292
639
|
'use client'
|
|
293
640
|
|
|
294
641
|
import {NextStudio} from 'next-sanity/studio'
|
|
@@ -301,10 +648,10 @@ export function Studio() {
|
|
|
301
648
|
}
|
|
302
649
|
```
|
|
303
650
|
|
|
304
|
-
|
|
305
|
-
`app/studio/[[...index]]/page.tsx`:
|
|
651
|
+
How to customize meta tags:
|
|
306
652
|
|
|
307
653
|
```tsx
|
|
654
|
+
// ./src/app/studio/[[...index]]/page.tsx
|
|
308
655
|
import type {Metadata} from 'next'
|
|
309
656
|
import {metadata as studioMetadata} from 'next-sanity/studio/metadata'
|
|
310
657
|
|
|
@@ -322,11 +669,10 @@ export default function StudioPage() {
|
|
|
322
669
|
}
|
|
323
670
|
```
|
|
324
671
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
`/pages/studio/[[...index]].tsx`:
|
|
672
|
+
### Studio Routes with Pages Router
|
|
328
673
|
|
|
329
674
|
```tsx
|
|
675
|
+
// ./pages/studio/[[...index]].tsx
|
|
330
676
|
import Head from 'next/head'
|
|
331
677
|
import {NextStudio} from 'next-sanity/studio'
|
|
332
678
|
import {metadata} from 'next-sanity/studio/metadata'
|
|
@@ -347,9 +693,9 @@ export default function StudioPage() {
|
|
|
347
693
|
}
|
|
348
694
|
```
|
|
349
695
|
|
|
350
|
-
###
|
|
696
|
+
### Lower level control with `StudioProvider` and `StudioLayout`
|
|
351
697
|
|
|
352
|
-
If you want to go lower level and have more control over the
|
|
698
|
+
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
699
|
|
|
354
700
|
```tsx
|
|
355
701
|
import {NextStudio} from 'next-sanity/studio'
|
|
@@ -369,96 +715,50 @@ function StudioPage() {
|
|
|
369
715
|
}
|
|
370
716
|
```
|
|
371
717
|
|
|
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
718
|
## Migration guides
|
|
446
719
|
|
|
447
720
|
- From `v4` to `v5`
|
|
448
|
-
- [`app-router`]
|
|
449
|
-
- [`pages-router`]
|
|
450
|
-
- [From `<0.4` to `v4`]
|
|
721
|
+
- [`app-router`][migrate-v4-to-v5-app]
|
|
722
|
+
- [`pages-router`][migrate-v4-to-v5-pages]
|
|
723
|
+
- [From `<0.4` to `v4`][migrate-v1-to-v4]
|
|
451
724
|
|
|
452
725
|
## License
|
|
453
726
|
|
|
454
|
-
MIT-licensed. See [LICENSE]
|
|
455
|
-
|
|
456
|
-
[
|
|
457
|
-
[
|
|
458
|
-
[
|
|
459
|
-
[
|
|
727
|
+
MIT-licensed. See [LICENSE][LICENSE].
|
|
728
|
+
|
|
729
|
+
[api-versioning]: https://www.sanity.io/docs/api-versioning?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
730
|
+
[app-router]: https://nextjs.org/docs/app/building-your-application/routing
|
|
731
|
+
[cdn]: https://www.sanity.io/docs/asset-cdn?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
732
|
+
[ci-workflow]: https://github.com/sanity-io/next-sanity/actions/workflows/ci.yml
|
|
733
|
+
[content-source-maps-intro]: https://www.sanity.io/blog/content-source-maps-announce?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
734
|
+
[content-source-maps]: https://www.sanity.io/docs/content-source-maps?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
735
|
+
[draft-mode]: https://nextjs.org/docs/app/building-your-application/configuring/draft-mode
|
|
736
|
+
[embedded-studio-demo]: https://next.sanity.build/studio
|
|
737
|
+
[enterprise-cta]: https://www.sanity.io/enterprise?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
738
|
+
[groq-syntax-highlighting]: https://marketplace.visualstudio.com/items?itemName=sanity-io.vscode-sanity
|
|
739
|
+
[groq-webhook]: https://www.sanity.io/docs/webhooks?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
740
|
+
[image-url]: https://www.sanity.io/docs/presenting-images?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
741
|
+
[LICENSE]: LICENSE
|
|
742
|
+
[migrate-v1-to-v4]: ./MIGRATE-v1-to-v4.md
|
|
743
|
+
[migrate-v4-to-v5-app]: ./MIGRATE-v4-to-v5-pages-app-router.md
|
|
744
|
+
[migrate-v4-to-v5-pages]: ./MIGRATE-v4-to-v5-pages-pages-router.md
|
|
745
|
+
[next-cache]: https://nextjs.org/docs/app/building-your-application/caching
|
|
746
|
+
[next-data-fetching]: https://nextjs.org/docs/basic-features/data-fetching/overview
|
|
747
|
+
[next-preview-mode]: https://nextjs.org/docs/advanced-features/preview-mode
|
|
748
|
+
[pages-router]: https://nextjs.org/docs/pages/building-your-application/routing
|
|
749
|
+
[personal-website-template]: https://github.com/sanity-io/sanity-template-nextjs-app-router-personal-website
|
|
750
|
+
[perspectives-docs]: https://www.sanity.io/docs/perspectives?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
751
|
+
[portable-text]: https://portabletext.org
|
|
752
|
+
[preivew-app-router]: ./PREVIEW-app-router.md
|
|
460
753
|
[preview-kit-client]: https://github.com/sanity-io/preview-kit#sanitypreview-kitclient
|
|
461
|
-
[
|
|
462
|
-
[
|
|
463
|
-
[
|
|
464
|
-
[
|
|
754
|
+
[preview-kit-documentation]: https://github.com/sanity-io/preview-kit#sanitypreview-kit-1
|
|
755
|
+
[preview-kit-livequery]: https://github.com/sanity-io/preview-kit#using-the-livequery-wrapper-component-instead-of-the-uselivequery-hook
|
|
756
|
+
[preview-kit]: https://github.com/sanity-io/preview-kit
|
|
757
|
+
[preview-pages-router]: ./PREVIEW-pages-router.md
|
|
758
|
+
[revalidate-tag]: https://nextjs.org/docs/app/api-reference/functions/revalidateTag
|
|
759
|
+
[sales-cta]: https://www.sanity.io/contact/sales?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
760
|
+
[sanity-client]: https://www.sanity.io/docs/js-client?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
761
|
+
[sanity]: https://www.sanity.io?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
762
|
+
[visual-editing-intro]: https://www.sanity.io/blog/visual-editing-sanity-vercel?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
763
|
+
[visual-editing]: https://www.sanity.io/docs/vercel-visual-editing?utm_source=github&utm_medium=readme&utm_campaign=next-sanity
|
|
764
|
+
[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
|