
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
| Mistake | Symptom | Fix |
|---|---|---|
| CDN on builds and revalidation | Stale pages after publishing | useCdn: false for those paths |
... in production queries | Large payloads, vague types | Explicit projections |
| String-concatenated queries | Broken queries, injection risk | GROQ parameters |
No [] on reference arrays | null instead of data | field[]-> |
| Unvalidated slugs | 404s, duplicate URLs | Required, unique slugs |
| Token in client code | Private content exposed | Server-only clients |
| Drafts on the public site | Unfinished content visible | published perspective |
| No alt text | Accessibility and SEO gaps | Required alt field |
_updatedAt as publish date | Dates jump on every edit | publishedAt field |
| Renaming fields without migrating | Content seems to vanish | Add, migrate, deprecate |
No pinned apiVersion | Behaviour changes unexpectedly | Dated API version |
| Testing on production | Real content damaged | Separate datasets |
| No backups | Unrecoverable mistakes | Scheduled exports |
| Original image URLs | Huge downloads | Image 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.


