Sanity Studio Customization

Sanity Studio is a deployable React SPA that talks to the hosted Content Lake over HTTP and WebSocket. Because it decouples content modeling from presentation, you get full control of the authoring interface — at the cost of owning its configuration, component isolation, and deployment. This guide covers the sanity.config.ts architecture, the Desk Structure API, state management, and CI/CD, within Platform Integration Deep Dives.

Sanity's moving parts in a headless integrationEditors work in Sanity Studio, a React application configured in code, which reads and writes documents in the hosted Content Lake; the frontend queries published or draft perspectives with GROQ through the API CDN or the live API, receives GROQ-powered webhooks on changes, and can render visual editing overlays in preview.Sanity Studioconfig as codeContent LakedatasetsAPI CDNpublishedLive APIdrafts, listenersFrontendGROQ queriesGROQ webhookssignedpreviewrevalidate
The Studio and the frontend are both clients of the same Content Lake.

Integration Contract

A Sanity integration is defined by a few decisions. Schema as code: document types live in TypeScript in the Studio repository, reviewed like code, and the same schema is extracted for type generation. Datasets: one dataset per environment, such as production and staging, with the frontend’s dataset set by configuration. Perspectives: published content for public pages, drafts for preview, selected with the query’s perspective rather than by filtering ids. Tokens: no token for public reads through the API CDN when the dataset is public, a viewer token on the server for private datasets and drafts. Events: GROQ-powered webhooks with a projection and a secret, calling one revalidation handler. Queries: GROQ queries in one module, with generated types.

Bash
# .env: Sanity integration
SANITY_PROJECT_ID=abc123
SANITY_DATASET=production
SANITY_API_VERSION=2025-06-01
SANITY_VIEWER_TOKEN=server_only_viewer_token
SANITY_WEBHOOK_SECRET=secret_configured_on_the_webhook

Core Configuration Architecture

sanity.config.ts (or .js) is the single source of truth for project IDs, dataset routing, plugin registration, and schema. It’s evaluated at build time and should stay declarative — stuffing imperative business logic into it triggers extra rebuilds and degrades HMR during local development.

TSX
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schema'

export default defineConfig({
  name: 'production',
  title: 'Agency Content Hub',
  projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
  dataset: 'production',
  plugins: [structureTool(), visionTool()],
  schema: { types: schemaTypes },
  studio: {
    components: {
      logo: () => <img src="/brand/logo.svg" alt="Studio" />,
      navbar: CustomNavbar,
    }
  }
})

Extract validation rules, custom inputs, and API wrappers into dedicated modules, the same separation the Contentful Integration Guide uses to keep environments in parity. For env vars, follow the SANITY_STUDIO_ prefix convention (Vite env handling) so only safe values reach the client bundle.

Desk Structure & Custom Panes

The Structure API (formerly Desk) replaces the default document tree with role-aware, content-type-specific navigation — what localized, multi-tenant, or approval-driven workflows need. The StructureResolver receives the StructureBuilder (S) and context, so lists can branch on user role or dataset state.

TypeScript
// deskStructure.ts
import { StructureResolver } from 'sanity/structure'

export const structure: StructureResolver = (S, context) =>
  S.list()
    .title('Content Operations')
    .items([
      S.listItem()
        .title('Editorial Queue')
        .schemaType('article')
        .child(
          S.documentTypeList('article')
            .title('Drafts & Reviews')
            .filter('_type == "article" && _id in path("drafts.**")') // only documents with unpublished changes
            .child(S.document().schemaType('article'))
        ),
      S.divider(),
      ...S.documentTypeListItems().filter(
        (item) => !['article', 'author'].includes(item.getId()!)
      ),
    ])

Delegate complex filters to dedicated query modules instead of inline string concatenation. For validation-heavy workflows, Building custom Sanity Studio plugins for content teams encapsulates custom panes and inputs. Align desk-pane GROQ projections with the frontend fetch layer for preview consistency; Using Sanity GROQ for Complex Content Queries covers the projection-optimization patterns.

Component Isolation & State Management

Custom inputs and panes follow normal React lifecycle rules. Skip global state managers unless you genuinely need cross-pane sync; scope React Context to a plugin or pane instead, so state resets cleanly on route transitions and doesn’t leak. Defer heavy UI libraries with React lazy loading.

Audit dependencies for bundle size: dynamic-import charting libraries, diff viewers, and third-party rich-text extensions. Sanity’s Vite pipeline honors code-splitting, but third-party plugins often bundle their own React — enforce peer-dependency resolution in CI or you’ll ship duplicate React instances and hydration mismatches.

Deployment & CI/CD

Deploy the Studio as a static SPA to an edge network. Inject env vars at build time and pin dataset routing per environment so a migration can’t cross-contaminate datasets. Unlike the Strapi Self-Hosted Setup, the hosted Content Lake removes database provisioning, leaving schema versioning and build pipelines as the operational work.

