All skills
sanity-io avatar

/sanity-best-practices

@fc8116b official
by Sanitysanity-io/agent-toolkit187 stars
30

Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, webhooks, 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, event-driven content automation, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase.

Use this Skill: https://skilld.dev/gh/sanity-io/agent-toolkit/sanity-best-practices

This session only. Nothing lands on disk.

referencesvisual-editing.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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.

Scenario Clean? Why
Comparing strings (if (x === 'y')) ✅ Yes Stega breaks equality
Using as object keys ✅ Yes Keys won't match
Using as HTML IDs ✅ Yes Invalid characters
Passing to third-party libraries ✅ Yes May validate input
Rendering text (<h1>{title}</h1>) ❌ No Breaks click-to-edit
Passing to <PortableText /> ❌ No Handles internally
Passing to image helpers ❌ No Handles internally
import { stegaClean } from "@sanity/client/stega";

export function Layout({ align }: { align: string }) {
  // Good: Clean before comparison
  const cleanAlign = stegaClean(align);
  return <div className={cleanAlign === 'center' ? 'mx-auto' : ''} />
}

3. Token Handling (Security)

Store your read token in a dedicated file that throws if missing:

// src/sanity/lib/token.ts
export const token = process.env.SANITY_API_READ_TOKEN

if (!token) {
  throw new Error('Missing SANITY_API_READ_TOKEN')
}

Never expose tokens in client bundles. Pass to defineLive for server/browser use only when Draft Mode is enabled.

4. Setup: Presentation Tool

File: sanity.config.ts

import { defineConfig } from 'sanity'
import { presentationTool } from 'sanity/presentation'
import { resolve } from '@/sanity/presentation/resolve'

export default defineConfig({
  // ...
  plugins: [
    presentationTool({
      resolve, // Document locations (see below)
      previewUrl: {
        // The front-end origin — required when the Studio runs standalone
        origin: process.env.SANITY_STUDIO_PREVIEW_ORIGIN || 'http://localhost:3000',
        previewMode: {
          enable: '/api/draft-mode/enable',
        },
      },
    }),
  ],
})

Document Locations

Show where documents appear in the front-end — enables quick navigation between Structure and Presentation tools.

// src/sanity/presentation/resolve.ts
import { defineLocations, PresentationPluginOptions } from 'sanity/presentation'

export const resolve: PresentationPluginOptions['resolve'] = {
  locations: {
    post: defineLocations({
      select: { title: 'title', slug: 'slug.current' },
      resolve: (doc) => ({
        locations: [
          { title: doc?.title || 'Untitled', href: `/posts/${doc?.slug}` },
          { title: 'Posts index', href: `/posts` },
        ],
      }),
    }),
    // Add more document types as needed
  },
}

5. Visual Editing Overlays

Render <VisualEditing /> in Draft Mode for click-to-edit overlays.

Next.js (App Router):

// layout.tsx
import { VisualEditing } from 'next-sanity/visual-editing'
import { draftMode } from 'next/headers'
import { DisableDraftMode } from '@/components/disable-draft-mode'

export default async function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        {(await draftMode()).isEnabled && (
          <>
            <DisableDraftMode />
            <VisualEditing />
          </>
        )}
      </body>
    </html>
  )
}

Disable Draft Mode Button

Useful for content authors to exit preview and see published content:

// src/components/disable-draft-mode.tsx
'use client'
import { useDraftModeEnvironment } from 'next-sanity/hooks'

export function DisableDraftMode() {
  const environment = useDraftModeEnvironment()
  // Only show outside of Presentation Tool
  if (environment !== 'live' && environment !== 'unknown') return null

  return (
    <a href="/api/draft-mode/disable" className="fixed bottom-4 right-4 bg-gray-50 px-4 py-2">
      Disable Draft Mode
    </a>
  )
}

Remix/Svelte: See framework-specific rules for useLiveMode and enableVisualEditing patterns.

6. SEO & Metadata (Critical)

NEVER allow Stega strings in <head> tags (Title, Description, Canonical URLs). It destroys SEO rankings and looks broken in search results.

  • Next.js: Set stega: false in generateMetadata.
  • General: Explicitly clean fields used in <title> or <meta>.
