|
| 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. |
0 commit comments