In CI, validate the schema with sanity schema validate, extract it with sanity schema extract for type generation, enforce a bundle-size budget, and gate experimental panes behind feature flags so content teams opt in without risking production.

Querying from the Frontend

The frontend reads content with GROQ through the @sanity/client or next-sanity. Use the API CDN (useCdn: true) for published content, which is cached globally and fast, and the live API for draft reads and for anything that must reflect a publish immediately. Choose the perspective explicitly: published for public pages, drafts in preview, where drafts overlay published documents. Project exactly the fields each component needs, resolve references with -> only as deep as the page renders, and keep queries in a module next to their generated types, as described in using GROQ for complex queries.

TypeScript
// lib/sanity/client.ts
import { createClient } from "next-sanity";

export const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: process.env.SANITY_DATASET!,
  apiVersion: process.env.SANITY_API_VERSION!,
  useCdn: true,
  perspective: "published",
});

export function previewClient() {
  return client.withConfig({ useCdn: false, perspective: "drafts", token: process.env.SANITY_VIEWER_TOKEN });
}

Typed Queries with TypeGen

Sanity TypeGen extracts the schema from the Studio and generates TypeScript types for both document types and every GROQ query defined with the defineQuery helper, including projections. Queries then return precisely typed results, and a schema change that breaks a query fails type checking in CI. The TypeGen guide sets up the pipeline across the Studio and frontend repositories.

Visual Editing and the Presentation Tool

Sanity’s Presentation tool shows the frontend inside the Studio with click-to-edit overlays. The frontend renders content with encoded source maps in draft mode, which the overlays use to map each element back to its document and field, and a live connection refreshes the preview as editors type. Enable stega encoding only in draft mode, so production HTML contains no invisible markers, and restrict the preview route to authenticated editors. The visual editing guide walks through the setup.

Webhooks and Revalidation

GROQ-powered webhooks trigger on document create, update and delete, filtered by a GROQ expression and shaped by a projection, so the payload contains exactly what the handler needs, such as _type, slug and referenced ids. Filter out drafts with !(_id in path("drafts.**")) so draft saves never reach the public revalidation endpoint. Configure a secret on the webhook and verify its signature in the handler, as described in verifying Sanity webhooks, then revalidate tags by type and id.

Datasets per Environment

Datasets are separate content stores within a project, and the usual setup is one per environment. Schema lives in code and deploys with the Studio, so staging and production share the schema version that is deployed to them, while content differs. Copy production into staging periodically with dataset copy or export and import, keep the frontend’s dataset in configuration, and route webhooks by dataset. The datasets guide covers copying, access control and migrations.

Caching Strategy

The API CDN caches published query results and updates within seconds of a publish, which is enough for many sites without any framework caching. For sites that render on the server or regenerate incrementally, add framework caching with tags: tag each query’s result with the document types and ids it depends on, and let the webhook revalidate those tags. The combination gives fast pages and immediate updates. Draft queries in preview must bypass every cache, and so must fetches immediately after a webhook, which should use the live API rather than the CDN, so revalidation never reads a stale CDN response. Images from Sanity’s image pipeline are served with long cache lifetimes, and their URLs change when the asset changes, so they need no purging.

Publish-to-live latency by caching setupMedian time from publish to the change being visible, with the API CDN alone, with framework caching on a fixed interval, and with tag-based revalidation from GROQ webhooks.API CDN only9 secondsFixed 5-minute revalidation160 secondsTag revalidation from webhooks3 seconds
Tags from webhooks gave the fastest updates while keeping pages cached.

Images and the Image Pipeline

Sanity serves images through a CDN with URL-based transformations: width, height, crop, format and quality, plus automatic format negotiation. Store hotspot and crop on image fields, and build URLs with @sanity/image-url, which applies them for each requested size so art direction works across layouts. Request asset metadata such as dimensions and a low-quality preview (lqip) in the same GROQ projection, for layout stability and placeholders, as described in reducing CLS with image placeholders. A custom next/image loader that returns Sanity image URLs moves all transformation to Sanity’s CDN.

Modeling Content in Sanity

Sanity schemas are code, which makes good modeling practices easy to enforce. Page builders are arrays of object types or references, each with a _type that works as the discriminator for rendering, as described in modeling page-builder blocks. Portable Text, Sanity’s rich text format, is an array of blocks that can contain custom object types, rendered on the frontend with a component map much like Contentful’s rich text. Validation rules live next to fields, with custom validators for rules such as unique slugs per locale. Initial value templates pre-fill new documents, and document actions can add workflow steps, such as requiring approval before publishing. Keep the schema small and consistent: shared object types for SEO, images with alt text and links, reused across document types, so the frontend’s resolvers handle them once.

Security and Tokens