// Next.js Example — disable stega at fetch level
export async function generateMetadata({ params }) {
  const { data } = await sanityFetch({
    query: SEO_QUERY,
    stega: false // Critical
  })
  return { title: data.title }
}

Alternative: If you can't disable stega at the fetch level, clean explicitly:

import { stegaClean } from "@sanity/client/stega";

export async function generateMetadata({ params }) {
  const { data } = await sanityFetch({ query: PAGE_QUERY })
  return { 
    title: stegaClean(data.title),
    description: stegaClean(data.description),
    openGraph: { url: stegaClean(data.canonicalUrl) }
  }
}

7. Drag-and-Drop Reordering (Advanced)

For arrays (e.g., "Related Posts"), enable drag-and-drop in the preview using data-sanity attributes and useOptimistic:

import { createDataAttribute } from 'next-sanity'
import { useOptimistic } from 'next-sanity/hooks'

// Add data-sanity to array container
<ul data-sanity={createDataAttribute({ id: documentId, type: 'post', path: 'relatedPosts' }).toString()}>
  {items.map((item) => (
    <li key={item._key} data-sanity={createDataAttribute({
      id: documentId, type: 'post', path: `relatedPosts[_key=="${item._key}"]`
    }).toString()}>
      {item.title}
    </li>
  ))}
</ul>

Key requirements:

  • Query must include _key for array items
  • 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:
*[_id == $documentId][0]{
  "heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
    title, subtitle, image
  }
}
  1. Use a presentation query hook in your component (e.g., usePresentationQuery in Next.js)

  2. Fall back to initial props when not in presentation mode

This pattern works for both Page Builder blocks (pageBuilder[]) and Portable Text blocks (body[]).

See framework-specific rules for implementation:

  • Next.js: nextjs.md (Section 9)
  • Page Builder: page-builder.md (Section 5)
  • Portable Text: portable-text.md (Section 7)

9. Framework Specifics

Framework Loader Package Key Components
Next.js next-sanity <VisualEditing />, defineLive, usePresentationQuery
Remix @sanity/react-loader createQueryStore, useLiveMode, enableVisualEditing
Svelte @sanity/svelte-loader createRequestHandler, useLiveMode, enableVisualEditing
Nuxt @nuxtjs/sanity Automatic via module config (visualEditing: {})
Astro @sanity/astro sanity({ useCdn: false, stega: true })

Source: SKILL.md on GitHub

2 warnings14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    This skill provides comprehensive Sanity.io development best practices, covering schema design, GROQ queries, and integration with major frontend frameworks. It promotes secure development habits, such as proper management of API tokens and environment variables.

  • Socket14d

    1 alert: gptAnomaly

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer6mo

    8/24 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at fc8116b. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
  • Next.js
  • Nuxt
  • sanity
  • groq
  • schema
  • typegen
  • portable-text
  • visual-editing
  • astro
  • remix
  • sveltekit
  • migrations
  • localization
  • cms

README badge

README badge for sanity-io/agent-toolkit/sanity-best-practices

Provides guidelines and reference materials for Sanity schema design, GROQ queries, TypeGen, Visual Editing, Portable Text, Studio structure, localization, migrations, Sanity Functions, and framework integrations including Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, and Hydrogen. Use this skill when setting up Sanity projects, designing content models, writing queries, implementing live preview, or integrating Sanity with a frontend framework.

Generated from the current SKILL.md.

Does this skill cover framework integrations like Next.js, Nuxt, and Astro?
Yes. The skill includes integration guides for Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and standalone Studio patterns.
What does this skill cover for GROQ queries?
The skill provides GROQ query patterns, type safety approaches, and performance optimization guidelines, with reference materials for detailed examples.
Does this skill include guidance on Sanity Functions and event automation?
Yes. The skill covers Sanity Functions for automating content workflows, including documentEventHandler and defineDocumentFunction patterns.
Can I use this skill for schema design and content modeling?
Yes. The skill provides schema design best practices, field definitions, validation patterns, and content modeling guidance for different use cases.
Does this cover Visual Editing and live preview setup?
Yes. The skill includes guidance on the Presentation Tool, Stega, overlays, and live preview configuration for frontend frameworks.

Generated from the current SKILL.md. These answers refresh after source changes.