Type something to search...
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, shipping API tokens in client bundles, letting drafts leak onto the public site, skipping alt text, using _updatedAt as a publish date, and renaming fields without migrating the data. Each one is easy to make and easy to fix once you know what to look for. Most of them don't show up in development, which is why they tend to surface after launch.

This guide works through fourteen mistakes in the same format: what the mistake looks like, why it hurts, and how to fix it. They're grouped into querying, schema and content, security, and operations. If you want the positive version of this list, read Sanity best practices for production projects alongside it.

Mistakes at a Glance

MistakeSymptomFix
CDN on builds and revalidationStale pages after publishinguseCdn: false for those paths
... in production queriesLarge payloads, vague typesExplicit projections
String-concatenated queriesBroken queries, injection riskGROQ parameters
No [] on reference arraysnull instead of datafield[]->
Unvalidated slugs404s, duplicate URLsRequired, unique slugs
Token in client codePrivate content exposedServer-only clients
Drafts on the public siteUnfinished content visiblepublished perspective
No alt textAccessibility and SEO gapsRequired alt field
_updatedAt as publish dateDates jump on every editpublishedAt field
Renaming fields without migratingContent seems to vanishAdd, migrate, deprecate
No pinned apiVersionBehaviour changes unexpectedlyDated API version
Testing on productionReal content damagedSeparate datasets
No backupsUnrecoverable mistakesScheduled exports
Original image URLsHuge downloadsImage URL builder

Querying Mistakes

1. Using the CDN Where You Need Fresh Data

The mistake: one shared client with useCdn: true is used everywhere, including static builds and the handler that runs after a webhook.

Why it hurts: the API CDN can briefly serve the previous version of a document. If a webhook triggers a rebuild or revalidation and that fetch hits the CDN, the page is regenerated with the old content and stays stale until the next change.

The fix: use the live API for anything triggered by a publish, and for builds:

// Public, high-traffic reads
export const client = createClient({ projectId, dataset, apiVersion: '2025-02-19', useCdn: true })

// Builds, revalidation and preview
export const freshClient = client.withConfig({ useCdn: false })

Sanity CDN vs live API explains when each endpoint is the right choice.

2. Over-Fetching With the Spread Operator

The mistake: queries like *[_type == "post"]{ ... }, or no projection at all, on listing pages.

Why it hurts: every post comes back with its full Portable Text body, image crop data and system fields. Payloads grow, pages slow down and TypeGen produces types that don't reflect what the component uses.

The fix: project only what the page renders, and keep separate list and detail queries:

*[_type == "post" && defined(slug.current)] | order(publishedAt desc)[0...12]{
  _id,
  title,
  "slug": slug.current,
  publishedAt,
  "imageUrl": mainImage.asset->url
}

Using projections in GROQ queries covers renaming, nesting and conditional fields.

3. Building Queries With String Concatenation

The mistake: client.fetch(`*[slug.current == "${slug}"][0]`).

Why it hurts: a quote in the value breaks the query, user input can change what the query does, and every variation is a different query string, which hurts caching.

The fix: pass values as parameters:

const post = await client.fetch(
  `*[_type == "post" && slug.current == $slug][0]{ title, body }`,
  { slug },
)

See how to use GROQ parameters for arrays, dates and optional filters.

4. Forgetting [] When Dereferencing Arrays

The mistake: "categories": categories->title on an array of references.

Why it hurts: the result is null, with no error, so it looks like the data is missing.

The fix: write categories[]->title, or categories[]->{ title, "slug": slug.current } for objects. How to dereference documents in GROQ covers the other reference patterns.

Schema and Content Mistakes

5. Leaving Slugs Unvalidated

The mistake: a slug field with no validation, so editors publish documents without one, or two posts share the same slug.

Why it hurts: documents without slugs produce broken links or null paths in your sitemap, and duplicates mean one page silently replaces another.

The fix: make slugs required, and let Sanity's built-in uniqueness check run (it's on by default for the slug type unless you override isUnique):