Sanity tokens are scoped to a project with roles such as viewer, editor or deploy studio. The frontend should never hold a token with write access. Public datasets need no token for published reads through the CDN; private datasets and draft reads need a viewer token, kept on the server and used only in draft mode or server-side fetches. Studio users authenticate with their own accounts, with roles that control which documents they may edit or publish on plans that support custom roles. Configure CORS origins for the Studio and any browser clients explicitly, and remove unused origins and tokens during periodic reviews. Webhook secrets and viewer tokens belong in the secret manager, rotated like any other credential.

Localization in Sanity

Sanity supports both localization models through plugins. Document-level internationalization creates a separate document per language, linked through a translation metadata document, which suits pages whose structure differs by market. Field-level internationalization stores values per language in object fields, such as title.en and title.de, which suits content with identical structure across languages. The choice is per document type, as discussed in localization strategies. GROQ’s coalesce() resolves field-level fallback chains in the query, and projecting which language supplied each value keeps fallback notices and hreflang honest. For document-level translations, query the translation metadata to build hreflang clusters from the documents that actually exist in each language.

Error Handling & Resilience

Sanity’s API CDN is highly available, but integrations still fail in predictable ways: a query error after a schema change, a token that lost access, or a reference that points to a deleted document. Validate query results at the data layer, treat missing references as absent rather than crashing, and log query errors with the query name. When the live API is unavailable, preview degrades but published pages keep serving from caches. Keep the API version date pinned in configuration and upgrade it deliberately, since behaviour can change between versions.

Testing & Observability

Run TypeGen in CI and fail on type errors. Keep fixture datasets small and synthetic for integration tests, imported into a test dataset, and test preview with draft documents present. Monitor API usage per dataset and request type in Sanity’s project dashboard, and your own metrics for query latency and webhook handler outcomes.

Sanity APIs and when to use eachThe API CDN, live API, listener and mutation APIs compared on freshness, caching and typical use in a headless integration.EndpointFreshnessTypical useAPI CDNseconds behind publishespublic pages, buildsLive APIimmediatepreview, revalidation fetchesListenerreal timelive preview, dashboardsMutationswritesmigrations, imports, tooling
Public pages read from the CDN; preview and editing tools use the live endpoints.

Worked Example

A media startup used Sanity with a Next.js frontend. Its first integration fetched every page with the live API and no caching, used a write token in the frontend for convenience, and rebuilt the site on a timer. After a traffic spike exhausted its API quota, the team moved public reads to the API CDN with the published perspective, removed the write token entirely, generated types with TypeGen, added GROQ webhooks with signatures and tag-based revalidation, and set up the Presentation tool for editors. API usage fell by more than 90 percent, updates appeared within seconds of publishing, and editors started using click-to-edit for most corrections.

Choosing Sanity

Sanity suits teams that want a highly customizable editing experience defined in code, real-time collaboration and a flexible query language, backed by a hosted content store. Its trade-offs are that the Studio is a React application the team builds and maintains, that GROQ is a new language for most developers, and that usage-based pricing rewards careful caching. Compared with Contentful, it offers more control over the editing interface and schema in code by default; compared with self-hosted options like Strapi and Directus, it removes database operations while keeping the content in a hosted service.

Ownership and Workflow

A Sanity project spans two codebases, the Studio and the frontend, often maintained by the same team but deployed separately. Keep them in one repository or tie their releases together, because schema changes in the Studio and query changes in the frontend must ship in the right order: additive schema first, then queries that use new fields, and removals last after nothing queries them. Assign an owner for the Studio’s configuration and plugins, who reviews changes to editing experience with content leads, since the Studio is a product for editors as much as the frontend is for readers. Content operations own document actions, validation messages and structure, the parts editors meet every day; engineering owns queries, webhooks, TypeGen and caching.

Frequently Asked Questions

Is Sanity Studio required?

It is the editing interface for Sanity’s Content Lake. Most teams customize it; content can also be written through the API by scripts and other tools.

Should the dataset be public or private?

Public datasets allow unauthenticated reads of published content, which is convenient for the API CDN. Private datasets require a token for every read; choose private when content should not be readable by anyone with the project id.

How are drafts represented?

As separate documents with ids prefixed by drafts.. Perspectives handle the overlay, so queries do not need to filter ids manually.

Does Sanity support localization?

Through document-level or field-level internationalization plugins, with the choice made per document type.

Can the Studio be embedded in the frontend application?

Yes. The Studio can be mounted at a route of a Next.js application, which simplifies deployment, or deployed separately. Separate deployment keeps the public site’s bundle and security surface smaller.

How do we migrate content when the schema changes?

With migration scripts using Sanity’s migration tooling, which runs transformations over documents in a dataset, tested first on a copy of production.

How many plugins should a Studio have?

As few as needed to support the editorial workflow. Each plugin adds bundle size and upgrade work; prefer built-in features and a small number of well-maintained plugins, and remove experiments that did not stick, together with their dependencies.

Is GROQ required, or can we use GraphQL?

Sanity offers a GraphQL API deployed from the schema, but GROQ is the primary query language, with projections, joins and TypeGen support. Most teams use GROQ for new projects and keep GraphQL only where existing tooling depends on it.