Skip to content

Commit 404acba

Browse files
committed
feat: update Sanity dependencies and enhance draft mode functionality
- Upgraded Sanity-related packages to their latest versions for improved performance and features. - Implemented dynamic fetching options in various components to support draft mode, enhancing content visibility during editing. - Adjusted the sitemap and page components to utilize the new fetching methods, ensuring accurate data retrieval based on the selected perspective. - Introduced caching mechanisms for improved efficiency in data fetching.
1 parent 827fd8b commit 404acba

65 files changed

Lines changed: 3480 additions & 738 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
---
2+
name: sanity-live-cache-components
3+
description: Integrates Sanity Live with Next.js Cache Components in next-sanity v13+ apps. Sets up sanityFetch, <SanityLive>, Visual Editing, Presentation Tool, draft mode handling, and the three-layer (Page/Dynamic/Cached) component pattern with explicit perspective/stega prop-drilling. Use when configuring or migrating a Next.js app to cacheComponents with Sanity, when adding sanityFetch, when wiring <SanityLive>/<VisualEditing>, or when refactoring components that hardcode perspective/stega.
4+
---
5+
6+
# Sanity Live + Cache Components
7+
8+
Wires `next-sanity` into a Next.js 16+ app with `cacheComponents: true`. Data is fetched with `sanityFetch` (which calls `cacheTag`/`cacheLife` internally), and `<SanityLive>` in the root layout revalidates cached content over an EventSource connection to Sanity Content Lake. Visual Editing and Presentation Tool are fully supported when draft mode is enabled.
9+
10+
Read the relevant guide in `node_modules/next/dist/docs/` (when available) before writing code. If a guide conflicts with this skill, follow this skill.
11+
12+
This skill assumes familiarity with the `next-cache-components` skill — it covers `'use cache'`, `cacheLife`, `cacheTag`, and the cookies/headers/params rule. The only Sanity-relevant exception: `await draftMode()` is allowed inside `'use cache'` (Next.js bypasses caching when draft mode is enabled — see [the `use cache` reference](https://nextjs.org/docs/app/api-reference/directives/use-cache#draft-mode)).
13+
14+
## Prerequisites
15+
16+
- Next.js 16.2+ installed in the project (check `package.json` or run `pnpm list next` / `npm ls next` — don't use `pnpm view next version`, that reports the registry's latest, not what's installed).
17+
- `AGENTS.md` exists, or [follow the guide](https://nextjs.org/docs/app/guides/ai-agents#existing-projects).
18+
- These environment variables are set:
19+
- `NEXT_PUBLIC_SANITY_PROJECT_ID`
20+
- `NEXT_PUBLIC_SANITY_DATASET`
21+
- `SANITY_API_READ_TOKEN`
22+
- Embedded Sanity Studio configuration (`sanity.config.ts`, `sanity.cli.ts`, anything under `sanity/`) needs no changes — this skill only touches the Next.js app surface.
23+
24+
## Reference files
25+
26+
| File | When to read |
27+
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
28+
| [reference/live-helpers.md](reference/live-helpers.md) | Full `client.ts` / `live.ts`, `sanityFetch*` and `getDynamicFetchOptions` details |
29+
| [reference/three-layer-pattern.md](reference/three-layer-pattern.md) | The Page → Dynamic → Cached pattern for `page.tsx`, including the `searchParams` variant |
30+
| [reference/layouts.md](reference/layouts.md) | Non-blocking data fetching inside `layout.tsx` with a shared `'use cache'` helper |
31+
| [reference/dynamic-segments.md](reference/dynamic-segments.md) | High-performance `[slug]` routes: `loading.tsx` + partial `generateStaticParams`, or non-blocking dynamic `params` in a layout |
32+
33+
---
34+
35+
## 1. Install `next-sanity@^13`
36+
37+
```bash
38+
npm install next-sanity@^13 --save-exact
39+
```
40+
41+
### Migrating an existing Sanity Live setup
42+
43+
If the app is already using `defineLive`, this skill is a refactor, not a rewrite. The 5-step sequence below still applies, but watch for these specific differences:
44+
45+
- **Don't overwrite `client.ts` or `live.ts`** if they exist. Append missing options. Preserve any existing `token` and `stega.*` settings — see [reference/live-helpers.md](reference/live-helpers.md).
46+
- **Search the codebase for hardcoded `perspective: 'published'` and `stega: false`** in `sanityFetch` callsites and refactor them to source `perspective`/`stega` via `getDynamicFetchOptions` and the three-layer pattern.
47+
- **Search for `sanityFetch` calls inside `generateStaticParams`** → swap for `sanityFetchStaticParams`.
48+
- **Search for `sanityFetch` calls inside `generateMetadata` / `sitemap.ts` / `opengraph-image.tsx` / etc.** → swap for `sanityFetchMetadata`.
49+
- **Search for `sanityFetch` calls directly inside a `'use server'` function** → split into a separate `'use cache'` helper.
50+
- **Verify there is exactly one `<SanityLive>` and one `<VisualEditing>` in the tree.** Multiple renders are undefined behavior.
51+
52+
The "Anti-patterns to grep for" section at the bottom of this file lists the search patterns.
53+
54+
---
55+
56+
## 2. Configure `next.config.ts`
57+
58+
Enable `cacheComponents` and set `cacheLife.default` to `sanity` so default revalidation is 1 year (instead of 15 minutes). `sanityFetch` is optimized for on-demand revalidation and doesn't need time-based revalidation.
59+
60+
```ts
61+
// next.config.ts
62+
import type {NextConfig} from 'next'
63+
import {sanity} from 'next-sanity/live/cache-life'
64+
65+
const nextConfig: NextConfig = {
66+
cacheComponents: true,
67+
cacheLife: {default: sanity},
68+
}
69+
70+
export default nextConfig
71+
```
72+
73+
---
74+
75+
## 3. Configure `defineLive` and export helpers
76+
77+
Create `src/sanity/lib/client.ts` and `src/sanity/lib/live.ts`. The minimal `defineLive` call:
78+
79+
```ts
80+
// src/sanity/lib/live.ts (excerpt)
81+
export const {SanityLive, sanityFetch} = defineLive({
82+
client,
83+
serverToken: token,
84+
browserToken: token,
85+
strict: true,
86+
})
87+
```
88+
89+
Full file contents (including `client.ts`, `getDynamicFetchOptions`, `sanityFetchMetadata`, `sanityFetchStaticParams`) and per-helper guidance: [reference/live-helpers.md](reference/live-helpers.md).
90+
91+
The helpers exported from `live.ts`:
92+
93+
| Helper | Used in |
94+
| ------------------------- | ---------------------------------------------------------------------------------------------- |
95+
| `sanityFetch` | `'use cache'` components rendered from `page.tsx` / `layout.tsx` |
96+
| `sanityFetchMetadata` | `generateMetadata`, `generateViewport`, `sitemap.ts`, `robots.ts`, `opengraph-image.tsx`, etc. |
97+
| `sanityFetchStaticParams` | `generateStaticParams` only |
98+
| `getDynamicFetchOptions` | Resolving `perspective`/`stega` outside any `'use cache'` boundary |
99+
| `SanityLive` | Rendered once in a root layout |
100+
101+
---
102+
103+
## 4. Render `<SanityLive>` in a root layout
104+
105+
`<SanityLive>` and `<VisualEditing>` both belong in a `layout.tsx`, never a `page.tsx`. Both must be rendered at most once across the whole tree — duplicate renders are undefined behavior.
106+
107+
- `includeDrafts` is **required** when `defineLive` is configured with `strict: true` (the recommended setup). TypeScript will surface the error if it's missing; pass `includeDrafts={isDraftMode}` so live revalidation includes drafts only in draft mode.
108+
- Preserve any existing optional callback props on `<SanityLive>` when migrating: `onError`, `onWelcome`, `onReconnect`. They are commonly wired to a toast/notification helper and silently dropping them regresses UX.
109+
110+
```tsx
111+
// src/app/layout.tsx
112+
import {SanityLive} from '@/sanity/lib/live'
113+
import {VisualEditing} from 'next-sanity/visual-editing'
114+
import {draftMode} from 'next/headers'
115+
116+
export default async function RootLayout({children}: LayoutProps<'/'>) {
117+
const {isEnabled: isDraftMode} = await draftMode()
118+
return (
119+
<html lang="en">
120+
<body>
121+
{children}
122+
<SanityLive includeDrafts={isDraftMode} />
123+
{isDraftMode && <VisualEditing />}
124+
</body>
125+
</html>
126+
)
127+
}
128+
```
129+
130+
### With an embedded Sanity Studio
131+
132+
If a route mounts `NextStudio` from `next-sanity/studio` (e.g. `app/studio/[[...index]]/page.tsx`), `<SanityLive>` must live in a layout the embedded studio doesn't share. Use [route groups](https://nextjs.org/docs/app/api-reference/file-conventions/route-groups): put `<SanityLive>` in `src/app/(website)/layout.tsx` and keep the rest of the app under `src/app/(website)`.
133+
134+
---
135+
136+
## 5. Apply the three-layer pattern to pages and layouts
137+
138+
Every route that should be statically prerendered uses the same shape:
139+
140+
```text
141+
Page/Layout (Layer 1: draftMode branch)
142+
├── NOT draft mode → <CachedX perspective="published" stega={false} /> (no Suspense)
143+
└── draft mode → <Suspense fallback={...}>
144+
<DynamicX params={params} /> (Layer 2: awaits dynamic APIs)
145+
└── <CachedX perspective={p} stega={s} /> (Layer 3: 'use cache')
146+
```
147+
148+
**Critical rule**: Only Layer 3 carries `'use cache'`. The top-level `Page` / `Layout` must **not** have `'use cache'` — it awaits `params`, `searchParams`, or `cookies()` (via `getDynamicFetchOptions`), and those dynamic APIs are forbidden inside `'use cache'`. Layer 3 carrying `'use cache'` is enough for the whole route to prerender into the static shell. Adding `'use cache'` to the top-level function is the most common failure mode — TypeScript and the runtime will both complain.
149+
150+
Pick the right reference for the file you're editing:
151+
152+
- **`page.tsx`** with static or `generateStaticParams`-backed params → [reference/three-layer-pattern.md](reference/three-layer-pattern.md).
153+
- **`page.tsx`** that uses `searchParams` or other dynamic APIs → the `searchParams` variant in [reference/three-layer-pattern.md](reference/three-layer-pattern.md).
154+
- **`layout.tsx`** that fetches its own data → [reference/layouts.md](reference/layouts.md).
155+
- **Dynamic `[slug]` route** that needs the `loading.tsx` + partial `generateStaticParams` optimization, or a layout that needs non-blocking `params`[reference/dynamic-segments.md](reference/dynamic-segments.md).
156+
157+
---
158+
159+
## Anti-patterns to grep for
160+
161+
When auditing an app, search for these and refactor:
162+
163+
- `perspective: 'published'` and `stega: false` hardcoded together in a `sanityFetch` call → use the three-layer pattern, source `perspective`/`stega` via `getDynamicFetchOptions`.
164+
- `sanityFetch(` directly inside a function whose body begins with `'use server'` → split into a separate `'use cache'` helper.
165+
- `sanityFetch(` inside `generateStaticParams` → swap for `sanityFetchStaticParams`.
166+
- `sanityFetch(` inside `generateMetadata` / `generateViewport` / `sitemap.ts` / `robots.ts` / `opengraph-image.tsx` etc. → swap for `sanityFetchMetadata` and resolve `perspective` via `getDynamicFetchOptions`.
167+
- `await draftMode()` immediately followed by `await getDynamicFetchOptions()` at the top of a `page.tsx` or `layout.tsx` without a sibling `loading.tsx` → move those dynamic-API calls into a child component wrapped in `<Suspense>` so the static shell can prerender.
168+
- More than one `<SanityLive>` or `<VisualEditing>` rendered in the tree → consolidate to a single render in the right layout.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# High-performance dynamic segments
2+
3+
[Dynamic routes](https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes) should always implement `generateStaticParams`, even if only a subset of pages — see [the Cache Components note on dynamic routes](https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes#with-cache-components). Whether to use `loading.tsx` or `<Suspense>` for fallback UI depends on the use case — see [the streaming guide](https://nextjs.org/docs/app/guides/streaming#when-to-use-loadingjs-vs-suspense).
4+
5+
## Contents
6+
7+
- [Case 1: `page.tsx` with `loading.tsx` + partial `generateStaticParams`](#case-1-pagetsx-with-loadingtsx--partial-generatestaticparams)
8+
- [Case 2: `layout.tsx` with non-blocking dynamic `params`](#case-2-layouttsx-with-non-blocking-dynamic-params)
9+
10+
## Case 1: `page.tsx` with `loading.tsx` + partial `generateStaticParams`
11+
12+
`generateStaticParams` returns only the 100 most recently updated pages. A sibling `loading.tsx` renders fallback UI, so `page.tsx` itself can skip the `<Suspense>` wrapper. The same fallback UI is reused in draft mode.
13+
14+
This scales to thousands of pages without ballooning `next build` and without compromising UX in production:
15+
16+
- Prerendered pages load instantly.
17+
- Pages not prerendered start rendering on `<Link>` hover (or when scrolled into view), so on click:
18+
- If prerendering finished in time → serves instantly, no loading state.
19+
- If not → instantly shows the cached `loading.tsx` fallback.
20+
21+
Add a sibling `src/app/[slug]/loading.tsx` that renders the same skeleton you would otherwise pass to `<Suspense>`. Keep it cheap and free of layout shift:
22+
23+
```tsx
24+
// src/app/[slug]/loading.tsx
25+
export default function Loading() {
26+
return (
27+
<article aria-busy>
28+
<p>Loading…</p>
29+
</article>
30+
)
31+
}
32+
```
33+
34+
```tsx
35+
// src/app/[slug]/page.tsx
36+
import {
37+
getDynamicFetchOptions,
38+
sanityFetch,
39+
sanityFetchStaticParams,
40+
type DynamicFetchOptions,
41+
} from '@/sanity/lib/live'
42+
import {defineQuery} from 'next-sanity'
43+
44+
export async function generateStaticParams() {
45+
const pageSlugsQuery = defineQuery(
46+
`*[_type == "page" && defined(slug.current)] | order(_updatedAt desc) [0...100]{"slug": slug.current}`,
47+
)
48+
const {data} = await sanityFetchStaticParams({query: pageSlugsQuery})
49+
return data
50+
}
51+
52+
// With sibling `loading.tsx`, skip the `<Suspense>` + `DynamicPage` indirection: await `params`
53+
// and `getDynamicFetchOptions` directly inside `Page`.
54+
export default async function Page({params}: PageProps<'/[slug]'>) {
55+
const [{slug}, {perspective, stega}] = await Promise.all([params, getDynamicFetchOptions()])
56+
return <CachedPage slug={slug} perspective={perspective} stega={stega} />
57+
}
58+
async function CachedPage({
59+
slug,
60+
perspective,
61+
stega,
62+
}: Awaited<PageProps<'/[slug]'>['params']> & DynamicFetchOptions) {
63+
'use cache'
64+
const pageQuery = defineQuery(`*[_type == "page" && slug.current == $slug][0]`)
65+
const {data} = await sanityFetch({
66+
query: pageQuery,
67+
params: {slug},
68+
perspective,
69+
stega,
70+
})
71+
return <article>{/* use `data` to render stuff */}</article>
72+
}
73+
```
74+
75+
## Case 2: `layout.tsx` with non-blocking dynamic `params`
76+
77+
A `layout.tsx` can't use `loading.tsx` for fallback UI — [it's one level higher in the hierarchy](https://nextjs.org/docs/app/getting-started/project-structure#component-hierarchy). To fetch data that depends on dynamic `params` without blocking `children` from streaming, pass the unawaited `params` promise into a `<Suspense>` boundary and await it inside.
78+
79+
```tsx
80+
// src/app/(website)/[slug]/layout.tsx
81+
82+
import {getDynamicFetchOptions, sanityFetch, type DynamicFetchOptions} from '@/sanity/lib/live'
83+
import {defineQuery} from 'next-sanity'
84+
import {Suspense} from 'react'
85+
86+
export default function WebsiteLayout({children, params}: LayoutProps<'/[slug]'>) {
87+
return (
88+
<>
89+
{children}
90+
{/* The footer renders below the fold, no fallback needed */}
91+
<Suspense>
92+
<DynamicFooter
93+
// Don't await `params` here — pass the promise and await inside Suspense so `children` streams in parallel
94+
params={params}
95+
/>
96+
</Suspense>
97+
</>
98+
)
99+
}
100+
async function DynamicFooter({params}: Pick<LayoutProps<'/[slug]'>, 'params'>) {
101+
const [{slug}, {perspective, stega}] = await Promise.all([params, getDynamicFetchOptions()])
102+
return <Footer slug={slug} perspective={perspective} stega={stega} />
103+
}
104+
async function Footer({
105+
slug,
106+
perspective,
107+
stega,
108+
}: Awaited<LayoutProps<'/[slug]'>['params']> & DynamicFetchOptions) {
109+
'use cache'
110+
const footerQuery = defineQuery(`*[_type == "footer" && slug.current == $slug][0]`)
111+
const {data} = await sanityFetch({query: footerQuery, params: {slug}, perspective, stega})
112+
return <footer>{/* use `data` to render stuff */}</footer>
113+
}
114+
```

0 commit comments

Comments
 (0)