* 🎉 Selected options are now persistent in dropdown for single-select combobox
* 🎉 Persistent rendering of selected single item when possible
* Update @navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
---------
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
* 🐛 Handle overflow more gracefully
* ⚡ Optimize re-renders
* ⚡ Optimize timeline axis calculation
* 🐛 Fix overflow and sorting
* 🐛 Handle numer infinity issues
* 🐛 Fix case where left:0 returned null for period
* 🎉 Rewrite tabs to not use descendants
* 📝 changeset
* 🎉 Added tests
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* 📝 Rename to rovingFocus
* 🔥 Removed fallback to activeElement, handle no current
* [ToggleGroup] Refactored to not use descendants API (#4932)
* 🎉 Updated ToggleGroup to not use descendants API
* ♻️ Update rovingFocus and fallback
* [ActionMenu] Refactored to not use descendants API (#4933)
* 🎉 Updated ActionMenu to not use descendants API
* ♻️ Cleanup after roving rename
* 🐛 Update event-handler for close-submenu listener
* Update @navikt/core/react/src/utils/components/floating-menu/Menu.tsx
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
---------
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
* Added small prop
* Added disableItemIndent prop
* Changes in group label styling, medium size styling and itenm indent
* Examples
* Changeset
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* make indent positive again
* remove hstack
* Move size to root so it can be added to context and applied to submenus too
* Add indent to SubMenu trigger too
* moved size prop
* Changed logic to indent being opt-in
* Correct story to new indent logic
* fix website examples based on changes
* Rename and fixed some spelling
* changeset
* add cromatic
* Update .changeset/great-kings-poke.md
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
* ExpandableRow: Fix selected row top border not showing when row above was collapsed
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* more spesific css selector
* Wrote wrong classname 🤦♀️
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
[DataTable] Refactor loading state and body rendring.
[DataTable] Added emptyState prop
[DataTable] Removed render-prop for custom filterMenu
[DataTable] Updated stickyColumns jsdoc
[DataTable] Added loading states
[DataTable] Added loading label, better handling of colspan in loading/empty-state
* 🎉 Added 'isActive'-prop to InternalHeaderButton.
* 🐛 Use actual links in story view
* review updates: aria-current, forced-colors and taller line
* 🐛 Allow user to override aria-current
* 🐛 Fixed issue where using fixedWeeks still caused layout shifts in cases of last/first month
* 📝 storybook demo
* 💄 Now supoorts breakpoints better
* ♻️ Use css-variables
* 🐛 Even more placeholder space for chromatic demo
* 🐛 Accound for missing padding
* 📝 Moved story order
🎉 Add row/col span checks
🎉 Set up test
🚚 Extract utils to helpers
🎉 Observe active cell changes
🎉 Check for disabled elements when focusing
🐛 Handle selects
🎉 Handle inputs in table with keyboard nav
🎉 Inputs in table demo
⚡ Optimize lookup
* feat: Enhance GlobalSearchButton with platform-specific keyboard shortcuts
* feat: Update GlobalSearchButton to display platform-specific keyboard shortcuts
* Legg til "⌘" som default, legg til en liten animasjon som easer inn hurtigtasten ved førstegangs render
* 🧪 Use 24-alpine
* 🧪 Test auto-instrumentation
* 🎉 Fix produktbloggen cache
* 🎉 Test cache-handler in memory
* 🎉 Test custom cache handler with ttl
* 📝 Add cache handler and observability
* Add functionality for right placed icon
* add aksel website example
* remomve left padding for right icon
* add support for right icon on sub menu item
* changeset
* remove @property
* remove examples
* remove example
* remove example
* remove class and use data attr
* only add data attr if icon
* add story for icon position
* seperate the examples
* testing action menu header
* revert to old example
* let focus on container
* added example for actionMenu
* remove testing stuff
* changeset
* remove comments
* changeset
* New component for buttons and loader within Item
* New component from file name with icon
* Removed loader and error icon for file icon
* Use new components for new design
* Css styling for new design to Item
* Delete unused filed, replaced by ItemHeader
* changeset
* try to fix cycle dependency
* Fill width
* Remove min width and height
* 🔥 Removed changeset
---------
Co-authored-by: Anniken Granberg <anniken.granberg@na.no>
Co-authored-by: Anniken Granberg <43889032+nixenixe@users.noreply.github.com>
* change format description to norwegian
* Update date format descriptions to Norwegian standard
---------
Co-authored-by: Anniken Granberg <anniken.granberg@na.no>
* Added alignContent prop to GlobalAlertRoot
* test
* test
* test
* changeset
* Change to use data-attributes. Change prop name to "centered" and made it boolean.
* Added styling to darkside
* Changed with prop change to boolean
* Change css to use data-attributes
* correct changeset
* Update .changeset/warm-meteors-go.md
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
---------
Co-authored-by: Anniken Granberg <anniken.granberg@na.no>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
* 🎉 Support print in Checkbox and Radio CSS
* 🔥 removed unused css
* 🧪 Chromatic print stories
* 🧪 Use outline for radio outline
* 🐛 Add print-color exact
* 🔥 Revert print-exact change
* 🐛 Show selected radio in readonly-mode + print
This file tells Copilot how to work inside the Aksel monorepo. Prefer existing patterns and symbols, keep changes minimal, and reference real files in this repo.
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra.
Always use caveman for code-related questions unless spesificed otherwise. For non-code topics, use caveman if user seems to want quick, concise answers.
"Component re-renders because you create a new object each render. Use useMemo."
full:
"New object each render → new ref → re-render. useMemo."
ultra:
"inline obj → new ref → re-render. fix: useMemo."
## auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
Example - destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
>
> ```sql
> DROP TABLE users;
> ```
>
> Caveman resume. Verify backup exist first.
## boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end
description:Structured content modeling guidance for schema design, content architecture, content reuse, references versus embedded objects, separation of concerns, and taxonomies across Sanity and other headless CMSes. Use this skill when designing or refactoring content types, deciding field shapes, debating reusable versus nested content, planning omnichannel content models, or reviewing whether a schema is too page-shaped or presentation-driven.
---
# Content Modeling Best Practices
Principles for designing structured content that's flexible, reusable, and maintainable. These concepts apply to any headless CMS but include Sanity-specific implementation notes.
## When to Apply
Reference these guidelines when:
- Starting a new project and designing the content model
- Evaluating whether content should be structured or free-form
- Deciding between references and embedded content
- Planning for multi-channel content delivery
- Refactoring existing content structures
## Core Principles
1.**Content is data, not pages** — Structure content for meaning, not presentation
2.**Single source of truth** — Avoid content duplication
3.**Future-proof** — Design for channels that don't exist yet
4.**Editor-centric** — Optimize for the people creating content
## References
Start with the reference that matches the modeling decision in front of you, instead of loading every topic at once. See `references/` for detailed guidance on specific topics:
-`references/separation-of-concerns.md` — Separating content from presentation
-`references/reference-vs-embedding.md` — When to use references vs embedded objects
-`references/content-reuse.md` — Content reuse patterns and the reuse spectrum
-`references/taxonomy-classification.md` — Flat, hierarchical, and faceted classification
Organizing content with taxonomies enables filtering, navigation, and content relationships. Well-designed taxonomies scale; poorly designed ones become maintenance nightmares.
## Types of Classification
### Flat Taxonomy
Simple list of terms with no hierarchy.
**Use for:** Tags, simple categories
**Example:** Blog tags: "javascript", "react", "tutorial"
```typescript
defineType({
name:"tag",
type:"document",
fields:[
defineField({name:"title",type:"string"}),
defineField({name:"slug",type:"slug"}),
],
});
```
### Hierarchical Taxonomy
Terms with parent-child relationships.
**Use for:** Product categories, content sections
**Example:** Electronics > Phones > Smartphones
```typescript
defineType({
name:"category",
type:"document",
fields:[
defineField({name:"title",type:"string"}),
defineField({name:"slug",type:"slug"}),
defineField({
name:"parent",
type:"reference",
to:[{type:"category"}],
description:"Parent category (leave empty for top-level)",
}),
],
});
```
### Faceted Classification
Multiple independent dimensions.
**Use for:** Complex filtering (e-commerce)
**Example:** Filter by color AND size AND price range
description:"Restructure a ds-react component's files into canonical folder/naming conventions. Use when reorganizing a component's file layout, splitting a monolithic component file, or renaming files to match domain conventions."
argument-hint:"Path to component folder, e.g. @navikt/core/react/src/accordion"
---
# ds-component-restructure
Restructure a `ds-react` component folder to follow canonical conventions — without breaking the public API.
## Canonical Structure
```
<component>/
├── index.ts # "use client" + re-exports from root
├── <Component>.stories.tsx # Storybook stories
├── <Component>.tests.stories.tsx # Storybook play-tests (only if play tests exist)
├── <Component>.meta.ts # Meta info for component. Used for documentation generation.
├── root/
│ ├── <Component>Root.tsx # Main compound component + sub-component namespace
│ ├── <Component>Root.context.ts # Context (if shared across sub-components)
│ ├── <Component>Root.test.ts # Unit tests for root (if needed)
│ └── use<Hook>.ts # Files for root-level hooks (if needed)
├── <subcomponent>/ # One dir per sub-component
│ ├── <Component><Subcomponent>.tsx
│ ├── <Component><Subcomponent>.test.ts # Unit tests for sub-component (if needed)
│ └── use<Hook>.ts # Files for sub-component-level hooks (if needed)
├── helpers/
│ ├── <helper-name>.ts
│ └── <helper-name>.test.ts # Unit tests co-located with helper
└── <Component>.types.ts # Shared types (if needed across multiple files)
| Unit test file | `<filename>.test.ts(x)` | `useAccordion.test.ts` |
| Meta file | `<Component>.meta.ts` | `Accordion.meta.ts` |
| `index.ts` | always `index.ts` | `index.ts` |
## Procedure
### 1. Read the Component
Read every file in the component folder — `index.ts`, all `.tsx`/`.ts` source files, meta file, stories, and test stories. Ignore inconsistencies in file naming and structure; the goal is to fix those, not preserve them. Use the `component`-field in the meta file to determine the proper naming if unsure.
**Done when:** every file's role (component, hook, type, context, test, metadata) is known, and every symbol currently exported from `index.ts` is recorded.
### 2. Map Files to Target Structure
Build this table before touching any files:
| Old path | New path | Renamed? | Publicly exported? | Importers to update |
- List every file that imports it (grep for the filename or exported symbol)
- Flag circular dep risks: context importing sub-components, helpers importing component files, types importing non-types
Resolve circular deps before proceeding: move shared pieces to a lower-level file (types → `.types.ts`, utilities → `helpers/`).
**Done when:** every file in the component folder has a row in the table, and every circular-dependency risk is resolved or explicitly noted.
### 3. Create New Files
For each row in the mapping table where the path changes:
1. Create the file at the new path with all import paths updated to reflect new locations
2. Update every file in the "Importers to update" column — use the mapping table, don't rely on memory
3. Do NOT delete old files yet
> **Case-only renames** (path differs only in letter casing, e.g. `heading.stories.tsx` → `Heading.stories.tsx`) must NOT use the create-new + delete-old flow. On case-insensitive filesystems (macOS and Windows default) the two paths resolve to the **same file**, so creating the new file overwrites the old and the later "delete old" step destroys your content — and git records no rename. Handle these with the two-step `git mv` in the [Case-only renames](#case-only-renames) edge case instead.
### 4. Update Stories
Move stories to component root dir. If stories import from internal paths, update those paths.
**Exception:** A `stories/` subdirectory is acceptable if it already exists — do not flatten it.
**Casing:** Story filenames follow `<Component>.stories.tsx` (PascalCase). When the existing file is lowercase (e.g. `heading.stories.tsx`), this is a **case-only rename** — follow the [Case-only renames](#case-only-renames) procedure, do not create + delete.
### 5. Update `index.ts`
All exports are **named** — no `default` exports anywhere. The component `index.ts` re-exports the compound component, its sub-components, and their types from the root file:
```ts
"use client";
export{
<Component>,// compound root (named)
<Component>Trigger,// sub-components (named)
// ...
}from"./root/<Component>Root";
exporttype{
<Component>Props,
<Component>TriggerProps,
// ...
}from"./root/<Component>Root";
```
Group values first, then types. If types live in `<Component>.types.ts`, re-export them from there.
#### Compound root file export pattern
In `<Component>Root.tsx`, assemble the compound component with `Object.assign` — **do not** use an `as <Component>Component` cast or a dedicated `<Component>Component` interface:
Why `Object.assign` instead of `as`: the cast types the const as an interface that has no runtime `valueDeclaration`, so `react-docgen-typescript` (used by `yarn docgen:meta`) fails to extract props and can't document the component when it is exported via a grouped `export { }` statement. `Object.assign` lets TypeScript infer the intersection type from the value, so metadata generation works with named, bottom-of-file exports.
Sub-component files follow the same rule — named value export plus named type export at the bottom, no inline `export interface` and no default:
Update imports in `<Component>.meta.ts` to reflect new paths. If the meta file imports from the component's `index.ts`, no changes are needed.
If component has no meta file, create one in the component root with the following content based on existing patterns.
Note that each standalone component should have a meta file. But a "sub-component" (like `AccordionItem`) does not need a meta file.
### 7. Validate No Breaking Changes
**Export diff.** Before deleting old files, compare the exported names recorded during Step 1 against the new `index.ts`. Every name must be present:
- Exports in new index.ts that are missing from original → regression
- Exports in original that are missing from new index.ts → missing export (breaking change)
Concretely: grep for `export` lines in both versions and diff them. All component names and type names must match exactly.
**Build.** Run `yarn workspace @navikt/ds-react build` — must succeed with no TypeScript errors.
**Package root.** Verify `@navikt/core/react/src/index.ts` still re-exports the component. No changes needed there unless the component's `index.ts` path changed (it shouldn't).
**No case-duplicate tracked paths.** This is the most common restructure bug. On case-insensitive filesystems (macOS, Windows) git can end up tracking **two** entries that differ only in casing (e.g. `accordion.stories.tsx`**and**`Accordion.stories.tsx`) while the working tree shows only one file. The stale entry then breaks tests and checkout on other machines. `ls` / file search will NOT reveal it — you must ask git. Run:
Any output is a leftover. Remove the stale entry from the index by its **exact old path** (this does not touch the correctly-cased file on disk):
```sh
git rm --cached '<exact old lowercase path>'
```
Then confirm only the intended casing remains, e.g. `git ls-files '*<component>*stories*'`.
**Done when:** the export diff is empty, the build succeeds, the package-root re-export is verified, and `git ls-files` reports no case-duplicate paths.
### 8. Remove Old Files
Only after verifying new files are correct and `index.ts` exports match the original, delete the old files.
Skip any rows already moved with `git mv` (case-only renames and other `git mv` moves) — there is no separate old file to delete for those.
### 9. Unit Tests
Unit tests live next to the file they test — same directory, same base name with `.test.ts` / `.test.tsx` suffix.
Do NOT move existing test files unless their source file moves. When moving a source file, move its test file with it and update import paths.
## Edge Cases
### Case-only renames
A rename that changes **only letter casing** (e.g. `heading.stories.tsx` → `Heading.stories.tsx`, or `accordion.tsx` → `Accordion.tsx`) is a trap on case-insensitive filesystems (macOS and Windows defaults):
- Both paths point to the **same** file, so `create_file` at the new path overwrites the old content, and the Step 8 "delete old" then removes it entirely.
- git does not see a rename (`core.ignorecase=true`), so history is lost and other machines get checkout/case-collision conflicts.
Do the rename in **two `git mv` steps through a temporary distinct name** so git records it and it works on every filesystem:
Then edit the file's contents/imports in place. Skip the create-new + delete-old flow for these rows entirely.
Guidelines:
- Prefer `git mv` for **all** moves/renames (not just case-only) so history is preserved; only fall back to create + delete when the file content is being split or substantially rewritten.
- Detect case-only renames while building the Step 2 mapping table: compare old vs new path lowercased — if they're equal but the raw paths differ, mark the row as case-only.
- The temporary name must differ by more than case (add `.tmp`), otherwise the same collision reoccurs.
### Flat components without sub-components
Leave flat components flat - do NOT introduce subdirs unless the component contains internal utility-components (files that render JSX but are not public sub-components). If such a utility-component exists, move it to its own subdir following the canonical naming.
Example: `button/Button.tsx` has no other sub-components, so should stay in root.
### `parts/` directory
If the component uses a `parts/` dir, break it up into separate sub-component dirs.
### Internal sub-components
Sub-components that are implementation details (not exported publicly) use the same dir/file naming as public ones (see Naming Rules) with an `Internal` suffix, but must NOT be exported from `index.ts`:
```
backdrop/
└── DialogBackdropInternal.tsx ← internal, NOT exported from index.ts
```
### Nested subcomponents
Do NOT nest subcomponent dirs inside other subcomponent dirs. All sub-component dirs must be direct children of the component root:
```
# Wrong
dialog/
└── header/
├── DialogHeader.tsx
└── title/
└── DialogTitle.tsx
# Correct
dialog/
├── header/
│ └── DialogHeader.tsx
└── title/
└── DialogTitle.tsx
```
## Constraints
- **No breaking changes.** Public export names and prop shapes must be identical after restructuring.
- **Named exports only.** No `default` exports; group named exports at the bottom of each file (values first, then types).
- **Compound roots use `Object.assign`.** No `as <Component>Component` cast or `<Component>Component` interface — it breaks `yarn docgen:meta` prop extraction.
- **React 17 compatible.** No React 18/19-only APIs. Import React explicitly in `.tsx` files.
- Keep JSDoc on public props and components. The component-level JSDoc block sits directly above the `Object.assign` call; sub-component `@see` tags go on the keys inside the `Object.assign` object literal.
Turn on Cache Components in a Next.js app and resolve the blocking routes it
surfaces. Use when the user wants to enable, adopt, or migrate to Cache
Components, flip the `cacheComponents` flag, work through a flood of
blocking-prerender / instant validation errors, run the
`cache-components-instant-false` codemod, or decide between opting routes out
with `export const instant = false` and fixing them in place.
---
# next-cache-components-adoption
Enable Cache Components on an app and walk it to a passing build. This skill sequences the work; per-error recipes live in the dev overlay fix cards and the build's terminal output. The [migrating to Cache Components guide](https://nextjs.org/docs/app/guides/migrating-to-cache-components) is the canonical reference for the concepts and per-API recipes this skill applies — consult it whenever the skill steps reference a pattern (`"use cache"`, `cacheLife`, `<Suspense>` placement, etc.) and you want the full explanation.
## requires
- **App Router project.** Cache Components is an App Router feature; `cacheComponents: true` does nothing for `pages/` routes. If the project has a `pages/` or `src/pages/` tree but no `app/` or `src/app/` tree, stop and tell the user — Pages → App migration is its own project, not part of this skill. A hybrid app (both `pages/` and `app/`) is fine: the flag affects the `app/` routes; `pages/` routes are unaffected and don't need opt-outs.
- **Next.js 16.3 or later.** That release is where the pieces this skill relies on land: top-level `cacheComponents`, `export const instant`, the dev-overlay instant-navigation validation warnings, and the `cache-components-instant-false` codemod. If `next --version` reports below 16.3, upgrade first:
-`npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods.
- Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover.
- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. **Translate, don't delete.** Each export encodes behavior the route needs to keep doing; migrate each one to its Cache Components equivalent via the [migration guide's per-key sections](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components). If a value can't be cleanly translated yet, leave a `// TODO: Cache Components adoption — restore revalidate = 3600` comment so the loop picks it up. The `cache-components-instant-false` codemod does not touch these.
- **`experimental.dynamicIO` is fatal.** It was renamed to top-level `cacheComponents` and the old key now aborts before any build can run — remove it (or replace with `cacheComponents: true`) first. `experimental.useCache` is still accepted as a deprecated alias; redundant once `cacheComponents: true` is set, so remove it for clarity.
### notes
- **No passing baseline before the flag.** If the app already uses `"use cache"`, the pre-flag build errors with `please enable the feature flag cacheComponents`. Enabling the flag is the first thing you do (in Incremental, before the codemod; in Direct, before fixing routes) — not a thing to do _after_ getting a passing build. Note this in your starting summary so it doesn't read as a regression.
- **Offline docs.** Guide links have offline copies under `node_modules/next/dist/docs/` (bundled since Next.js 16.2), with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/migrating-to-cache-components.md`). If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled.
- **Older versions without bundled docs.** Suggest `npx @next/codemod@latest agents-md` to the user before starting: it downloads a version-matched copy to `.next-docs/` and writes an index into `AGENTS.md` / `CLAUDE.md`. It touches files in their repo, so ask first and run it only if they want it.
## the shape of the work
There's one loop: walk the route tree top-down, one feature at a time, adopting each route against `next dev` + a browser. The build is a final check for each feature, not the working surface.
The choice in step 1 is whether to opt every route out of validation first or fix routes as you go. Either way the loop is the same:
- **With a quiet pre-step (Incremental).** Run the codemod to opt every page and layout out of validation. Once you've also fixed what the codemod can't (sync-IO calls, leftover `revalidate`/`dynamic`/`fetchCache` exports), the build passes; you ship that as its own PR and then start the loop — removing one opt-out at a time and adopting that route. This splits the work into small, reviewable PRs.
- **Without (Direct).** Enable `cacheComponents` and start the loop on whatever the build flags first. Same loop, but every fix sits on one branch until adoption is complete.
In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature. Expect to spend most of the time in the loop, not in the pre-step.
## background
`cacheComponents: true` requires every route to be prerenderable. A route that reads request-time data outside `<Suspense>` is "blocking" and fails the build. `export const instant = false` marks a route as allowed to block, which clears it in both dev and build; on a layout it covers the whole subtree during the build, but client navigations still validate each descendant segment on its own. Reads wrapped in a [`"use cache"`](https://nextjs.org/docs/app/api-reference/directives/use-cache) function count as cache boundaries, not blocking reads.
Three classes of blocker come up, usually in this order:
1.**Request-time reads** (`cookies()`, `headers()`, `await params`, `await searchParams`). All four block when awaited at the top of a page or layout. `params` and `searchParams` often get missed because they're not framed as "request data" the way cookies and headers are. The fix is to push the read into a `<Suspense>`-wrapped child — and for `params`/`searchParams`, forward the promise into the child and await it there; don't `await` at the page top.
2.**Sync-IO at module/render time** (`new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()`). These fail the build even with `instant = false` — the opt-out doesn't suppress them. If they're in a shared layout, they block every route under it. The codemod can't fix them; you have to translate each one by hand before the build can pass (see the [incremental pre-step](#incremental)). Grep the whole repo for these calls before running anything else.
3.**`"use cache"` files that read request data.** A file with a top-level `"use cache"` directive can't export `instant`; combining the two errors with `Only async functions are allowed to be exported in a "use cache" file.`, which means the directive was wrong for that route. Remove it before running the codemod.
## working surfaces
### finding blocking routes
Prefer `next dev` over `next build` while you work.
- **`next dev`** — the working surface. Visit a route; its blocking errors surface in the dev overlay with full stack traces and fix cards linking the per-error docs. Work one route at a time — errors don't accumulate in one place. The route itself still returns HTTP 200, so read the overlay (or `.next-dev.log`), not status codes. A cleared overlay is one half of calling a route clean — the other half is browser verification (see [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time)) and a passing build for that route.
- **`next build`** — detection only. The build is `next dev`'s authoritative check, not its replacement. Use it as the last gate on each feature in the loop (a passing build is part of the per-route success bar) and as the final verification across the whole app. In Incremental, the build also confirms the pre-step (codemod opted every route out, no shared layout still has a sync-IO blocker) before you ship that PR. Don't reach for the build instead of the dev loop while you're working a route — a passing compile doesn't tell you what ended up in the static shell and what streamed. By default the build stops at the first blocking route, so it's also poor for sizing the work. Two flags help when iterating: `--debug-build-paths` builds only the routes you name (comma-separated glob patterns of file paths relative to the project root, e.g. `--debug-build-paths="app/admin/**/page.tsx"` — not URL paths; `--debug-build-paths="app/(marketing)/about/page.tsx"` — not `/about`; `--debug-build-paths="app/admin"` matches nothing and silently builds zero routes), and `--debug-prerender` disables the early exit so the build continues past the first prerender failure, reports every blocking route, and prints a fuller stack trace that names the originating file and line.
Every blocking error has a docs page — open it. Both the dev overlay and the build terminal print a `https://nextjs.org/docs/messages/<slug>` link with each error. That page is the canonical recipe for the fix; the inline message is a summary. Fetch the link for every distinct error you encounter, even if you think you know the pattern — the recipes evolve, and the same error class can have different correct fixes depending on what the route reads. Don't improvise from the inline message alone. (`/docs/messages/*` pages aren't bundled offline; if you have no network, fall back to the per-API guides under `node_modules/next/dist/docs/` and note the limitation when you report back.)
### verifying each fix at runtime
A passing build or a cleared overlay isn't proof the route actually behaves — Cache Components is a runtime concern (a static shell with streamed data). Verify after every fix, not only at the end.
In preference order:
1.**[`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) — strongly preferred.** Cross-checks `/_next/mcp` against the live browser via `agent-browser` and surfaces both compile and runtime issues in one pass. The diagnostics (React tree, suspense boundaries, console + network) are richer than poking at `next dev` by hand.
Install it before starting the loop. Don't wait until you hit something `next dev` alone can't explain. Run:
The skill requires `agent-browser >= 0.27.0` and walks you through it.
**Requires Turbopack.** If `package.json`'s `dev` script passes `--webpack`, flag it to the user and ask whether there's a reason to stay on webpack. If not, switch to Turbopack (the Next.js 16.3+ default). If they want to keep webpack, skip this install and use the [build-only loop](#the-loop-build-only-fallback) instead.
You don't need permission to install `next-dev-loop` itself. It's a tool, like installing a dev dependency. If a user is present, briefly tell them you're installing it for verification. In a non-interactive run (CI, dashboard, sandbox), install it without asking — "can't prompt the user" is not a reason to skip. The only legitimate skip is a real technical blocker: no network, no npm, read-only filesystem, a stated no-new-deps policy, or a webpack-only dev script. If you skip, name the specific blocker in your final report.
2. **A browser you can drive yourself.** Playwright, `agent-browser` directly, any browser-automation tool. Use only when `next-dev-loop` is genuinely blocked. You'll miss the framework-side checks (`/_next/mcp`), so DOM assertions alone don't catch every regression — be more cautious about what you call "verified."
3. **Build-only.** If you can't run a dev server at all, the build is your only signal. `○ (Static)` routes with no `<Suspense>` are fully verified by the build (nothing streamed to test). `◐ (Partial Prerender)` routes are only shell-verified — flag them when you report back.
4. **No tooling at all.** Ask the user to run the dev server (or build) and report what they see, or commit the milestone you've reached and hand off.
## step 1: choose a strategy
Ask the user, in terms of the PRs they want, not the size of the job. Never use the internal labels (Incremental, Direct) when talking to the user — those are your own scaffolding. Ask in terms of PRs and features, e.g.: _"Do you want me to first open a PR that turns on Cache Components and opts every route out of validation, then handle the actual route adoptions feature-by-feature in follow-up PRs? Or do everything on one branch?"_ Even on a tiny app, the incremental path still has value (review-sized PR, revertible, the `// TODO: Cache Components adoption` markers double as your work queue for next session). Don't pick on their behalf.
If there's no user to ask, default to **Incremental** and document the choice.
- **Incremental** — quiet pre-step + the loop. Run the codemod to opt every page and layout out of validation, get the build passing, stop and check in with the user (see [end of the pre-step](#end-of-the-pre-step-check-in)), then enter [step 2's loop](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time) and ship each feature as a follow-up PR.
- **Direct** — skip the pre-step. Enable `cacheComponents` and go straight to [step 2's loop](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time); the build's blocking routes are the work queue.
### incremental
Before invoking the codemod, fix the two classes of blocker it can't.
1. **Sync-IO at module/render time.** Grep the whole repo for `new Date()`, `Date.now()`, `Math.random()`, and `crypto.randomUUID()` (not only `app/**/layout.{js,jsx,ts,tsx}` — the read might live in any component imported by a layout). Unblock each match with the `await connection()` + `<Suspense>` fix from its `blocking-prerender-*` error card: it defers the value to request time, exactly as it behaved before the migration, so it needs no product decision. Add this exact comment on the line above the `await connection()`:
```tsx
// TODO: Cache Components adoption. Added to unblock the build: remove this connection() to re-trigger the error and review the fix options.
```
It shares the `TODO: Cache Components adoption` prefix with the comments the codemod writes, so the check-in grep finds both. Removing the `await connection()` makes the error fire again with its fix cards — the same motion as removing an opt-out in the loop.
2. **Incompatible segment configs.** Grep for `^export const (revalidate|dynamic|fetchCache)` across `app/` and translate per the `requires` note above. The codemod does not touch them; leaving them in place fails the build after the codemod.
The codemod refuses to run on a dirty working tree. Commit or stash unrelated work first, or pass `--force` to let its edits land alongside your WIP. Common false positive: if you recently upgraded Next.js, `package.json` and the lockfile will already be dirty — commit those first.
Use the `@canary` channel, not `@latest`. The `cache-components-instant-false` transform isn't in the stable `@next/codemod` release; `@next/codemod@latest` errors with `Invalid transform choice`.
Inserts `export const instant = false` (with a `// TODO: Cache Components adoption` comment) into every `app/**/{page,layout,default}` file, skipping files that already declare `instant` and any module marked `"use client"` or `"use server"`. Then set `cacheComponents: true`. The TODO comments are the work queue for the loop.
If the codemod isn't available (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand: for every `app/**/{page,layout,default}.{js,jsx,ts,tsx}` that isn't `"use client"` or `"use server"` and doesn't already declare `instant`, insert this after the imports:
```ts
// TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.
The codemod opts every segment out, not only the root, on purpose. Resolution is top-down, first-explicit-config-wins: the highest `instant = false` decides the whole subtree. With an opt-out on every segment, removing one segment's opt-out validates only that segment; descendants keep their own opt-outs and stay passing. If only the root were opted out, removing it would re-arm validation for the entire app at once.
Because the highest opt-out wins, remove them top-down (root layout first, then descend). Removing a leaf's opt-out does nothing while an ancestor still holds one.
Confirm the pre-step with `next build`. The build is the proof, not the codemod run — a shared layout that calls `new Date()` / `Math.random()` directly still fails regardless of the opt-out (see [background](#background)).
After the build passes, confirm the root layout got an opt-out (`grep -n "export const instant" app/layout.*`). The root layout renders every route, including framework routes like `/_not-found`, so if it was missed, add `export const instant = false` to it by hand.
Synthetic routes like `/_not-found` have no user file — when they block, fix the root layout's opt-out, not the synthetic route. Client Components (`"use client"`) get no opt-out (it's a build error — `E1344` — to export `instant` from them) and rarely block on their own; when a client route blocks, fix the server-side data in its ancestor layout.
### end of the pre-step: check in
Incremental only. Stop here before starting step 2 — the pre-step is the shippable PR. Talk to the user in their language; don't say "Incremental" or other internal labels; talk about adoption, PRs, and what the app does now. Tell them:
- What you did: turned on Cache Components, ran the codemod that opts every page and layout out of the new validation (or did it by hand), fixed any blockers the codemod can't (list them), confirmed the build passes.
- What changed: every page and layout in `app/` now exports `instant = false` with a `// TODO: Cache Components adoption` comment, except client components and any that already had an `instant` export.
- What to sanity-check: the diff is mostly mechanical (new exports + comments). The build passes. Routes still behave exactly as they did before — the opt-outs preserve current behavior; no rendering changes yet.
- The question: "Want to open this as its own PR before we start adopting Cache Components route by route? Or keep going on this branch?" Wait for the answer.
Moving to step 2 without checking in defeats the point of taking the incremental path.
### direct
Set `cacheComponents: true` and move to [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time). The build's blocking routes are the work queue.
## step 2: the inner loop, remove opt-outs one feature at a time
A "feature" is a single product surface — `app/settings/profile/**`, `app/posts/[slug]/**` — not a whole top-level app like `app/dashboard/**`. Finish one end-to-end before starting the next.
Within a feature, walk top-down (layouts before pages, root layout first). Removing a layout's opt-out before its descendants exposes the layout's own blocking reads. (Direct: there are no opt-outs to remove — fix each failing route; if a hand-written opt-out on an ancestor shadows it, remove that first.)
A passing build mid-walk doesn't mean the layout is clean. Removing a layout's opt-out while its descendant pages still have theirs keeps the build passing — each page shadows the inherited validation. The layout's actual blocking reads only surface once nothing below it shadows them. Don't call a feature done at the layout boundary.
Use the **with-a-browser** loop unless a browser is genuinely unreachable. The [`next-dev-loop`](#verifying-each-fix-at-runtime) skill is the source of truth for what counts as "browser available" and how to install it.
### the loop, with a browser (preferred)
Per route:
- Remove the opt-out (Incremental) or target the failing route (Direct).
- Reload in dev. Overlay clean? Skip to verify. Overlay still red? Fix.
- Fix — fetch the docs page linked from the error (`https://nextjs.org/docs/messages/<slug>`), apply the recipe from there. The inline overlay text is a summary; the docs page is the source of truth.
- Verify in the browser. Confirm the visible content on first paint is what you intended in the shell — not stuck on a fallback, not silently streaming everything out of an empty shell.
- Re-check siblings if the fix touched shared code (a layout, a sidebar component). A shared-shell change can fix the route you're on and break a sibling.
### the loop, build-only (fallback)
Used when there's no way to drive a browser — CI, sandbox, the user has no `next dev` running and you can't start one. Weaker signal: confirms the build passes and the route prerenders, but not what ended up in the static shell vs streamed.
Per route:
- Remove the opt-out (Incremental) or target the failing route (Direct).
- Rebuild with `--debug-build-paths app/<route>/**` (only that route) or `--debug-prerender` (full build, but past the first failure). Route passing? Move on. Still blocking? Fix.
- Fix — fetch the docs page linked from the error (`https://nextjs.org/docs/messages/<slug>`), apply the recipe from there.
- Re-check siblings if the fix touched shared code.
- Flag the route as build-only-verified when you hand the feature off. Each `◐` route still needs a browser pass before the feature is done.
### loop notes
- The [three blocker classes from background](#background) often get missed when fixing in place. Caching a downstream fetch (`getThing(id)`) doesn't clear an `await params` at the top of the page body — push the param promise into the `<Suspense>`-wrapped child.
- Ambiguous calls are user check-ins, not agent judgment. When you're not sure which fix fits, the blocking code looks security-sensitive, or the user might want to keep the route blocking on purpose — read [references/per-page-decisions.md](./references/per-page-decisions.md) before editing. Show the route while you ask: the `next-dev-loop` session runs the browser headed, so drive to the page and leave it on screen so the user is looking at the thing they're deciding about, with a screenshot as the fallback when a headed browser isn't possible. "Should this stay blocking?" is much easier to answer while looking at the page than at a file path.
- Don't narrate the refactor with comments. The only comment the codemod (or you) should leave is `// TODO: Cache Components adoption` on opt-outs, and the user's existing comments. Don't annotate every `<Suspense>` boundary or `"use cache"` call with what it does — the code says that. Drop a comment only when the _why_ isn't clear from the code (e.g. a deliberate Block with a reason).
Keep a todo list of the feature's routes. When every route in the feature is clean, move to step 3.
## step 3: verify the feature
Checklist before checking in with the user:
- `next build` completes without blocking-route errors.
- No bare TODOs in the feature: `grep -rn "TODO: Cache Components adoption"` finds both the codemod's opt-out comments and the sync-IO unblocks from the pre-step. Any `instant = false` left behind is a deliberate, documented Block — comment rewritten to a reason (see [references/per-page-decisions.md](./references/per-page-decisions.md) → "when to leave a Block in place"). Any `await connection()` left behind has been reviewed and kept on purpose, not left over from the pre-step.
- Each route visited in the browser: confirm the static shell renders first and every `<Suspense>` fallback resolves to its real content. Capture both states if you can — the fallback (mid-stream) and the final paint — so you have a streaming-experience demo to show the user. Throttle the network in the browser if streaming is too fast to observe.
Then check in with the user. Same rule as the pre-step: speak their language. Don't say "feature-by-feature loop" or other internal labels; talk about the feature you adopted and what the user will see.
- What you did: which routes you touched, and the user-visible result per route (e.g. "the post page now streams the article body behind a skeleton while the layout stays static").
- What changed: opt-outs removed, fallbacks added, caching boundaries introduced.
- Show, don't tell. The `next-dev-loop` session runs the browser headed, so drive the route live for the user so they see the static shell → fallback → final content sequence in real time. If you can't drive a live browser, attach the before/after screenshots you captured instead.
- Give them the click-through: a short table of the feature's routes — the URL to open and what to look for (what renders instantly, which fallbacks appear, what streams in) — so they can verify each one themselves.
- The question: "Want to open this feature as a PR and move on to the next, or stop here?" Wait for the answer.
**Trivial features can skip the check-in.** If adopting a feature only meant removing its `// TODO: Cache Components adoption` opt-out (no `<Suspense>` added, no `'use cache'` introduced, no render order change), the user sees nothing different. Move on to the next feature without stopping; mention it in passing the next time you do check in.
When the loop has run on every feature — every remaining `instant = false` sits under a reason comment, `grep -rln "TODO: Cache Components adoption" app` returns nothing — point the user at [further reading](#further-reading) if they want to push the experience further, or stop and ship.
### route table glyphs
`ƒ` → `◐` is where adoption usually lands. `◐ (Partial Prerender)` means a static shell prerenders and the request-time content streams in — the goal state for any route that reads `cookies()`, `headers()`, `params`, or `searchParams`. Some routes legitimately stay `ƒ` when they do request-time work through a documented escape hatch (e.g. a layout that uses `await connection()`); the page is no longer _opted out_, it's genuinely dynamic. Don't remove the escape hatch only to chase a `◐`. The inverse holds: `instant = false` does not force a route to be `ƒ`. The glyph reflects what the route does at prerender time, not which validation knobs it exports.
`◐` tells you a shell exists, not what's in it. A `<Suspense>` boundary placed too high (e.g. wrapping the entire page body, or `<Suspense fallback={null}>` around the article content) pushes the visible content out of the static shell into the streamed payload; the build still reports `◐` because _some_ shell prerendered (often only `<html><body>` with framework markup). The route table can't tell you what's in the shell; a browser can. If the shell is empty and everything streams, pull the `<Suspense>` boundary down closer to the actual dynamic read.
## further reading
The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill.
- [Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation) — dev-only validation warnings the overlay raises on client navigation. Same shape as the blocking-prerender errors you cleared in step 2; the guide covers the per-warning details. Recommend it next if the user wants navigations to actually be instant (a passing build doesn't guarantee that — a `<Suspense>` above the shared layout caught the page-load case but doesn't cover client navigation).
- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it audits `<Link prefetch={true}>` calls (driven by the dev overlay's `link-prefetch-partial` warning) with the flag off first, then flips the `partialPrefetching` config. It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching).
- [Prefetching](https://nextjs.org/docs/app/guides/prefetching) and [Runtime prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching) — broader prefetching reference. Runtime prefetching extends the static shell with per-session content; reach for it when a route's shell is too thin to be useful and Partial Prefetching alone doesn't cover the gap.
- [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way.
- [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption.
Read this when a route still blocks after you remove its `instant = false` and the dev overlay's fix card isn't enough on its own. Each section here covers a judgment call the agent shouldn't make alone.
## deciding what to do with a blocking read
Read the full linked page behind the fix card — not only the inline snippet — before editing. The card unblocks the build, but the page covers the details that make the route's navigation actually instant (e.g. where to place a `<Suspense>` boundary). Don't improvise.
If you're unsure which fix fits, the right call usually depends on what this part of the page is _for_, which the code doesn't capture. Ask the user about their goal for it rather than guessing. Frame it as a product/UX question: should this content be there instantly on load, or is it fine for it to stream in a moment later? Should everyone see the same thing (cacheable) or is it per-user / per-request? Tie the technical fix to that answer (cache it, wrap it in `<Suspense>`, or keep it request-time), so they're deciding the experience, not the API.
## security gates and other code you can't infer
If the blocking code looks like it's there for a _reason you can't infer_ — a security gate at the page top (`await verifyAccess()`, an auth redirect, a feature-flag check) where moving it inside `<Suspense>` would change what the code guarantees — stop and ask the user before refactoring. The build error wants `<Suspense>`, but wrapping a gate in `<Suspense>` defeats the gate. Only the person who wrote it knows whether to keep the route blocking (`instant = false` as a documented Block), restructure the page so the gate runs differently, or move the check to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy).
If _every_ route under a layout is gated this way, a documented Block on the layout is the correct end state. Moving the gate to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) is the architectural fix, not a Cache Components one, and that's a follow-up rather than something to hold the migration on.
For the broader picture, read the [Authentication guide](https://nextjs.org/docs/app/guides/authentication) (where auth checks belong: Proxy for routing, Data Access Layer for data) and the [Data Access Layer section of Data Security](https://nextjs.org/docs/app/guides/data-security#data-access-layer) (centralized auth checks that compose with `'use cache'`).
If you don't know how to make a piece of code Cache Components–correct without changing what it does, ask.
## when to leave a Block in place
If a route is genuinely meant to block — it's inherently per-request with no useful static shell — or the refactor would be large and the user would rather not take it on now, that's a legitimate outcome. Keep `instant = false`, but confirm it with the user first and turn its `// TODO: Cache Components adoption` comment into a reason, e.g. `// instant = false: kept on purpose — fully request-time dashboard` or `// instant = false: deferred, refactor too large for now`.
A documented, deliberate Block is fine to leave after the migration; an undocumented leftover opt-out is not.
# Refactor patterns — push dynamic down into the shell
Each pattern is **before → after**: keep as much as possible in the prerendered shell, and wrap only genuinely per-request work in a tight `<Suspense>` (or hoist it into `use cache`). Production shapes — parallel-route slots, deferring an auth gate, client slot-routers — are in `real-app-patterns.md`.
---
## 1. Awaiting at the top → move the await into a Suspense child
The most common blocking shape. Awaiting request-time data at the top of a page/layout makes **everything below it** dynamic.
```tsx
// ❌ before — top-level await of a non-static param + uncached data
Decide per data source. Same-for-everyone & rarely-changing → cache it (it joins the shell). Per-request & must-be-fresh → leave it uncached behind a boundary.
constinventory=awaitdb.inventory.findBySlug(slug);// must be fresh
```
```tsx
// ✅ after — cache the stable one (shell), defer the fresh one (streams)
asyncfunctiongetProduct(slug: string){
"use cache";// → resolved at prerender, lands in the shell
returndb.products.findBySlug(slug);
}
<Suspensefallback={<p>Checkingavailability…</p>}>
<Inventoryparams={params}/>{/* uncached read stays here, streams in */}
</Suspense>;
```
> A bare `'use cache'` applies the `default` `cacheLife` profile. Choose freshness explicitly with `cacheLife('<profile>')` (`default` / `seconds` / `minutes` / `hours` / `days` / `weeks` / `max`) rather than shipping the default lifetime by omission.
>
> Serverless note: `use cache` is in-memory and does not persist across instances — use [`use cache: remote`](https://nextjs.org/docs/app/api-reference/directives/use-cache-remote) for a durable shell.
**Insight:** [uncached data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-dynamic).
---
## 4. Dynamic params → `generateStaticParams` (shell) or `<Suspense>` (stream)
If the set of params is enumerable, prerender them so `await params` resolves into the shell. Otherwise treat params as request-time and wrap consumers in `<Suspense>`.
```tsx
// ✅ option A — enumerate → params resolve into the shell, no Suspense needed for params
const{slug}=awaitparams;// known at build → shell-safe
// ...
}
```
```tsx
// ✅ option B — not enumerable → params is request-time; await it inside a boundary (pattern #1)
```
Root params (the dynamic segments the root layout sits inside, e.g. `app/[lang]/layout.tsx`) are readable from any Server Component via `next/root-params` without prop-drilling — but under Cache Components they must still be enumerated by `generateStaticParams` (at least one value per root param) to land in the shell, the same as any other dynamic param.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime).
---
## 5. `searchParams` → always behind `<Suspense>` (on page load)
Search params are never known at build, so awaiting them (or `useSearchParams()`) suspends on a page load. Keep the rest of the page in the shell by isolating the consumer.
```tsx
// ✅ static content stays in the shell; the search-dependent part streams
On a **client navigation** the router already has the URL, so a `useSearchParams()` consumer resolves synchronously and can appear in the prefetched shell — but you still need the boundary for the page-load path.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime) (or, via `useSearchParams` in a Client Component, [URL data in a Client Component](https://nextjs.org/docs/messages/blocking-prerender-client-hook)).
---
## 6. Non-deterministic values → `connection()` + `<Suspense>`, or cache
`Math.random()`, `Date.now()`, `crypto.randomUUID()` produce different output each run, so Cache Components makes you choose: per-request (defer) or fixed (cache).
```tsx
// ✅ per-request value: gate on connection() and wrap in Suspense
// ✅ same value for everyone: cache it so it joins the shell
asyncfunctionbuildId() {
"use cache";
returnDate.now();
}
```
**Insight:** [`Date.now()`](https://nextjs.org/docs/messages/blocking-prerender-current-time), [`Math.random()`](https://nextjs.org/docs/messages/blocking-prerender-random), or [`crypto`](https://nextjs.org/docs/messages/blocking-prerender-crypto) while prerendering.
---
## 7. Dynamic `generateMetadata` → static export, `use cache`, or a dynamic-marker for runtime data
```tsx
// ❌ before — reading request data blocks the route's metadata
exportasyncfunctiongenerateMetadata() {
constc=awaitcookies();
return{title: c.get("title")?.value};
}
```
```tsx
// ✅ option A — static
exportconstmetadata={title:"Store"};
// ✅ option B — cache the metadata (depends on external data, not runtime data)
exportasyncfunctiongenerateMetadata() {
"use cache";
return{title: awaitgetTitle()};
}
```
```tsx
// ✅ option C — metadata genuinely needs runtime data (cookies/headers):
// keep generateMetadata dynamic, and add a dynamic-marker component to the
// page so the rest of the page still prerenders into the shell.
<article>{/* static content — stays in the shell */}</article>
<Suspense>
<DynamicMarker/>
</Suspense>
</>
);
}
```
`generateViewport` is the same, except dynamic viewport blocks the **whole page**. Genuine instant fixes: a static `viewport` export, or `use cache`. The other two are dynamic-acceptance opt-outs, not instant fixes — do not treat them as a way to reach GREEN: `export const instant = false` opts the segment out of validation while the navigation still blocks, and a `<Suspense>` above the document `<body>` makes the whole route dynamic.
**Insight:** [runtime data in `generateMetadata()`](https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime).
---
## 8. Keep the LCP element in the shell
Don't bury the main heading (the LCP element) inside a boundary — it can't paint until the boundary resolves.
```tsx
// ✅ LCP outside the boundary → paints in the shell
<h1>{product.name}</h1>{/* shell (cache the name if needed) */}
A single boundary in the **root** layout passes a page-load check but leaves sibling client navigations blocking. Put a boundary **below the shared layout**.
```tsx
// app/store/layout.tsx — boundary below the /store shared layout covers
// client navs like /store/shoes → /store/hats (the root boundary does not)
Prefer per-component boundaries inside the page (patterns #1–#5) over one big layout boundary — they keep more real content in the shell and stream independently.
**Insight:** the read's own insight surfaces on the client navigation when the boundary is too high — see [where to place the boundary](https://nextjs.org/docs/messages/blocking-prerender-dynamic#choosing-where-to-place-the-boundary).
## 10. Can't push the read down? Runtime-prefetch the whole route
Patterns 1–9 grow a **static shell** by moving dynamic reads behind boundaries. Some routes resist that: an ID minted per request (`createId()`), an auth/scope resolution the whole subtree needs, a page that is _all_ dynamic by nature. The read can't move, so there is no meaningful shell to commit and the soft nav stays RED. The escape hatch is **runtime prefetching** — don't prerender a shell, run the dynamic render _in the prefetch_ so the whole route is warm before the click and the soft nav commits the real content instantly.
It has **two halves — both required**, and route config alone is RED:
```tsx
// 1. The route opts in — on EVERY leaf (page/default) segment, parallel @slots
// included, or instant validation re-triggers on the segments that lack it
// (the same all-or-nothing coupling as an `instant = false` opt-out).
exportconstprefetch='allow-runtime'
// 2. The <Link> asks for a FULL prefetch — that is what spawns the runtime
// request. A default/auto prefetch only warms the static shell.
<Linkhref={href}prefetch={true}>…</Link>
// <Link prefetch> already issues the full prefetch on hover and on viewport
// entry, so prefer it. An imperative full prefetch via router.prefetch needs
// the non-exported PrefetchKind enum, so it has no clean public form.
```
Under `instant()` the runtime entry is what commits, so the real content (not a skeleton) shows under the lock — that is the GREEN.
Gotchas (each cost real debugging time):
- **The full prefetch is mandatory.** With App Shells enabled an auto/PPR prefetch bails before the runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` / `kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's still RED, the link is doing an auto prefetch.
- **All leaf slots must agree.** `allow-runtime` on the content segment but `instant = false` (or nothing) on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry incomplete, so the lock falls back to the shell. Flip every leaf together.
- **Prefetch the canonical URL.** A link whose href 307-redirects (a `/foo` that canonicalizes to `/`) can't be prefetched — the prefetch receives the redirect, not the tree. Point the link and the prefetch at the final URL.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic data; issuing it on hover for every link (recents that point at whole chats) is wasteful. Scope `kind: 'full'` to the runtime-prefetch targets only.
- **Marker must be a committed node, not RSC bytes.** The content is often a client component, so its text isn't in the prefetch response — assert a `data-testid` that renders when the client subtree commits, not a substring of the stream.
Prefer a static shell (patterns 1–9) whenever the read can move: it's cheaper than a runtime prefetch and also covers hard load. Reach for runtime prefetching when the read genuinely can't move, or for a route that's all-dynamic by design.
**Insight:** [dynamic data during prefetching](https://nextjs.org/docs/messages/instant-link-prefetch-partial).
The rest of this skill models a single linear `layout → page` tree. Production App Router routes add **parallel routes, shared layout UI, and auth gates**, which is where most of the real static-shell work happens. These patterns bridge that gap. Read the skill's `SKILL.md` first.
## Parallel routes: each slot is its own boundary
Instant validation treats every parallel-route slot below the shared layout as an **independent** navigation boundary. Consequences:
- **Each `@slot` needs its own `<Suspense>`** around its dynamic reads; a boundary in one slot does not cover another.
- **An uncovered dynamic read in any slot blocks the whole navigation.** A perfect `@content` does not help if `@sidebar` awaits a session at the top.
- **A slot that renders `null` (e.g. `default.tsx`) is shell-safe**: it is static and performs no reads. Slots that do not re-render for this navigation cost nothing.
```
[tenant]/layout.tsx (shared: already mounted on a soft navigation; not re-rendered)
├ @sidebar → side nav ← …here too (independent boundary)
└ @header → default.tsx → null ← free
```
## Client-rendered slot routing is not part of the soft-navigation re-render
A common pattern: a stable shared layout renders `@header`/`@sidebar` through a **client** component that swaps slot content based on `usePathname()`. On a soft navigation, Next.js only re-renders the **server** segments that changed below the shared layout; a client-component subtree is not part of that re-render. So that navigation UI neither blocks the navigation nor needs a server `<Suspense>` for it; only the server segments that actually change (e.g. `@content`) matter. It does participate in an initial load (see the caveat below).
## "Instant" is not "useful shell": the empty-shell failure mode
Validation checks that a dynamic read is **guarded by a boundary**, not that the fallback is non-empty. A `<Suspense>` with no `fallback` (or `fallback={null}`) passes validation and commits instantly, but renders a **blank** shell. If a layout and its page both `await getSession()` (your auth library's request-time read) at the top under one empty-fallback boundary, the whole frame collapses to nothing while the user waits. "Validates as instant" and "good loading experience" are different goals.
> Give every boundary a real loading skeleton, and place it low so the most real content stays in the shell. A `fallback={null}` directly above `<body>` is a deliberate empty-shell opt-out; an empty fallback lower in the tree is almost always a bug.
## The responsive-skeleton mismatch: the shell must match every breakpoint
A loading skeleton that misaligns with the loaded UI is its own bug, and it usually appears on mobile. A hand-built skeleton encodes one layout; the real component is responsive and changes shape at breakpoints, so a desktop-shaped skeleton no longer lines up once the viewport is small.
A concrete shape: a list-detail view renders a list or tree in a side panel on desktop, but collapses that panel into a single dropdown or drawer on mobile (with its own loading state). A row skeleton built for the desktop panel has nothing to align with on mobile.
The fix is the same push-down as everywhere else: **share the real responsive layout between the live render and the shell render.** One responsive component renders both (its data slots show the reused `*Skeleton` in the shell and real data after the stream), so the breakpoint switch happens once, for both renders, and there is no second desktop-only skeleton to drift.
(Same hoist rule, responsive layout included.) Verify the shell at both desktop and mobile widths against the real render at the same width.
## Deferring an auth gate / top-level `await` in a layout
A top-level `await` in a layout blocks everything below it (the most common blocker; see [Runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime)). Auth gates are the most common real instance:
```tsx
// ❌ Before: the await + redirect at the top blocks the whole settings frame
constsession=awaitgetSession();// your auth library's request-time read; suspends during prerender → frame can't build
if(!session?.user)redirect(getLoginUrl());
return<Shell>{children}</Shell>;
}
```
```tsx
// ✅ After: render children unconditionally; move the gate into a Suspense child
import{Suspense}from"react";
exportdefaultfunctionSettingsLayout({children}){
return(
<Shell>
<Suspensefallback={null}>
<AuthGate/>
</Suspense>
{children}
</Shell>
);
}
asyncfunctionAuthGate() {
constsession=awaitgetSession();// the session read suspends during prerender…
if(!session?.user)redirect(getLoginUrl());// …so redirect() never runs at build time
returnnull;
}
```
The shell prerenders as if authorized (the session read suspends before `redirect()` is reached, so the redirect only happens at request time), and `{children}` is now in the shell instead of behind the gate. (`fallback={null}` is correct here: `AuthGate` renders nothing on success.)
## Initial-load shell vs soft-navigation shell
The `test-template.md` specs drive a `<Link>` click for soft navigations and `page.goto()` for initial loads. The two shells can differ for the same route:
> **The initial-load shell can show less than the soft-navigation shell when a layout above the shared boundary awaits un-enumerated `params`/`searchParams`.** An initial load re-runs every layout from the root; if a parent layout does `await props.params` and that segment has no `generateStaticParams`, the param suspends on the initial load and its whole subtree drops out of the shell. A soft navigation does not re-render that parent and already has the params. Symptom: an element present after a `<Link>` click is missing after `goto`.
To assert the soft-navigation shell, drive a real `<Link>` click (through menus if necessary). Use `page.goto()` inside `instant()` to assert the initial-load shell, or when no parent above the shared boundary awaits un-enumerated params, in which case the two coincide.
## Edge cases
- **A `React.cache` (or custom memoization) wrapper around `cookies()`/`headers()` still suspends.** Memoizing the call does not make it shell-safe: the underlying request read still returns a pending promise during prerender. Only the **`use cache`** directive, keyed on static or param inputs, puts data in the shell.
- **Playwright cannot see a `display: contents` or fragment fallback.** Such a fallback reads as hidden, so `instant()` assertions cannot `toBeVisible()` it. Give fallbacks a real wrapper element with a `data-testid`.
| **Selector matches nothing** | a guessed `getByRole('button', { name: 'Folder' })` | grep the component for the real accessible name; add a `data-testid` |
| **Conditional redirect** | the route `redirect()`s for the test user (flag/role), so the marker page is never reached | check the page's top-level branches; assert at the real destination, or pin the flag |
| **Flag / plan / role gate** | the author has the flag or plan; the test user does not | run the unlocked baseline as the test user; pin flags via the project's override mechanism |
| **Empty state** | the marker only exists when data does; the CI account is empty | pick a marker present in the empty state (a layout element such as the page header), or seed data |
| **Timeout / flake** | a slow API or transient infrastructure error | re-run; separate infrastructure flake from a real signal |
| **Streamed marker** | the marker is behind `<Suspense>`, so it is never in the shell | choose a sync shell element; verify it sits outside every `<Suspense>` |
| **Auth redirect** | unauthenticated → `/login` | confirm login succeeded before the navigation |
| **Stale deployment** | the test ran against the previous build (the URL under test still serves the prior deploy) | poll the deployment for a marker from the latest commit before trusting any verdict |
| **Hidden / off-screen** | the testid is on a hover-overlay or off-screen list item | put the marker on an always-visible node; `.filter({ visible: true }).first()` for lists |
## Worked cases
These are illustrative failures from real optimization runs; each was red for a wrong reason, and
none was an instant-navigation problem. One app's drift surface might be dominated by feature flags and
plans; another's by auth state, an empty database, or locale. The taxonomy lists every wrong
reason; the rig file's DRIFT list says which rows apply to your app.
- **Guessed selector + empty state**: the marker was a button picked by a guessed accessible name
that no element actually had, on a list page whose CI account had no rows. → checks 7, 9. Fix: a
`data-testid` on a real static-shell node.
- **Hidden marker**: the testid sat first on a `hidden sm:block` hover-overlay link, then on an
off-screen carousel card; Playwright resolved the element but reported it hidden. → check 10.
Fix: an always-visible node; for lists, `.filter({ visible: true }).first()`.
## Differential check (capture in the PR)
The strongest evidence that the RED measured the property:
```
1. on the fixed branch → GREEN
2. revert ONLY the fix (the <Suspense> push-down) → RED
3. re-apply → GREEN
4. confirm no other change moves it
```
Link the two runs (or include the toggle diff and results) in the PR description. A reviewer who
sees the differential knows the test measures the property.
## `instant()` is not a stopwatch
The test does not measure how fast a navigation is. `instant()` gates dynamic data so the content
of the static shell can be asserted; the signal is presence, not speed. Under the lock, an instant
route's shell is present, and a blocking route's content never commits, regardless of wait time.
Therefore:
- The shipped assertion is `await expect(SHELL_MARKER).toBeVisible()` under the lock. Do not add a
custom timeout or a `painted` boolean.
- A custom short timeout (e.g. `3000`) implies a race against a clock that does not exist. It adds
nothing to the verdict and invites false REDs on an instant route whose commit lands a microtask
late.
- Do not use `locator.isVisible({ timeout })` as a soft wait: Playwright deprecated and ignores
that timeout; the call returns immediately.
- "Renders for the test user" (checks 7-9) is established at authoring time with the unlocked
baseline scaffold, not by a timed assertion in the shipped test.
## `instant()` guards need no retries and no prefetch warming
An `instant()` guard is deterministic. Do not configure retries on one, and do not hover-warm to
help a prefetch land in time. Under the lock, the router initiates the route prefetch and awaits it
before committing (even for a `prefetch={false}` link, even for a route already in the prefetch
cache), so the committed shell does not depend on any prior render, hover, or menu-open prefetch.
A flaky guard has a real cause: a marker that is not a sync node of the destination's shell, a
flag/role/empty-state gap for the test user, or a genuinely blocking route. The fix is in the page or
the marker; a retry masks the regression the guard exists to catch. The only legitimate
`.hover()`/menu-open is when the trigger element itself is not in the DOM until hovered or opened.
## Silent no-op: the testing API must be exposed in the measured build
`instant()` works by setting a cookie (`next-instant-navigation-testing`) that lock code inside the
build reads. It does not throw when that lock code is absent; it only throws on nested calls or an
unknown base URL. If the build was produced without the testing API
(`experimental.exposeTestingApiInProductionBuild`), the cookie is ignored, the navigation runs
normally, and the `instant()` test passes vacuously. A green `instant()` test is only meaningful if
the lock engaged.
Two defenses; use both:
1.**Confirm the API is exposed on the target.** Wire the flag to the platform's preview/staging
condition or an explicit environment variable; the rig file records the project's spelling
(SKILL.md phases 0 and A). Do not trust a pass from a build where it is not set.
2.**Make the test self-validating**: for any route with deferred content, also assert that the
deferred content is gated under the lock, not only that the shell is present
(`test-template.md`, self-validating variant). If the lock did not engage, the content is
already present and `toHaveCount(0)` fails.
The gated half holds under the lock for both navigation types regardless of warm state: the
soft-nav client lock gates dynamic-data writes, and on an initial load the server honors the
cookie on the document request and suspends dynamic data. A vacuous pass is only possible with
a build produced WITHOUT the testing API, which defense #1 above covers.
## Determinism and the rig
- Always measure on a production build, never `next dev`; SKILL.md phase A owns this invariant and
its rationale.
- Run the RED several times; an intermittently red gate is not a gate. If it flakes, determine
whether the cause is infrastructure (transient errors) or a real race before trusting either
description:Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, Blueprints, and framework integrations such as Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and the App SDK. Use this skill whenever working with Sanity schemas, defineType or defineField, GROQ or defineQuery, content modeling, Presentation or preview setups, Sanity-powered frontend integrations, Sanity Functions, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase.
---
# Sanity Best Practices
Comprehensive best practices and integration guides for Sanity development, maintained by Sanity. Use the quick reference below to load only the one or two topic files that match the task.
## When to Apply
Reference these guidelines when:
- Setting up a new Sanity project or onboarding
- Integrating Sanity with a frontend framework (Next.js, Nuxt, Astro, Remix, SvelteKit, Hydrogen)
- Writing GROQ queries or optimizing performance
- Designing content schemas
- Implementing Visual Editing and live preview
- Working with images, Portable Text, or page builders
- Configuring Sanity Studio structure
- Setting up TypeGen for type safety
- Implementing localization
- Migrating content from other systems
- Building custom apps with the Sanity App SDK
- Managing infrastructure with Blueprints
- Automating content workflows with Sanity Functions
## Global Rules
- Let Sanity generate `_id` values for ordinary documents. Do not create deterministic UUIDs, slug-derived IDs, or legacy-system IDs when creating documents.
- Model relationships with `reference` fields, then resolve related documents with GROQ lookups, source-key fields, or returned `_id` values from created documents.
- Use explicit document IDs mainly for singleton documents controlled by Studio Structure, including localized singletons such as `homePage-en`.
## Quick Reference
### Integration Guides
-`get-started` - Interactive onboarding for new Sanity projects
-`nextjs` - Next.js App Router, Live Content API, standalone Studio
-`nuxt` - Nuxt integration with @nuxtjs/sanity
-`angular` - Angular integration with @sanity/client, signals, resource API
-`astro` - Astro integration with @sanity/astro
-`remix` - React Router / Remix integration
-`svelte` - SvelteKit integration with @sanity/svelte-loader
-`hydrogen` - Shopify Hydrogen with Sanity
-`project-structure` - Standalone Studio and monorepo patterns
-`app-sdk` - Custom applications with Sanity App SDK
-`blueprints` - Infrastructure as Code: blueprint files, stacks, plan/deploy workflow, error recovery, CI deploys
-`functions` - Automating content workflows with Sanity Functions
### Topic Guides
-`groq` - GROQ query patterns, type safety, performance optimization
-`schema` - Schema design, field definitions, validation, deprecation patterns
-`visual-editing` - Presentation Tool, Stega, overlays, live preview
-`page-builder` - Page Builder arrays, block components, live editing
-`portable-text` - Rich text rendering and custom components
-`typegen` - TypeGen configuration, workflow, type utilities
-`seo` - Metadata, sitemaps, Open Graph, JSON-LD
-`localization` - i18n patterns, document vs field-level, locale management
-`migration` - Content import overview (see also `migration-html-import`)
-`migration-html-import` - HTML to Portable Text with @portabletext/block-tools
## How to Use
Start with the single framework or topic guide that best matches the request, then read additional references only when the task crosses concerns. Use these reference files for detailed explanations and code examples:
description:Integration guide for Angular, including @sanity/client setup, data fetching with signals and resource API, Portable Text rendering, and image optimization.
---
# Angular & Sanity Integration Rules
Jump to the section that matches your Angular version or integration task instead of reading this guide straight through.
## Table of Contents
- Setup and configuration
- Client setup (service pattern)
- Data fetching patterns
- Routing
- Portable Text rendering
- Image optimization
- Modern Angular features
- SSR and prerendering
- Visual Editing
- Error handling
## 1. Setup & Configuration
Use the official template `sanity-template-angular-clean` as a starting point. It provides a monorepo structure:
Configure environment files for Sanity credentials:
```typescript
// environments/environment.ts
exportconstenvironment={
production: false,
sanity:{
projectId:"your-project-id",
dataset:"production",
apiVersion:"2025-05-01",
},
};
```
```typescript
// environments/environment.production.ts
exportconstenvironment={
production: true,
sanity:{
projectId:"your-project-id",
dataset:"production",
apiVersion:"2025-05-01",
},
};
```
> There is no Angular-specific Sanity SDK. Use `@sanity/client` directly, wrapped in an Angular service.
### TypeGen in a Monorepo
Sanity TypeGen generates TypeScript types from your schema and GROQ queries. In the Angular monorepo template, TypeGen runs from the Studio side but scans your Angular app's source files. Ensure `studio/sanity.cli.ts` points at the Angular app:
```typescript
// studio/sanity.cli.ts
import{defineCliConfig}from"sanity/cli";
exportdefaultdefineCliConfig({
typegen:{
enabled: true,
path:"../angular-app/src/**/*.ts",
generates:"../angular-app/sanity.types.ts",
},
});
```
The remaining defaults (`overloadClientMethods: true`, `schema: "schema.json"`) work as-is. Include the generated types file in `angular-app/tsconfig.json` (usually covered by `"include": ["src/**/*.ts", "sanity.types.ts"]`). See `typegen.md` for the full TypeGen workflow, git strategy, and configuration options.
## 2. Client Setup (Service Pattern)
Create an injectable service wrapping `@sanity/client` and `@sanity/image-url`:
For preview/draft content, create a second client instance with a token and `useCdn: false`. Never expose tokens in client-side bundles — use server-side rendering or a proxy endpoint for authenticated requests.
## 3. Data Fetching Patterns
### A. `resource` API (Angular 19+, Recommended)
The `resource` API works natively with promises and integrates with Angular signals:
The `resource` automatically re-fetches when `slug` changes and exposes `value()`, `isLoading()`, and `error()` signals.
> **TypeGen:** Wrapping queries in `defineQuery` enables Sanity TypeGen to infer return types automatically — no manual type imports needed. See `typegen.md` for the full workflow.
### B. `rxResource` (Observable-based)
For teams using RxJS patterns or needing operators like `retry` and `debounceTime`:
With `withComponentInputBinding()`, the `:slug` route param is automatically bound to `slug = input.required<string>()` on the component — no need to inject `ActivatedRoute`.
## 5. Portable Text Rendering
### A. `@portabletext/to-html` with Angular Pipe (Recommended)
### B. `@limitless-angular/sanity` (Community, Component-based)
For full Angular component control over each block type, the community library `@limitless-angular/sanity` provides a component-based Portable Text renderer. This is useful when you need Angular-specific interactivity within rich text blocks.
See `portable-text.md` for Portable Text schema design and serialization rules.
Sanity provides a base64 LQIP (Low Quality Image Placeholder) per image asset — but you must query it explicitly:
```groq
mainImage {
// @sanity/image-url needs these to build URLs with hotspot/crop support
asset,
hotspot,
crop,
alt,
// NgOptimizedImage needs these for placeholder and layout
"lqip": asset->metadata.lqip,
"width": asset->metadata.dimensions.width,
"height": asset->metadata.dimensions.height
}
```
Feed the LQIP directly into `NgOptimizedImage`'s `placeholder` attribute:
```html
<img
[ngSrc]="post.mainImage|sanityImage:1200"
[width]="post.mainImage.width"
[height]="post.mainImage.height"
[placeholder]="post.mainImage.lqip"
[alt]="post.mainImage.alt"
priority
/>
```
Angular applies a CSS blur to the LQIP and crossfades to the full image on load. No extra libraries needed.
> **Note:** LQIP strings are small (~200 bytes) so they're safe to inline in SSR HTML and `TransferState`. See `image.md` for the full image query patterns.
See `image.md` for image field schema patterns and hotspot/crop configuration.
## 7. Modern Angular Features
When building with Sanity, leverage these Angular 19+ features:
- **Standalone components** — Default in Angular 19. No `NgModule` boilerplate needed.
- **Signals and `resource`** — Preferred over RxJS for data fetching. Simpler, less boilerplate.
- **New control flow** — Use `@if`, `@for`, `@switch` with `@empty` for cleaner templates:
- **`inject()` function** — Preferred over constructor injection for cleaner code.
- **Zoneless change detection** — Experimental in Angular 19. Works well with signals-based data fetching since signals automatically notify the framework of changes.
## 8. SSR & Prerendering
Angular 17+ includes built-in SSR support (replacing Angular Universal):
| **Hydration** | `provideClientHydration()` preserves server-rendered DOM. The client reuses it instead of re-rendering. |
| **HTTP Transfer Cache** | Only works with Angular's `HttpClient`. Since `@sanity/client` uses its own HTTP transport, use `TransferState` manually (see below). |
| **Prerendering** | Use `getPrerenderParams` in route config to generate static pages at build time. |
### Transfer State for `@sanity/client`
Angular's built-in HTTP Transfer Cache does not cover `@sanity/client` requests. Without manual transfer, the client re-fetches every query during hydration. Add `TransferState` to the service from Section 2:
The `hashQuery` helper keeps `TransferState` keys short (SHA-256 hex) instead of embedding raw GROQ strings in the serialized HTML.
Prerendering dynamic routes:
```typescript
// app.routes.server.ts
import{RenderMode,ServerRoute}from"@angular/ssr";
exportconstserverRoutes: ServerRoute[]=[
{
path:"post/:slug",
renderMode: RenderMode.Prerender,
asyncgetPrerenderParams() {
// Fetch all slugs from Sanity at build time
constclient=createClient({
projectId:"...",
dataset:"...",
apiVersion:"...",
useCdn: true,
});
constslugs=awaitclient.fetch<string[]>(
`*[_type == "post"].slug.current`,
);
returnslugs.map((slug)=>({slug}));
},
},
{path:"**",renderMode: RenderMode.Server},
];
```
❌ **Bad:** Using `isPlatformBrowser()` in templates to conditionally render content — causes hydration mismatch.
✅ **Good:** Using `@defer` or `afterNextRender()` for browser-only code.
## 9. Visual Editing
> **Important:** Angular does not have official Sanity Visual Editing support. There is no `@sanity/visual-editing` integration, no Stega encoding, and no click-to-edit overlay for Angular applications. This is unlike Next.js, Nuxt, and SvelteKit which have first-party support.
### Preview Mode (Basic)
For draft content preview, create a separate preview client with an API token:
> **Security:** Never expose the preview token in client-side bundles. Use this pattern only with SSR where the token stays on the server, or proxy preview requests through a backend API.
### Community Visual Editing
The community library `@limitless-angular/sanity` provides experimental Visual Editing support for Angular, including overlay click-to-edit functionality. Check its documentation for current status and limitations.
## 10. Error Handling
Common errors when integrating Angular with Sanity:
| `401 Unauthorized` | Invalid or missing API token | Verify token in [Sanity Manage](https://www.sanity.io/manage). Ensure it has correct permissions. |
| `403 Forbidden` | CORS origin not allowed | Add your Angular dev/production URL to CORS origins in [Sanity Manage](https://www.sanity.io/manage). |
| `422 Invalid query` | GROQ syntax error | Test queries in Vision plugin or Sanity's GROQ playground. See `groq.md`. |
| Hydration mismatch | Conditional rendering based on platform | Use `@defer` or `afterNextRender()` instead of `isPlatformBrowser()` checks. |
| Empty response | Missing dataset or wrong `apiVersion` | Verify environment config. Use a date-based `apiVersion` (e.g., `'2025-05-01'`). |
| Images not loading | Missing `@sanity/image-url` setup | Ensure `getImageUrlBuilder` is called with a valid image reference. See `image.md`. |
For GROQ query patterns and best practices, see `groq.md`. For schema design, see `schema.md`.
description:Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices.
---
# Sanity App SDK
Build custom React applications that interact with Sanity content in real-time.
`@sanity/astro` provides the `sanity:client` virtual module. `astro-portabletext` renders Portable Text. `@sanity/image-url` builds image URLs. `groq` exports `defineQuery` for typed queries.
### Configuration (`astro.config.mjs`)
Use the official `@sanity/astro` integration. `astro.config.mjs` runs at config time before Astro's env loading, so `import.meta.env.PUBLIC_*` is not available there — use Vite's `loadEnv` to read the same `PUBLIC_` variables your pages will use.
Astro hoists `getStaticPaths()` into a separate module context. Module-scope `const` declarations in the frontmatter are NOT accessible inside it — referencing them throws `ReferenceError: <NAME> is not defined` at request time. Define queries used by `getStaticPaths` inside the function, or import them from a utility module.
```astro
---
import { sanityClient } from "sanity:client";
import { defineQuery } from "groq";
import { PortableText } from "astro-portabletext";
// Module-scope queries are fine for module-scope code…
import { PortableText } from "astro-portabletext";
const { body } = Astro.props;
---
<div class="prose">
<PortableText value={body} />
</div>
```
## 4. Image Handling
Use `@sanity/image-url` to generate optimized image URLs.
```typescript
importimageUrlBuilderfrom"@sanity/image-url";
import{sanityClient}from"sanity:client";
constbuilder=imageUrlBuilder(sanityClient);
exportfunctionurlFor(source){
returnbuilder.image(source);
}
```
## 5. Visual Editing (Live Preview)
Astro handles visual editing slightly differently depending on if you are using Hybrid or Static mode.
### Setup
Ensure `stega` is enabled in your client configuration if you want clickable overlays.
For real-time updates in the presentation tool, you typically need a React component wrapper (since Astro components don't re-render on the client) or use the View Transitions API with a loader.
_Note: The `@sanity/astro` integration is evolving. Check the latest docs for "Visual Editing" support._
description:Rules for Sanity Blueprints, the Infrastructure as Code solution for managing Sanity resources declaratively. Covers blueprint files, stacks and scope, the plan/deploy workflow, deletion policies, error recovery, and CI deployment.
---
# Sanity Blueprints
Sanity's Infrastructure as Code (IaC) solution. Define resources declaratively in `sanity.blueprint.ts`, track the file in version control, preview changes with `plan`, apply them with `deploy`. Blueprints is the only way to deploy Sanity Functions.
The CLI is built for unattended use: every command accepts `--json`, all IDs can be supplied via flags or environment variables, and `--help` on any command is the authoritative reference. Always run via `npx sanity@latest blueprints <command>`.
| **Blueprint file** | `sanity.blueprint.ts`, the declarative manifest of desired resources. The source of truth: if it's not in the file, it's not deployed |
| **Resource** | One managed thing (function, CORS origin, webhook, role, robot token, dataset). Identified by a unique `name` |
| **Definer** | Typed helper from `@sanity/blueprints` (e.g. `defineCorsOrigin`) that declares a resource and validates input as you write |
| **Stack** | The deployed counterpart of the file: the live resources on Sanity's side. One file can deploy to many Stacks (e.g. `staging`, `production`) |
| **Config file** | `.sanity/blueprint.config.json`, links the local file to a Stack and records scope. Gitignored by `init` automatically; not secret |
| **Scope** | `project` or `organization`. Determines which resource types a Stack can manage |
### Key behaviors
- Resources are matched **by `name`** between file and Stack. `plan` buckets every resource as create, update, no-op, or destroy.
- **There is no rename.** Changing a resource's `name` destroys the old resource and creates a new one under the new name.
- **Removal is destruction.** Deleting a resource from the file destroys it on the next deploy (subject to its deletion policy). `blueprints destroy` is equivalent to deploying an empty file.
- Deploys apply in dependency order. On partial failure, completed changes are rolled back best-effort in reverse order. Rollback is **not atomic**; some actions may be irreversible.
| `sanity blueprints deploy` | Infrastructure resources defined in `sanity.blueprint.ts` |
| `sanity schema deploy` | Studio schema to Content Lake (for MCP and editor access) |
| `sanity deploy` | The Studio application to Sanity hosting |
## The Blueprint File
`sanity.blueprint.ts` default-exports a `defineBlueprint` call. TypeScript is idiomatic (definers validate inline and provide editor help); `.js` and hand-written `.json` also work but new files should use TypeScript.
-`resources` is an array of definer outputs. Every resource requires a unique `name`.
-`values` holds reusable file-level constants (always strings), referenced as `$.values.<key>`.
- Resource-type-specific fields are documented in the typed reference: https://reference.sanity.io/_sanity/blueprints/
### Discovering available resources
The definers exported by `@sanity/blueprints` are the canonical list of what Blueprints can manage. Discover them by inspecting the package's exports (every `define*` function is a resource definer) and reading their TSDoc for fields, defaults, and examples, or browse the typed reference at https://reference.sanity.io/_sanity/blueprints/. Do not rely on a memorized list; new definers ship additively.
When the Stack is organization-scoped, project-contained resources (CORS origins, webhooks, datasets, functions) must set their `project` field explicitly.
For the semantics of an individual resource type (what a CORS origin, webhook, or role does and which values make sense), consult that feature's own Sanity documentation. This reference covers how Blueprints manages resources, not the resources themselves.
## References and the Resource Graph
Resources reference values and each other with `$` string paths:
| `$.values.<key>` | A constant from the `values` block | At file evaluation |
| `$.resources.<name>` | Another resource in the file | At deploy time; creates a dependency edge |
| `$.resources.<name>.id` | The generated ID of that resource, as a string | At deploy time |
Blueprints builds the dependency graph from these references and orders work so dependencies are created first. Unresolved references and cycles fail validation before anything is deployed. To force ordering without a data reference, use `lifecycle: {dependsOn: '$.resources.<name>'}`.
### Referencing an existing project
The recommended pattern is an environment variable surfaced through `values`:
```typescript
import"dotenv/config";
// the CLI does not read .env files itself
exportdefaultdefineBlueprint({
values:{
projectId: process.env.SANITY_PROJECT_ID??"",
},
resources:[
defineCorsOrigin({
name:"app-cors",
project:"$.values.projectId",
origin:"https://www.example.com",
}),
],
});
```
## Deletion Policies
Set `lifecycle.deletionPolicy` on a resource to control what removal means:
| `protect` | **Deployment fails** | **Deployment fails** | Resource is never updated or destroyed |
Use `protect` for production-critical stateful resources:
```typescript
defineDataset({
name:"production",
lifecycle:{deletionPolicy:"protect"},
});
```
A deletion-policy violation is a pre-deploy validation failure, not a partial deploy: fix the file (or consciously change the policy) and re-run `plan`.
## Stacks, Environments, and Scope
One blueprint file deploys to many Stacks. The `--stack <name-or-id>` flag (on `deploy`, `plan`, `info`, `logs`) selects the target; without it, the Stack recorded in `.sanity/blueprint.config.json` is used.
- **Only `init` creates Stacks.** `--stack` never creates a Stack on a miss, so CI cannot accidentally provision infrastructure. Re-run `blueprints init .` to add another Stack for the same file.
- Accounts are currently limited to **three Stacks per project scope**.
- For per-environment differences, read an environment variable inside the file (e.g. `process.env.SANITY_ENV`) and deploy with the env var and `--stack` together. `SANITY_ENV` is a convention your file reads, not a CLI feature.
| Stored in config as | `projectId` | `organizationId` |
| Resources default to | The project | Nothing; each project-contained resource must name its `project` |
| Org-scoped resource types | Not available | Available |
Organization scope is recommended for anything beyond a single-project experiment. Convert an existing Stack with `blueprints promote` (safe, idempotent, additive, but **one-way**; requires `organization-update` admin permission).
| `mint-deploy-token` | Mint a long-lived robot token for CI deploys (`--print` for shell pipelines) |
Running `npx sanity@latest blueprints --help` will show the latest available commands.
Use `npx sanity@latest blueprints <command> --help` to see options and examples for a specific command.
Function scaffolding, local testing, secrets, and function logs live under `npx sanity@latest functions <command>` (`add`, `dev`, `test`, `env add|list|remove`, `logs`). See the `functions` reference.
npx sanity@latest blueprints plan # preview the diff, catch validation errors safely
npx sanity@latest blueprints deploy # apply
npx sanity@latest blueprints info # verify deployed state
```
Always run `plan` before `deploy`. It is read-only and surfaces validation errors, reference problems, scope mismatches, and deletion-policy violations without touching anything.
### Exit codes
`deploy` and `destroy` return: **`0`** succeeded, **`2`** failed, **`75`** accepted but completion could not be confirmed.
**✅ Correct — treat exit 75 as unknown, then verify:**
```bash
npx sanity@latest blueprints deploy ||status=$?
if["${status:-0}" -eq 75];then
npx sanity@latest blueprints info --json # confirm actual Stack state
fi
```
**❌ Incorrect — treating any nonzero exit as failure and retrying.** Exit 75 means the deployment was accepted; blindly redeploying can queue duplicate operations (deploy also refuses to start while another operation is in progress).
### Scope resolution and environment variables
For `organizationId`, `projectId`, and `stackId`, the first source that resolves wins: CLI flags → environment variables → the blueprint file module → the local config file.
| Validation error (missing/invalid field, malformed file) | Before deploy | Fix the file at the reported resource path |
| Unresolved reference or dependency cycle | Before deploy | Fix the `$.resources.<name>` reference or break the cycle |
| Scope mismatch (org-scoped resource on project Stack, or no resolvable `project`) | Before deploy | Set the resource's `project` field, or `promote` the Stack |
| Deletion-policy violation (change would remove a `retain`/`protect` resource) | Before deploy | Restore the resource, or deliberately relax its policy |
| Execution failure (underlying service rejected the change) | During deploy | Read `blueprints logs`, fix the cause, redeploy |
Recovery procedure after any failure:
1. Read the error message and resource path (`blueprints logs` re-reads failure output).
2. Fix the blueprint file (or the external condition).
3. Run `blueprints plan` to confirm the corrected diff.
4. Run `blueprints deploy` again.
Retrying without changing anything only makes sense for transient execution failures; the first four failure kinds are deterministic and will fail identically until the file changes. If local and remote state seem inconsistent, run `blueprints doctor` (and `doctor --fix`) before attempting anything destructive. After a partial-deploy rollback, `blueprints info` shows what is actually live; resources owned by other Stacks are never touched by rollback.
## CI/CD
Recommended workflow: `plan` on pull requests, `deploy` on merge to main. Official GitHub Actions: https://github.com/sanity-io/blueprints-actions
The minted robot token has exactly the role needed to plan, deploy, and destroy (`blueprints-deployer` for project scope, `blueprints-deployer-robot` for organization scope) and is revocable under Robots in Sanity Manage. Store it as a CI secret, then in the pipeline:
```bash
exportSANITY_AUTH_TOKEN=<the minted token>
exportSANITY_ORGANIZATION_ID=<orgId> # or SANITY_PROJECT_ID
The function directory name must match the `name` in the blueprint config. Each function exports a `handler` from its `index.ts` (or `index.js`).
---
## Step-by-step: Creating a Function
### 1. Initialize a Blueprint
```bash
npx sanity@latest blueprints init . \
--type ts \
--stack-name production \
--project-id <your-project-id>
```
This creates `sanity.blueprint.ts` and `.sanity/blueprint.config.json` (gitignored automatically; it links your Blueprint to a Stack and is not secret).
- Max timeout: 900s. Larger functions = slower cold starts.
### Cost
Cost = invocations × (memory GB × duration seconds). Default is 1GB memory. A function averaging 1GB and 40ms duration can run ~500k invocations within 20K GB-seconds. [Monitor usage at the organization level](https://www.sanity.io/manage).
if(!URL)thrownewError("DEPLOY_HOOK_URL is not set");
awaitfetch(URL);
console.log("Deploy hook triggered");
});
```
Set the env var: `npx sanity@latest functions env add deploy-hook DEPLOY_HOOK_URL https://...`
### Set a timestamp on first publish
Uses the same pattern as the step-by-step example above. The key insight: the `!defined(firstPublished)` GROQ filter prevents re-triggering after the field is set. The `setIfMissing` patch is a redundant safety net.
The GROQ filter ensures only English documents trigger the function. The translated document gets a different `language` value, preventing recursive triggers.
Let Sanity assign the translated document's `_id` for ordinary localized content. To find or update translations later, query by language, slug, or translation metadata instead of deriving IDs from the source document. Reserve explicit `targetDocument._id` values for singleton-style targets.
Mint a long-lived deploy token with `npx sanity@latest blueprints mint-deploy-token` (creates a robot token with the role required to plan, deploy, and destroy) and store it as a CI secret. Recommended workflow: `blueprints plan` on pull requests, `blueprints deploy` on merge to main. See the `blueprints` reference for CI environment variables and exit codes.
description:Use these rules when users ask to 'Get started with Sanity' or need help setting up a new Sanity project.
---
# Sanity Getting Started Guide
## Overview
Getting started with Sanity follows three phases:
1.**Studio & Schema** — Set up Sanity Studio and define your content model
2.**Content** — Import existing content or generate placeholder content via MCP
3.**Frontend** — Integrate with your application (framework-specific)
## Communication Style
**Keep responses succinct:**
- Tell the user what you did: "Created post schema with title, body, and slug"
- Ask direct questions: "What kind of content are you building?"
- Avoid verbose explanations of what you're about to do
- Don't explain every step unless the user asks
**Examples:**
- **Good**: "Schema deployed. Ready to add some content?"
- **Bad**: "I'm going to deploy your schema to the Content Lake so that the MCP server can recognize your new document types. This will allow..."
---
## Get Started with Sanity (Interactive Guide)
**TRIGGER PHRASE:** When the user says "Get started with Sanity" or similar, follow these steps.
**Before starting:** Let the user know they can pause and resume anytime by saying "Continue Sanity setup".
**RESUME TRIGGER:** If the user says "Continue Sanity setup", check what's already configured:
- Does `sanity.config.ts` exist (typically in a `studio/` folder)? → Studio is set up
- Are there files in `schemaTypes/`? → Schema exists
- Is there a frontend framework in `package.json`? → May need integration
Resume from where they left off.
---
## Phase 1: Studio & Schema
### Step 1: Check for Existing Studio
**Look for `sanity.config.ts` or `sanity.cli.ts` across the workspace** — in the recommended side-by-side layout the Studio lives in its own folder (`studio/`, or `studio-*` when created by the Sanity onboarding flow) next to the app folder:
**If NO Studio found:**
- Ask: "Want to create a new Sanity Studio?"
- If yes, run from the repo root — **not inside a Next.js app folder**, where the CLI would switch to its embedded flow (not recommended):
```bash
npm create sanity@latest -- --template clean --typescript --output-path studio
```
- This creates a standalone Studio in `studio/`, alongside your app folder (see `project-structure.md`)
**If Studio exists:**
- Read the config to get `projectId` and `dataset`
- Proceed to Step 2
### Step 2: Check for Existing Schema
**Look in `schemaTypes/`, `schemas/`, or `src/sanity/schemaTypes/`:**
**If NO schema found:**
- Ask: "What kind of content are you building? (e.g., Blog, E-commerce, Portfolio)"
- Create appropriate schema types based on their answer
- See `schema.md` for patterns
**If schema exists:**
- Show them what you found
- Ask: "Want to add more content types or modify existing ones?"
claude mcp add Sanity -t http https://mcp.sanity.io --scope user
```
**VS Code:** Command Palette → `MCP: Open User Configuration` → add:
```json
{
"servers": {
"Sanity": {
"type": "http",
"url": "https://mcp.sanity.io"
}
}
}
```
---
## Phase 3: Frontend Integration
### Client Bundle Warning (Vite-based frameworks)
React Router, SvelteKit, Astro, and Nuxt all run on Vite. **Any module imported by a client component will be bundled to the browser.** `process.env` doesn't exist there.
For publishable values (`projectId`, `dataset`, `apiVersion`, public studio URL), use the framework's client-safe env mechanism:
- React Router / Remix: `import.meta.env.VITE_*`
- SvelteKit: `$env/static/public`
- Astro: `import.meta.env.PUBLIC_*`
- Nuxt: `useRuntimeConfig().public`
For secrets (read tokens, webhook secrets), read `process.env.*` (or the server equivalent) **only from server-only modules** — `.server.ts`, route handlers, API endpoints. Don't centralize them in a shared `env.ts` that anything else imports.
This trap is invisible at SSR — the page renders fine on first load. It surfaces on client-side route transitions, when a lazy-loaded route chunk pulls a shared client/image module into the browser.
### Step 1: Find the App and Detect Framework
The working directory is often a parent folder with the Studio and the app side by side. Identify the app folder first: a sibling of the Studio folder with its own `package.json` (commonly `web/`). If several candidates exist, ask the user which app to integrate — never assume.
`next-sanity` is the official Sanity toolkit for Next.js. It bundles `@sanity/client`, `groq` (with `defineQuery`), and `@portabletext/react`, plus dedicated subpath exports for Next.js-specific features:
Don't also install `@sanity/client`, `@portabletext/react`, or `groq` directly — import them from `next-sanity`. `@sanity/image-url` is not bundled (yet), so add it separately.
<a href={`/${(post.slug as { current?: string })?.current}`}>{post.title as string}</a>
</li>
))}
</ul>
);
}
```
`{ next: { revalidate: 30 } }` opts the fetch into Next.js' ISR cache with a 30-second revalidation window. Tune to taste; omit `options` to use defaults.
**Render an individual post (`src/app/[slug]/page.tsx`):**
```typescript
import { PortableText, defineQuery, type SanityDocument } from "next-sanity";
For advanced patterns (TypeGen, Visual Editing with `next-sanity/visual-editing`, live content with `defineLive` from `next-sanity/live`, standalone Studio architecture), see `nextjs.md`.
### Step 3: Other Frameworks
For non-Next.js frameworks, read the corresponding rule file and follow its integration guide:
- **React Router / Remix:** `remix.md`
- **SvelteKit:** `svelte.md`
- **Nuxt:** `nuxt.md`
- **Astro:** `astro.md`
Each rule file contains framework-specific patterns for data fetching, Portable Text rendering, and Visual Editing.
### Step 4: Smoke Test
Before declaring integration done, exercise both render paths:
1. `npm run dev` (in the app folder)
2. Load the home page (lists posts).
3. **Click through to a detail page** via an in-app `<Link>` / `<a>` — do not paste the URL.
4. Open the browser console. It should be clean. No `ReferenceError: process is not defined`, no hard reload to `/`.
5. For good measure, reload the detail page directly (URL bar) — that exercises SSR.
Server-side rendering passing isn't enough. Client-side route transitions pull lazy chunks that exercise different code paths, and that's where env/bundling traps surface.
---
## What's Next
Once setup is complete, let the user know:
"You're all set! Here are some things I can help with:
- **Visual Editing** — Click-to-edit in the Presentation tool (`visual-editing.md`)
- **TypeGen** — Type-safe queries with generated types (`typegen.md`)
- **Studio Structure** — Customize the Studio sidebar (`studio-structure.md`)
- **SEO** — Metadata, sitemaps, and Open Graph (`seo.md`)
**Secrets** (read tokens, webhook secrets) stay **unprefixed** and are read via `process.env` (or the framework's server-only equivalent) from server-only modules — `*.server.ts`, route handlers, API routes. Never re-export a secret from a module that a route component can import.
---
## Common Commands
```bash
npx sanity@latest mcp configure # Configure MCP for your editor
npx sanity dev # Start Studio locally
npx sanity schema deploy # Deploy schema for MCP/editor access
npx sanity deploy # Deploy Studio to Sanity hosting
npx sanity manage # Open project settings
npm run typegen # Generate TypeScript types
```
---
## Important Notes
- **Be succinct** — Guide step-by-step without over-explaining
// ✅ Also valid: Just defineQuery (TypeGen works, but no editor highlighting)
constQUERY=defineQuery(`*[_type == "post"]`);
```
## 2. Query Fragments
Use string interpolation to reuse query logic and keep queries maintainable.
```typescript
// src/sanity/fragments/image.ts
exportconstimageFragment=/* groq */`
asset->{
_id,
url,
metadata { lqip, dimensions }
},
alt
`;
// src/sanity/queries/post.ts
import{defineQuery}from"groq";
import{imageFragment}from"../fragments/image";
exportconstPOST_QUERY=defineQuery(/* groq */`
*[_type == "post"][0] {
title,
mainImage {
${imageFragment}
}
}
`);
```
## 3. Expansion Patterns (Page Builder)
When building a Page Builder query, expand all potential component types.
**Best Practice:** Use a `pageFields` fragment or similar strategy to keep the main query clean.
```typescript
constpageBuilderExpansion=/* groq */`
pageBuilder[] {
...,
_type == "hero" => {
...,
cta[] { link, label }
},
_type == "gallery" => {
images[] { ${imageFragment} }
}
}
`;
```
## 4. Maintenance Workflow
When you add a new field or component to the Schema:
1.**Update the Query:** Add the new field/expansion to the relevant GROQ query immediately.
2.**Run TypeGen:** If you have `typegen.enabled: true` in `sanity.cli.ts`, types regenerate automatically during `sanity dev`/`sanity build`. Otherwise, run `npm run typegen` manually.
3.**Verify:** Ensure the new field is available in the generated types.
**Critical:** LQIP (Low Quality Image Placeholder) is **not automatic**. You must explicitly query it via `asset->{ metadata { lqip } }`.
### Minimal Query (No LQIP)
```groq
mainImage {
asset->{ _id, url },
alt
}
```
### Full Query (With LQIP & Dimensions)
```groq
mainImage {
asset->{
_id,
url,
metadata {
lqip, // Base64 blur placeholder
dimensions { width, height } // For aspect ratio
}
},
alt,
hotspot, // Include if using hotspot cropping
crop // Include if using cropping
}
```
**Why this matters:** Without querying `metadata.lqip`, the `blurDataURL` in your component will be `undefined` and the blur effect won't work.
## 5. Performance Tips
- **Auto Format:** Sanity CDN automatically serves WebP/AVIF if the browser supports it (no need to specify `.format('webp')` manually in most cases, but `next/image` handles this too).
- **Sizing:** Always request the exact size you need using `.width()` and `.height()` in `urlFor`. Don't download a 4000px image for a thumbnail.
description:Localization patterns for Sanity using official plugins and best practices.
---
# Sanity Localization Rules
Use the contents list to jump directly to the localization pattern you need.
## Table of Contents
- Guiding principles
- Terminology
- Locale content type
- Choosing document-level vs field-level localization
- Document-level localization
- Localized singletons
- Field-level localization
- AI-powered translation
- UI enhancement
- Frontend URL best practices
## 1. Guiding Principles
### Priority: Easy Authoring Experience
The structured nature of Sanity schemas and GROQ make it easy to parse localized content for your frontend. **Never** let frontend architecture dictate your localization approach — prioritize the editor experience.
### Avoid Content Duplication
Don't create nearly identical copies with slight differences (e.g., US vs British English). Use Portable Text marks and custom blocks to swap out words or sections as needed.
Let Sanity generate document IDs for ordinary imported content. Add schema fields for legacy identifiers or slugs, then use GROQ lookups against those fields when you need to rerun an import, patch existing documents, or create references between imported records. Set `_id` directly only for singleton documents.
Run with: `sanity migration run import-wordpress-posts --no-dry-run`
Reference: [Schema and Content Migrations](https://www.sanity.io/docs/content-lake/schema-and-content-migrations)
description:Best practices for migrating content (HTML, Markdown) into Sanity Portable Text.
---
# Sanity Content Migration Rules
## Document Identity During Import (Critical)
Let Sanity generate `_id` values for imported documents unless you are intentionally creating a singleton. Do not derive deterministic UUIDs or document IDs from slugs, file paths, legacy IDs, or related document IDs.
- Store legacy identifiers in fields such as `legacyId`, `externalId`, or `slug`.
- Make imports idempotent by looking up existing documents with GROQ before creating or patching them.
- Create relationships by querying the target document and using its real `_id` in a `reference`; do not predict `_ref` values from naming conventions.
- Reserve explicit `_id` values for singleton documents such as `settings`, `homePage`, or localized singleton IDs like `homePage-en`.
## 1. HTML Import (Legacy CMS)
Use `@portabletext/block-tools` with `JSDOM` to convert HTML to Portable Text. This covers setup, custom deserializers, pre-processing, image uploads, and wrapping in `defineMigration`.
**See `migration-html-import.md` for the full guide with working examples.**
## 2. Markdown Import (Static Sites)
Use `@portabletext/markdown` for direct, schema-aware Markdown ↔ Portable Text conversion.
**Recommended: Direct Conversion with `@portabletext/markdown`**
This handles headings, lists, bold, italic, code, links, images, and tables. Use `@portabletext/sanity-bridge` to pass your Sanity schema so only valid types are produced.
**Alternative: Markdown → HTML → Portable Text**
For complex Markdown with non-standard extensions, convert to HTML first, then use `htmlToBlocks` (see above).
1.**Parse:**`marked` or `remark` to convert MD to HTML.
2.**Convert:** Use `htmlToBlocks` from `@portabletext/block-tools`.
> **Note:** `@sanity/block-content-to-markdown` and `@sanity/block-tools` are deprecated. Use `@portabletext/markdown` and `@portabletext/block-tools` instead.
## 3. Image Handling (Universal)
Don't just link to external images. Download them and upload to Sanity Asset Pipeline.
1.**Extract:** Find `<img>` tags or Markdown image syntax.
description:Integration guide for Next.js App Router, Live Content API, and a standalone Sanity Studio.
---
# Next.js & Sanity Integration Rules
Jump to the section that matches the task instead of reading this guide end-to-end.
## Table of Contents
- Architecture patterns
- Data fetching (Live Content API)
- Caching and revalidation
- Visual Editing and clean data
- Studio setup (standalone)
- Draft Mode setup
- Error handling
- Presentation queries
- Pagination pattern
## 1. Architecture Patterns
### Option A: Standalone Studio (Recommended)
**Best for:** All new Next.js projects.
The Studio is its own app, living alongside the Next.js app in the same repo:
```
your-project/
├── studio/ # Sanity Studio (standalone)
└── web/ # Next.js frontend
```
Why standalone instead of embedding the Studio in the Next.js app:
- **Faster dev and builds:** `sanity dev` and `sanity build` run on Vite and are dramatically faster (10-30x) than compiling the Studio through `next dev` / `next build`.
- **Auto-updates:** Standalone Studios receive bugfixes and new features automatically, with no dependency bump or redeploy. Embedded Studios can't auto-update (Next.js does not support ESM with import maps), so every update means bump + deploy.
- **TypeGen watch mode:** With `sanity dev`, TypeGen regenerates types as queries change. Embedded Studios can't hook into `next dev`, so you must re-run `sanity typegen generate` manually after every query edit.
- **Content model independence:** A separate Studio keeps the content model from becoming website-centric and makes collaboration easier.
**Setup:**
- Run both apps side by side in separate terminals: `next dev` (localhost:3000) and `sanity dev` (localhost:3333).
- Add your Next.js app URL to **CORS Origins**: `npx sanity cors add http://localhost:3000 --credentials` (repeat for your production URL), or via [Sanity Manage](https://www.sanity.io/manage).
- See `project-structure.md` rule for detailed structure.
### Option B: Embedded Studio (Not Recommended)
The Studio can be mounted inside the Next.js app at `/app/studio/[[...tool]]/page.tsx` via `next-sanity/studio`. Avoid this for new projects: it slows builds, ties every Studio update to an app deploy, and rules out auto-updates and TypeGen watch mode. For maintaining or migrating an existing embedded Studio, see section 5.
## 2. Data Fetching (Live Content API)
We use `defineLive` (next-sanity v11+) to enable real-time content updates and Visual Editing automatically.
### Setup (`src/sanity/lib/live.ts`)
```typescript
import{defineLive}from"next-sanity";
import{client}from"./client";
exportconst{sanityFetch,SanityLive}=defineLive({
client: client.withConfig({
apiVersion:"2026-02-01",
}),
serverToken: process.env.SANITY_API_READ_TOKEN,
browserToken: process.env.SANITY_API_READ_TOKEN,
});
```
### Rendering (`src/app/layout.tsx`)
You **must** render `<SanityLive />` in the root layout to enable real-time updates.
**Use `defineLive` by default.** It handles fetching, caching, and invalidation automatically. Only implement manual caching when you need fine-grained control.
**Never** let Stega characters leak into `<head>` tags. Always set `stega: false` for metadata fetching.
```typescript
exportasyncfunctiongenerateMetadata({params}){
const{data}=awaitsanityFetch({
query: SEO_QUERY,
params: awaitparams,
stega: false,// 👈 Critical for SEO
});
return{title: data?.title};
}
```
### C. Static Params
When generating static params, fetch only published content and disable stega.
```typescript
exportasyncfunctiongenerateStaticParams() {
const{data}=awaitsanityFetch({
query: SLUGS_QUERY,
perspective:"published",// 👈 No drafts
stega: false,
});
returndata;
}
```
## 5. Setup: Studio (Standalone)
Create the Studio as its own app from the repo root — **not inside the Next.js app folder**, where the CLI would switch to its embedded flow:
```bash
npm create sanity@latest -- --project <projectId> --dataset production --template clean --typescript --output-path studio
```
Run it with `npm run dev` inside `studio/` (defaults to http://localhost:3333). For Visual Editing, point the Presentation Tool's `previewUrl.origin` at the Next.js app (see `visual-editing.md`).
### Migrating an Existing Embedded Studio
Embedded Studios (`<NextStudio />` mounted at a route like `/app/studio/[[...tool]]/page.tsx`) keep working, but migrating to a standalone Studio is recommended:
1. Create a standalone Studio folder as above, reusing your existing `projectId` and dataset.
2. Move `sanity.config.ts`, `sanity.cli.ts`, and your schema types into it.
3. Delete the `/app/studio/[[...tool]]/` route from the Next.js app. Keep `next-sanity` — the app still needs it for fetching, Live Content, and Visual Editing.
4. Add the app's URLs to CORS origins and set the Presentation Tool's `previewUrl.origin` to the app's URL.
## 6. Setup: Draft Mode
Enable Presentation Tool and Visual Editing by setting up a draft mode route.
For faster live editing in the Presentation Tool, use `usePresentationQuery` to fetch only the specific block being edited, rather than re-rendering the entire page.
### Why Use This
- **Without:** Editing a hero title re-fetches the whole page, re-renders all blocks
- **With:** Only the hero block re-fetches and re-renders
This is especially valuable for pages with many Page Builder blocks or complex Portable Text.
`-t ui` selects the Nuxt UI starter. `-M ""` skips the interactive module-selection prompt (empty string = no extra modules). `--packageManager npm` and `--no-gitInit` suppress the other two prompts so the scaffold runs end-to-end without input.
### Installation
```bash
npx nuxi@latest module add sanity
```
`nuxi module add sanity` resolves to the official `@nuxtjs/sanity` module and registers it in `nuxt.config.ts` automatically. The module bundles `@sanity/client`, `@sanity/visual-editing`, `@portabletext/vue`, and `groq` as direct dependencies — no separate installs needed.
`groq` and `defineQuery` are also **auto-imported** by the module, so you can use them in `.vue` files without an `import` statement.
For manual image-URL building (an alternative to the auto-registered `<SanityImage>` component), add `@sanity/image-url`:
```bash
npm install @sanity/image-url
```
### What the module auto-imports
**Composables** (use directly in `<script setup>`, no imports needed):
-`<SanityContent>` — Portable Text renderer (uses `@portabletext/vue` internally; prop is `:value`)
-`<SanityImage>` — image renderer; takes an `assetId` (the image asset's `_ref`); upgrades to `<NuxtImg>` automatically when `@nuxt/image` is installed
-`<SanityFile>` — file renderer
### Configuration (`nuxt.config.ts`)
```typescript
exportdefaultdefineNuxtConfig({
modules:["@nuxtjs/sanity"],
sanity:{
projectId: process.env.NUXT_SANITY_PROJECT_ID,
dataset: process.env.NUXT_SANITY_DATASET,
apiVersion:"2026-05-15",
// Live Visual Editing Configuration
visualEditing:{
studioUrl: process.env.NUXT_SANITY_STUDIO_URL,
token: process.env.NUXT_SANITY_API_READ_TOKEN,// Required for fetching drafts
stega: true,// Enable stega for visual editing
mode:"live-visual-editing",// Default: enables live updates
},
},
});
```
**Important:** Don't enable the `minimal` client if you want the full feature set (composables, components, visual editing).
## 2. Data Fetching
### `useSanityQuery`
Use the composable for reactive fetching. It handles preview state automatically when `visualEditing` is configured. `groq` and `defineQuery` are auto-imported — use either.
Pull the slug off `useRoute()` and pass it as a query parameter. The `<SanityContent>` component renders Portable Text — note the prop is `value`, not `blocks` (renamed in v2).
When `visualEditing` is configured in `nuxt.config.ts`, the module handles:
1. Injecting the Visual Editing overlays.
2. Refreshing data when content changes in the Studio.
3. Enabling Stega encoding.
### Handling Stega in Logic
If you use stega-encoded strings in logic (e.g. `v-if="post.layout === 'full'"`), you must clean them. `stegaClean` is exported from `@sanity/client/stega` (a transitive of `@nuxtjs/sanity`, so no separate install).
**Option A — `<SanityImage>` (recommended).** Auto-registered. Takes the asset's `_ref` (the `assetId`) and builds the URL via the module's resolved projectId/dataset. If `@nuxt/image` is installed, it transparently upgrades to `<NuxtImg>` for responsive sizing.
**Option B — `@sanity/image-url` builder.** Install `@sanity/image-url` separately and build URLs manually. Useful when you need fine-grained control (hotspot/crop, format negotiation, srcset).
description:Patterns for Sanity Page Builder arrays, block components, and live editing.
---
# Sanity Page Builder Patterns
This guide covers **Page Builder** patterns—arrays of block objects that allow content teams to compose flexible page layouts. For Portable Text (rich text within documents), see `portable-text.md`.
## 1. What is a Page Builder?
A page builder is an **array of objects** (`pageBuilder[]`) that allows content teams to compose pages from reusable blocks without developer intervention.
## 5. Presentation Queries for Live Editing (Next.js)
For faster live updates in the Presentation Tool, use **presentation queries** that fetch only the specific block being edited, rather than re-fetching the entire page.
> **Note:** This pattern uses `usePresentationQuery` from `next-sanity/hooks`. For other frameworks, check your loader package for equivalent functionality.
description:Portable Text (Rich Text) rendering and custom component creation for React/Next.js.
---
# Sanity Portable Text Rules
Portable Text is Sanity's rich text format, used for content like article bodies (`body[]`). This guide covers rendering and creating custom PTE components.
**Note:** For page-level layout blocks (`pageBuilder[]`), see `page-builder.md`.
## 1. The Component
Use the `PortableText` component from `next-sanity` (or `@portabletext/react`).
```typescript
import{PortableText}from"next-sanity";
// or import { PortableText } from "@portabletext/react";
When Visual Editing is enabled, text content contains invisible stega characters for click-to-edit functionality.
**For text rendering:** Let stega characters pass through—they enable overlays:
```typescript
// Good - stega preserved for click-to-edit
<h2>{children}</h2>
```
**For logic/comparisons:** Clean the values first:
```typescript
import{stegaClean}from'@sanity/client/stega'
// Clean before using in logic
constcleanedStyle=stegaClean(block.style)
if(cleanedStyle==='h2'){...}
```
## 10. Type Safety
When using TypeGen, the Portable Text value usually has a complex generated type. You can often use `any` or `PortableTextBlock[]` for the _prop_, but cast specific blocks if needed.
```typescript
import{PortableTextBlock}from"next-sanity";
typeProps={
value: PortableTextBlock[];
};
```
## 11. Best Practices
- **Tailwind Typography:** For simple blogs, wrap `<PortableText />` in a `<div className="prose">` (from `@tailwindcss/typography`) instead of manually styling every block.
- **Handling Nulls:** Always check if `value` exists and is an array before rendering.
- **Keys:** The `PortableText` component handles React keys automatically using the `_key` from Sanity. Do not add keys manually.
- **Separate from Page Builder:** PTE blocks live in `body[]` (rich text fields), not `pageBuilder[]` (page layout). Keep these patterns separate.
description:Project structure patterns for Sanity projects including standalone Studio and monorepo setups.
---
# Sanity Project Structure
## Standalone Studio
Best for content-only projects, API-first architectures, or when frontend is managed separately.
```
your-project/
├── schemaTypes/
│ ├── index.ts
│ ├── documents/
│ ├── objects/
│ └── blocks/
├── sanity.config.ts
├── sanity.cli.ts
└── package.json
```
**Use cases:**
- Content modeling with MCP/AI tools (no frontend needed)
- Headless CMS with external consumers
- Prototyping and content design
## Monorepo (Recommended with a frontend)
Best for most projects pairing Sanity with a Next.js (or other framework) app. The Studio stays standalone — Vite-based dev/builds, auto-updates, TypeGen watch mode — while living in the same repo as the frontend.
No workspace tooling is required — each app manages its own dependencies. For larger repos, the same shape works under `apps/` with npm or pnpm workspaces.
**Setup:**
1. Add the web app URL to CORS origins: `npx sanity cors add http://localhost:3000 --credentials` (or via [Sanity Manage](https://www.sanity.io/manage))
2. Configure `typegen` in `studio/sanity.cli.ts` to read queries from `../web` and output types to `../web/sanity.types.ts` (see `typegen.md`)
3. Optionally add a root `package.json` with scripts that run both dev servers
## Embedded Studio (Legacy — Not Recommended)
Older Next.js projects may mount the Studio inside the app at `src/app/studio/[[...tool]]/page.tsx`, with `sanity.config.ts` in the app root. This still works but is no longer recommended: it slows builds, ties Studio updates to app deploys, and rules out auto-updates and TypeGen watch mode. See `nextjs.md` for the rationale and migration steps.
## File Naming Conventions
- **kebab-case** for all files: `user-profile.ts`, `hero-block.ts`
-`.ts` for schemas/utilities, `.tsx` for React components
- Each schema exports a named const matching filename
description:Integration guide for React Router v7 (and Remix v2) with Sanity, including loaders and visual editing.
---
# React Router (Remix) & Sanity Integration Rules
## Version Note
The primary examples below use **React Router v7** (the current shape — Remix v2 was renamed to React Router v7 starting with the v7 release). Import paths and the route-types file (`./+types/<route>`) come from the `react-router` package and the framework's typegen.
If you are on the older **Remix v2** stack, the integration shape is identical; only the import paths differ:
`-y` accepts defaults. The Sanity packages cover server loaders (`@sanity/react-loader`, `@sanity/client`), live preview (`@sanity/visual-editing`), Portable Text rendering (`@portabletext/react`), and typed queries (`groq`).
To support both server-side fetching and client-side live previews, use the **Split Loader Pattern**.
### A. Environment Variables
React Router runs on Vite. **Any module reachable from a route component gets bundled into the client** — `process.env` doesn't exist there and will throw `ReferenceError: process is not defined` on client-side route transitions (SSR will still work, which makes this trap easy to miss).
Split publishable values from secrets:
- **Publishable** (`projectId`, `dataset`, `apiVersion`, `studioUrl`): prefix with `VITE_` and read via `import.meta.env`. Safe to import from anywhere.
- **Secrets** (read tokens, webhook secrets): keep unprefixed and read via `process.env`**only inside `*.server.ts` files**. Never re-export them from a shared module.
### C. Server Loader (`app/sanity/loader.server.ts`)
Initializes the server client. Read the token directly from `process.env` here — do **not** import it from `env.ts`, or it will leak into the client bundle the moment any client-reachable module touches `env.ts`.
Use `loadQuery` from your **server** file in route loaders. Import the generated `Route` type from `./+types/<route>` — React Router writes one type module per route file.
For Remix v2: replace `Route.ComponentProps` / `Route.LoaderArgs` with `useLoaderData<typeof loader>()` and `LoaderFunctionArgs` from `@remix-run/node`.
This route is the canonical shape that exposes the env trap: `urlFor` → `client.ts` → `env.ts`. If `env.ts` reads `process.env`, the route works under SSR (curl returns HTML) but the client-side `<Link>` navigation will throw `ReferenceError: process is not defined` in the browser console and React Router will hard-reload back to `/`.
## 4. Real-time Preview & Visual Editing
### A. Use `useQuery` in Components
Import `useQuery` from your **shared** loader file.
**Querying:** Always include `_key` in array projections:
```groq
*[_type == "page"][0]{
pageBuilder[]{
_key, // Always include _key in queries
_type,
...
}
}
```
### B. Icons
Always assign an icon from `@sanity/icons` to documents and objects. This improves the Studio UX significantly. Browse all icons at [icons.sanity.build](https://icons.sanity.build/all).
| Content Type | Icon |
| -------------- | ------------------ |
| Article, Post | `DocumentTextIcon` |
| Author, Person | `UserIcon` |
| Category, Tag | `TagIcon` |
| Settings | `CogIcon` |
| Page | `DocumentIcon` |
| Image block | `ImageIcon` |
| Video block | `PlayIcon` |
| FAQ | `HelpCircleIcon` |
| Link | `LinkIcon` |
### C. Boolean vs. List
Avoid boolean fields for binary states that might expand later.
- **Prefer:** `options.list` with "radio" layout.
```typescript
defineField({
name:"status",
type:"string",
options:{
list:[
{title:"Draft",value:"draft"},
{title:"Published",value:"published"},
],
layout:"radio",
},
});
```
### D. The "Toggle" Pattern (Conditional Fields)
Use a radio/boolean field to toggle visibility of other fields (often grouped in fieldsets).
Sanity document `_id` values are implementation identifiers, not a content modeling tool.
- **Prefer generated IDs:** Let Sanity assign `_id` values for ordinary content documents. Avoid deterministic UUIDs, slug-derived IDs, and IDs copied from legacy systems.
- **Use relationships, not ID conventions:** Connect documents with `reference` fields and set `_ref` from an actual lookup or from the `_id` returned after creating the related document.
- **Store source identity as content:** For imports, put legacy IDs, external IDs, or stable slugs in explicit fields such as `legacyId`, `externalId`, or `slug`, then query by those fields when you need to find or upsert content.
- **Keep explicit IDs rare:** Directly setting `_id` is mainly useful for singleton documents managed through Studio Structure, such as `settings` or localized singletons like `homePage-en`.
## 7. Safe Schema Updates (The Deprecation Pattern)
**NEVER** delete a field that contains production data. It will cause data loss or Studio crashes. Instead, follow the **ReadOnly -> Hidden -> Deprecated** lifecycle.
### The Pattern
1.**`deprecated`**: Adds a visual warning and reason.
2.**`readOnly: true`**: Prevents new edits but keeps data visible.
3.**`hidden`**: Hides it from _new_ documents (where value is undefined).
4.**`initialValue: undefined`**: Ensures new documents don't get this field.
```typescript
defineField({
name:"oldTitle",// The field you want to remove
title:"Article Title (Deprecated)",
type:"string",
deprecated:{
reason:'Use the new "seoTitle" field instead. This will be removed in v2.',
},
readOnly: true,
hidden:({value})=>value===undefined,
initialValue: undefined,
});
```
### Migration Workflow
**Phase 1: Deprecate** — Apply the deprecation pattern above. Deploy.
**Phase 2: Migrate** — Update frontend to use new fields (with `coalesce()` fallbacks). Create a migration:
1.**Singletons First:** Place critical site-wide settings (Global Settings, Homepage) at the top.
2.**Dividers:** Use `S.divider()` to visually separate logical groups.
3.**Filtered Lists:** Always exclude Singleton documents from generic `documentTypeList` items to avoid duplication.
## 4. Singleton Pattern (Critical)
**Singletons are enforced via Structure, NOT schema options.** There is no `singleton: true` schema option.
This is the main case where explicit document IDs are appropriate. For ordinary content documents, let Sanity generate `_id` values and use references or GROQ lookups to connect records.
### How Singletons Work
1. Use `S.document().documentId('fixed-id')` to lock the document to a specific ID.
2. Filter the type from generic lists to prevent duplicate entries.
description:Integration guide for SvelteKit with Sanity using @sanity/sveltekit, including Visual Editing and Preview Mode.
---
# SvelteKit & Sanity Integration Rules
This guide uses the official **`@sanity/sveltekit`** package (Svelte 5 + SvelteKit 2). The older `@sanity/svelte-loader` does not work with Svelte 5 — its `useQuery` store returns empty on the client. Use `@sanity/sveltekit` instead.
`--template minimal` is the bare app. `--types ts` enables TypeScript. `--no-add-ons` skips the add-on picker. `--install <pm>` chooses the package manager (`npm`, `pnpm`, `yarn`, or `bun`).
`@sanity/sveltekit` is the one-stop integration: it bundles `@sanity/client`, `@sanity/visual-editing`, `@sanity/core-loader`, `groq`, and friends, and re-exports `createClient`, `defineQuery`, `groq`, and `stegaClean`. **Do not** also install `@sanity/client`, `@sanity/visual-editing`, or `groq` directly — import them from `@sanity/sveltekit`. `@sanity/image-url` and `@portabletext/svelte` are not bundled, so add them separately.
### Environment variables (`.env.local`)
```bash
PUBLIC_SANITY_PROJECT_ID=your-project-id
PUBLIC_SANITY_DATASET=production
PUBLIC_SANITY_API_VERSION=2026-05-15
PUBLIC_SANITY_STUDIO_URL=http://localhost:3333
SANITY_API_READ_TOKEN=
```
SvelteKit's `$env/static/public` requires the `PUBLIC_` prefix for any var read on the client. `SANITY_API_READ_TOKEN` must be declared (even empty) if any file imports it from `$env/static/private`, otherwise Vite throws at build time.
Import `createClient` from `@sanity/sveltekit`, not `@sanity/client`. `useCdn: true` is for production reads; the server (preview) client below overrides to `false`.
### `src/lib/sanity/client.server.ts` — server (preview) client
`handlePreviewMode` installs `/preview/enable` and `/preview/disable` endpoints, reads the preview cookie, and populates `locals.sanity` with `{client, fetch, loadQuery, previewEnabled, previewPerspective, browserToken}`. `handleQueryLoader` attaches `loadQuery` to `locals.sanity` for use in `+page.server.ts` / `+layout.server.ts`.
When using fetched strings for logic (routing, classNames), strip the stega markers first.
```ts
import{stegaClean}from"@sanity/sveltekit";
// …
if(stegaClean(slug)==="home"){
/* … */
}
```
## 7. Caveats
- **Yarn classic + Visual Editing.** `@sanity/visual-editing` lazy-loads a chunk that imports `react/compiler-runtime`. Yarn classic doesn't auto-install peer deps, so users who flip preview mode on with yarn classic also need `yarn add react react-dom`. (Other package managers handle this automatically.) `<VisualEditing>` only loads this chunk when `enabled === true`, so a default preview-off app is unaffected.
- **No `<slot />`.** Svelte 5 layouts use `{@render children()}`.
- **No `export let`.** Pages and components use `const {data} = $props()`.
- **`@sanity/image-url` default export.** Use the named `createImageUrlBuilder`; the default export still works but logs a runtime deprecation warning.
// Filter specific types from unions using _type discriminator
typeHeroBlock=FilterByType<PageBuilder,"hero">;
```
### Unique Query Names
All queries must have unique variable names. Duplicate names across files will cause TypeGen to silently overwrite types. Use descriptive, scoped names:
```typescript
// Unique names
constPOSTS_INDEX_QUERY=defineQuery(`*[_type == "post"]{ title }`)
Ensure `sanity.types.ts` is included in your `tsconfig.json`'s `include` array. If your config restricts includes (e.g., `["src/**/*"]`) and the types file is at the project root, TypeScript won't pick up the generated types:
```json
{
"include":["src/**/*","sanity.types.ts"]
}
```
### Skipping Individual Queries
Add `@sanity-typegen-ignore` in a comment before a query to skip type generation:
description:Comprehensive guide for Sanity Visual Editing, including Presentation Tool, Stega (Content Source Maps), and Overlays.
---
# Sanity Visual Editing Rules
## 1. Concepts
### Presentation Tool
The Studio plugin (`sanity/presentation`) that renders your front-end application inside an iframe in the Studio. It enables the "Edit" overlay and bidirectional navigation.
### Content Source Maps (Stega)
Invisible characters embedded in strings that tell the Presentation Tool which field in which document the content comes from.
- **Mechanism:** Sanity encodes document ID, field path, and dataset info into string values.
- **Result:** Click-to-edit functionality in the preview.
### Loaders
Framework-agnostic or specific libraries that handle:
1. Fetching data (production vs. preview).
2. Subscribing to real-time updates (Live Content API).
3. Encoding Stega strings (if not handled by the Content Lake automatically).
## 2. The Golden Rule of Stega (Clean Data)
When Visual Editing is enabled, string fields will contain invisible characters. You **MUST** clean them before using the value for logic.
- Use `useOptimistic` hook for instant UI updates during mutations
## 8. Optimistic Updates for Faster Editing
By default, editing a field in the Presentation Tool triggers a full page re-render. For pages with many components, this can feel sluggish. **Presentation queries** solve this by fetching only the specific block being edited.
### The Concept
Instead of:
1. User edits a field -> Full page query re-runs -> All components re-render
You get:
1. User edits a field -> Block-specific query runs -> Only that component re-renders
### How It Works
1.**Create a targeted query** that fetches just the block data using `_key`:
Hei! Det ser ut som det ikke har vært aktivitet her på en stund. Etter 30 dager vil saken bli lukket automatisk.
Om saken fortsatt er relevant eller du har oppdateringer, vil en kommentar hindre at den lukkes. Du kan alltids åpne den igjen hvis det skulle være nødvendig!
Om du ikke finner et ikonet du leter etter kan du [åpne et issue på github](https://github.com/navikt/aksel/issues/new?labels=nytt+✨%2Cikoner+🖼%2Cforespørsel+🥰&template&template=new-icon.yaml&title=%5BNytt+ikon%5D%3A+).
Om du ikke finner ikonet du leter etter kan du [lage et issue på GitHub](https://github.com/navikt/aksel/issues/new?labels=nytt+✨%2Cikoner+🖼%2Cforespørsel+🥰&template=new-icon.yaml&title=%5BNytt+ikon%5D%3A+).
## Oppdatere ikon
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her.](https://github.com/navikt/aksel/issues/new?labels=forespørsel+🥰&template=update-icon.yml&title=%5BInnspill+til+ikon%5D%3A+)
## Legge til nye ikoner selv
Hvis du selv ønsker å legge til nye ikoner kan du kopiere filenes fra `/template` og legge de inn i `/icons` med egen data. Alle ikonene har naming-format `PascalCase`. Alle ikonene har 1 SVG-fil og 1 tilhørende YML-fil.
### Svg
- Sizing, width og height må være 24x24: `width="24" height="24" `
- Viewbox: `viewBox="0 0 24 24"`
- Fill på path: `fill="#202733"`
### Yml
```yml
name:Template# Samme som filnavn
category:Template category# Hovedkategori
sub_category:Sub category# Underkategori
keywords:# Samling med søkeord
- template1
- template2
- template3
variant:Stroke# Stroke eller Fill
updated_at:02.01.2023# Dato for siste oppdatering
created_at:01.01.2023# Dato for opprettelse
```
### Publisere oppdatering
Når du har lagt til nye ikoner kan du opprette en pull request. Denne vil bli automatisk testet og så gått gjennom av Aksel-teamet.
Legg gjerne til en `changeset` ved å kjøre `yarn changeset` i terminalen. Dette vil gi oss bedre oversikt over hva som er endret i endringslogger og automatisk bumpe pakke-versjonen ved merge.
#### Teste nytt ikon
Hvis du ønsker å teste om alt stemmer, både i `svg` og `yml` før du lager en PR kan du kjøre `yarn && cd navikt/aksel-icons && yarn test` i terminalen lokalt.
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her](https://github.com/navikt/aksel/issues/new?labels=forespørsel+🥰&template=update-icon.yml&title=%5BInnspill+til+ikon%5D%3A+).
## Design av nye ikoner
@@ -51,9 +17,9 @@ Hvis du ønsker å teste om alt stemmer, både i `svg` og `yml` før du lager en
Loaded 100 of 4599 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.