defineField({
  name: 'slug',
  type: 'slug',
  options: { source: 'title', maxLength: 96 },
  validation: (rule) => rule.required(),
})

Also guard queries with defined(slug.current), because documents created through the API skip Studio validation. Custom slugs in Sanity shows how to customise generation and uniqueness, for example per language.

6. Missing Alt Text on Images

The mistake: image fields with no alt subfield, or one that's optional and always empty.

Why it hurts: screen reader users get no description, search engines lose context, and accessibility audits fail.

The fix: add an alt field to every content image and validate it, while letting decorative images opt out:

defineField({
  name: 'mainImage',
  type: 'image',
  options: { hotspot: true },
  fields: [
    defineField({
      name: 'alt',
      title: 'Alternative text',
      type: 'string',
      description: 'Describe the image for people who cannot see it.',
      validation: (rule) => rule.required(),
    }),
  ],
})

Then render it: alt={image.alt ?? ''}. Image alt text in Sanity covers decorative images and AI-assisted descriptions.

7. Using _updatedAt as the Publish Date

The mistake: showing "Published on" with _updatedAt, or sorting a blog by it.

Why it hurts: _updatedAt changes on every edit to the published document. Fixing a typo moves an old post to the top of the feed. Bulk operations make it worse: a migration, an import, or strengthening references when migrating datasets can give hundreds of documents the same new timestamp.

The fix: add a dedicated publishedAt field with an initial value, and use _updatedAt only for things like a sitemap's lastmod:

defineField({
  name: 'publishedAt',
  type: 'datetime',
  initialValue: () => new Date().toISOString(),
  validation: (rule) => rule.required(),
})

8. Renaming Fields Without a Migration

The mistake: changing name: 'summary' to name: 'excerpt' in the schema and deploying.

Why it hurts: existing documents still store summary. The Studio shows an "Unknown field" warning, the new excerpt field is empty everywhere, and your front end renders blanks. The data isn't lost, but it looks lost.

The fix: add the new field, migrate the content, then retire the old one:

// migrations/rename-summary/index.ts
import { defineMigration, at, setIfMissing, unset } from 'sanity/migrate'

export default defineMigration({
  title: 'Rename summary to excerpt',
  documentTypes: ['post'],
  filter: 'defined(summary)',
  migrate: {
    document(doc) {
      return [at('excerpt', setIfMissing(doc.summary)), at('summary', unset())]
    },
  },
})

Run it as a dry run first with npx sanity migration run rename-summary, then add --no-dry-run when the output looks right. Sanity content migrations covers the full process.

9. Modelling Pages Instead of Content

The mistake: one document type per page, with fields like heroTitle and leftColumnText.

Why it hurts: content can't be reused, and every redesign means a schema change and a migration.

The fix: model the things your content describes and let pages display them. Content modelling best practices explains how.

Security Mistakes

10. Shipping Tokens in Client Bundles

The mistake: NEXT_PUBLIC_SANITY_TOKEN, or a token passed to a client used in a 'use client' component.

Why it hurts: anything with a public prefix is inlined into JavaScript that every visitor downloads. Anyone can copy the token and read drafts, private datasets or, with a write token, change your content.

The fix: keep tokens in server-only environment variables and server-only modules:

// src/sanity/lib/token.ts
import 'server-only'

export const token = process.env.SANITY_API_READ_TOKEN
if (!token) throw new Error('Missing SANITY_API_READ_TOKEN')

If a token has ever been in a client bundle, revoke it in the project settings and create a new one. Search your build output for sk followed by a long string to check. How to secure your Sanity project covers token roles, CORS and webhook secrets.

11. Letting Drafts Reach the Public Site

The mistake: querying with an authenticated client and the raw or drafts perspective for public pages, or using an API version older than 2025-02-19 with a token.

Why it hurts: unfinished content, including documents editors never intended to publish, appears on the live site, sometimes twice when both the draft and published version match a query.

The fix: use the published perspective for public requests, which is the default from API version 2025-02-19, and only switch to drafts inside Draft Mode:

const isDraft = (await draftMode()).isEnabled

const data = await client.fetch(QUERY, params, {
  perspective: isDraft ? 'drafts' : 'published',
  token: isDraft ? token : undefined,
  useCdn: !isDraft,
})

Live preview with Sanity shows the full Draft Mode setup.

Operations Mistakes

12. Not Pinning apiVersion

The mistake: no apiVersion, '1', or new Date().toISOString().split('T')[0].

Why it hurts: the API version controls behaviour such as the default perspective. A floating version means a redeploy can quietly change your results.

The fix: pin a dated version like '2025-02-19' and bump it deliberately after reading the changelog.

13. Testing on Production and Skipping Backups

The mistake: running migration scripts or schema experiments against production, with no recent export.

Why it hurts: a script with a bad filter can change thousands of documents in seconds. Document history can restore individual documents for a limited time, but it isn't built for bulk recovery and it doesn't cover a deleted dataset. Document history and revisions explains those limits.

The fix: develop against a separate dataset, copy production before risky changes, and schedule exports:

npx sanity datasets export production backups/production-$(date +%F).tar.gz

See managing datasets and environments and Sanity backup and restore.

14. Serving Original Image Files

The mistake: rendering asset->url directly in an <img> tag.

Why it hurts: editors upload 6,000-pixel photos. Every visitor downloads several megabytes, and the hotspot and crop editors set are ignored.

The fix: build URLs with @sanity/image-url, request the size you display and let Sanity choose the format:

urlFor(post.mainImage).width(800).height(450).fit('crop').auto('format').url()

How to optimise Sanity images covers responsive srcset and next/image loaders.

How to Catch These Early

Most of these mistakes can be caught before they reach production:

  • TypeGen flags null-able fields and broken projections at build time.
  • Validation rules stop editors publishing documents without slugs, dates or alt text.
  • A quick GROQ audit in Vision finds damaged content, for example *[_type == "post" && !defined(slug.current)]._id.
  • Bundle checks in CI can fail the build if a token pattern appears in client JavaScript.
  • Code review of schema changes catches renames that need a migration.

FAQ: Common Sanity Mistakes

The most likely cause is that your build or revalidation request is hitting the API CDN, which can briefly serve the previous version. Use a client with useCdn set to false for builds and webhook-triggered revalidation, and check that your webhook revalidates the right cache tags.

No. The data is still stored under the old field name, which is why the Studio shows an unknown field warning. Restore the old field definition, or write a migration that copies the value into the new field and unsets the old one.

Check whether it's stored in a variable with a public prefix such as NEXT_PUBLIC_, PUBLIC_ or VITE_, and search your built client JavaScript for it. If it has been exposed at any point, revoke it in the project settings and create a new token.

Your public queries are probably using a token with the raw or drafts perspective, or an older API version. Use the published perspective for public requests, which is the default from API version 2025-02-19, and only fetch drafts inside Draft Mode.

It's fine while prototyping and inside small nested objects where you need the full structure, such as Portable Text blocks. For production listing and detail queries, name the fields you need so payloads stay small and types stay precise.

Yes, when you mean the last time the content changed, such as a sitemap's lastmod or a cache key. It's the wrong choice for a visible publish date or for sorting a feed, because any edit or bulk operation changes it.


Conclusion

Most Sanity mistakes come from a small gap between how something behaves in development and how it behaves with real editors, real traffic and real content. The CDN serves slightly old data, validation only runs in the Studio, renamed fields leave data behind, and tokens with public prefixes end up in the browser. None of these are hard to fix once you know the cause.

Work through the table at the top of this guide on your current project, add the validation and TypeGen checks that catch problems early, and keep a recent export in case something slips through anyway. For the bigger picture of how to set up a project so these mistakes don't happen in the first place, see Sanity best practices.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper
How to Deploy Sanity Studio to the Web

How to Deploy Sanity Studio to the Web

The quickest way to deploy Sanity Studio is to run npx sanity deploy from your Studio folder. The CLI builds the Studio, asks you to choose a h

Dive Deeper