Sanity Troubleshooting
This guide covers common issues specific to Sanity-powered websites. Since Sanity is a headless CMS, most issues relate to your frontend framework implementation.
Common Issues Overview
Sanity is a headless CMS with real-time collaboration and flexible content structures. Troubleshooting typically involves GROQ queries, content schema, frontend integration, preview mode, and real-time updates.
Common Issue Categories
Performance Issues
- GROQ query optimization
- Image loading with Sanity's CDN
- Real-time preview performance
- Frontend framework SSR/SSG issues
Tracking Issues
- Analytics not firing in preview mode
- Events missing content metadata
- Cross-domain tracking with Sanity Studio
- Real-time content update tracking
Installation Problems
Sanity CLI Setup
Installation Failures
Install Sanity CLI:
# Global install
npm install -g @sanity/cli
# Or use npx
npx @sanity/cli --version
Common errors:
# Permission denied
sudo npm install -g @sanity/cli
# Or use nvm for local install
nvm use 18
npm install -g @sanity/cli
Project Creation Issues
Create new project:
npm create sanity@latest
# Or with CLI
sanity init
If project creation fails:
# Check Node version (needs 14+)
node -v
# Clear npm cache
npm cache clean --force
# Try again
npm create sanity@latest
Client SDK Installation
Install client libraries:
# For React/Next.js
npm install @sanity/client @sanity/image-url next-sanity
# For Vue/Nuxt
npm install @sanity/client @sanity/image-url
# TypeScript definitions
npm install -D @sanity/types
Verify installation:
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'production',
apiVersion: '2024-01-01',
useCdn: true
})
// Test connection
client.fetch('*[_type == "post"][0..2]')
.then(console.log)
.catch(console.error)
Analytics Integration
Next.js Pages Router:
// pages/_app.js
import Script from 'next/script'
export default function App({ Component, pageProps }) {
// Don't track in preview mode
const isPreview = pageProps.preview || false
return (
<>
{!isPreview && (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX`}
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
`}
</Script>
</>
)}
<Component {...pageProps} />
</>
)
}
Next.js App Router:
// app/layout.js
import { draftMode } from 'next/headers'
import Analytics from './analytics'
export default function RootLayout({ children }) {
const { isEnabled } = draftMode()
return (
<html lang="en">
<body>
{!isEnabled && <Analytics />}
{children}
</body>
</html>
)
}
Configuration Issues
| Issue | Symptoms | Common Causes | Solutions |
|---|---|---|---|
| Preview Mode Analytics | Tracking in preview | Preview detection not implemented | Check draftMode and disable analytics |
| GROQ Query Errors | Invalid query syntax | Wrong GROQ syntax | Validate query in Vision plugin |
| Image Not Loading | Broken images | Wrong image URL builder | Use @sanity/image-url |
| CORS Errors | API blocked | CORS not configured | Add domain to CORS settings |
| Schema Validation Failing | Document won't save | Required fields missing | Check schema validation rules |
| Real-Time Not Working | Updates not appearing | Listener not configured | Set up event listener correctly |
| Token Authentication | 401 Unauthorized | Missing/invalid token | Check API token in env variables |
| CDN Stale Content | Old content served | CDN caching | Use useCdn: false or purge cache |
| References Not Resolving | Missing referenced data | References not expanded | Add references to GROQ projection |
| Webhook Not Triggering | Deployment not triggered | Webhook misconfigured | Verify webhook URL and secret |
Debugging with Developer Tools
Sanity Studio Debugging
Vision Plugin (GROQ Playground)
Install Vision:
npm install @sanity/vision
Add to sanity.config.js:
import {defineConfig} from 'sanity'
import {visionTool} from '@sanity/vision'
export default defineConfig({
name: 'default',
title: 'My Project',
projectId: 'your-project-id',
dataset: 'production',
plugins: [visionTool()]
})
Test GROQ queries:
- Open Studio
- Click Vision tab
- Write query:
*[_type == "post"] {
_id,
title,
slug,
author-> {
name,
image
}
}
- Click "Fetch" to see results
Schema Validation
Test schema in Studio:
// schemas/post.js
export default {
name: 'post',
title: 'Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
validation: Rule => Rule.required().min(10).max(80)
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
validation: Rule => Rule.required()
}
],
preview: {
select: {
title: 'title',
subtitle: 'slug.current'
}
}
}
Check for errors:
- Open Studio
- Try creating/editing document
- Look for validation messages
Frontend Debugging
GROQ Query Logging
Log queries and performance:
import {createClient} from '@sanity/client'
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2024-01-01',
useCdn: true,
})
// Wrapper with logging
async function fetchWithLog(query, params = {}) {
console.log('GROQ Query:', query)
console.log('Parameters:', params)
const start = performance.now()
try {
const result = await client.fetch(query, params)
const duration = performance.now() - start
console.log(`Query took ${duration.toFixed(2)}ms`)
console.log('Result:', result)
return result
} catch (error) {
console.error('Query failed:', error)
throw error
}
}
// Usage
const posts = await fetchWithLog(`*[_type == "post"][0...10]`)
Network Tab Inspection
Monitor Sanity API calls:
- Open DevTools → Network
- Filter:
api.sanity.ioorcdn.sanity.io - Look for:
/v1/data/query/- GROQ queries/v1/data/listen- Real-time updates/images/- Image CDN
Check request:
https://projectid.api.sanity.io/v1/data/query/production?query=*[_type=="post"]
Headers:
Authorization: Bearer <token>
Response:
{ "result": [...], "ms": 42 }
Browser Console Debugging
Make client available:
if (typeof window !== 'undefined') {
window.sanityClient = client
console.log('Sanity client available at window.sanityClient')
}
// Test in console:
// const posts = await window.sanityClient.fetch('*[_type == "post"][0..2]')
Platform-Specific Challenges
Preview Mode Tracking
Disable Analytics in Preview
Next.js Pages Router:
// lib/sanity.js
export async function getClient(preview) {
return createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2024-01-01',
useCdn: !preview, // Don't use CDN in preview
token: preview ? process.env.SANITY_API_TOKEN : undefined
})
}
// pages/posts/[slug].js
export async function getStaticProps({ params, preview = false }) {
const client = getClient(preview)
const post = await client.fetch(query, { slug: params.slug })
return {
props: {
post,
preview
}
}
}
// pages/_app.js
export default function App({ Component, pageProps }) {
const isPreview = pageProps.preview || false
useEffect(() => {
if (isPreview) {
console.log('Preview mode - analytics disabled')
return
}
// Initialize analytics
if (typeof gtag !== 'undefined') {
gtag('config', 'G-XXXXXXXXXX')
}
}, [isPreview])
return <Component {...pageProps} />
}
App Router:
// app/layout.js
import { draftMode } from 'next/headers'
export default function RootLayout({ children }) {
const { isEnabled } = draftMode()
return (
<html>
<body>
{!isEnabled && <Analytics />}
{children}
{isEnabled && <PreviewBanner />}
</body>
</html>
)
}
Real-Time Updates
Track Content Changes
Listen to real-time updates:
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'production',
apiVersion: '2024-01-01',
useCdn: false, // Must be false for real-time
token: process.env.SANITY_API_TOKEN // Required for listen
})
// Listen to post changes
const query = '*[_type == "post"]'
const subscription = client.listen(query).subscribe({
next: (update) => {
console.log('Real-time update:', update)
if (typeof gtag !== 'undefined') {
gtag('event', 'content_update', {
'content_type': update.result?._type,
'content_id': update.documentId,
'transition': update.transition // 'update', 'appear', 'disappear'
})
}
},
error: (err) => console.error('Listen error:', err)
})
// Cleanup
// subscription.unsubscribe()
GROQ Query Optimization
Tracking Query Performance
Optimize complex queries:
// Bad - fetches everything
const posts = await client.fetch(`
*[_type == "post"] {
...,
author->,
categories[]->
}
`)
// Good - specific fields only
const posts = await client.fetch(`
*[_type == "post"] {
_id,
title,
slug,
publishedAt,
"authorName": author->name,
"categoryNames": categories[]->title
}[0...10]
`)
// Even better - with pagination
const posts = await client.fetch(`
*[_type == "post"] | order(publishedAt desc) [0...10] {
_id,
title,
slug,
publishedAt,
"authorName": author->name,
"excerpt": pt::text(body)[0...200]
}
`)
Track query performance:
async function trackQuery(queryName, queryFn) {
const start = performance.now()
try {
const result = await queryFn()
const duration = performance.now() - start
if (typeof gtag !== 'undefined') {
gtag('event', 'query_performance', {
'query_name': queryName,
'duration_ms': Math.round(duration),
'result_count': result?.length || 0
})
}
return result
} catch (error) {
if (typeof gtag !== 'undefined') {
gtag('event', 'query_error', {
'query_name': queryName,
'error_message': error.message
})
}
throw error
}
}
// Usage
const posts = await trackQuery('fetch_posts', () =>
client.fetch(`*[_type == "post"][0...10]`)
)
Content Lake Tracking
Track Document Views
Track specific content:
async function trackContentView(documentType, documentId) {
try {
// Fetch document metadata
const doc = await client.fetch(
`*[_type == $type && _id == $id][0] {
_id,
_type,
title,
publishedAt
}`,
{ type: documentType, id: documentId }
)
if (typeof gtag !== 'undefined') {
gtag('event', 'content_view', {
'content_type': doc._type,
'content_id': doc._id,
'content_title': doc.title,
'published_date': doc.publishedAt
})
}
} catch (error) {
console.error('Content tracking failed:', error)
}
}
// Usage in component
useEffect(() => {
trackContentView('post', postId)
}, [postId])
Error Messages and Solutions
Common Sanity Errors
"Unable to connect to Sanity API"
Error:
Could not connect to Sanity API
Check configuration:
// Verify environment variables
console.log('Project ID:', process.env.NEXT_PUBLIC_SANITY_PROJECT_ID)
console.log('Dataset:', process.env.NEXT_PUBLIC_SANITY_DATASET)
// Test connection
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2024-01-01',
useCdn: true
})
client.fetch('*[_type == "post"][0]')
.then(() => console.log('✓ Connected'))
.catch(err => console.error('✗ Connection failed:', err))
"Invalid GROQ query"
Error:
{"error":"Invalid query","message":"Query is invalid"}
Test query in Vision:
- Open Sanity Studio
- Go to Vision tab
- Paste query
- Fix syntax errors
Common mistakes:
# Wrong - missing quotes
*[_type == post]
# Correct
*[_type == "post"]
# Wrong - incorrect reference syntax
*[_type == "post"]{author}
# Correct
*[_type == "post"]{author->}
"Document validation failed"
Error:
Document failed validation: "title" is required
Check schema:
// Ensure required fields are provided
{
name: 'title',
type: 'string',
validation: Rule => Rule.required()
}
Handle in frontend:
// Defensive querying
const posts = await client.fetch(`
*[_type == "post" && defined(title) && defined(slug)]
`)
Performance Problems
Slow GROQ Queries
Problem: Queries taking too long
Solutions:
- Use projections:
*[_type == "post"] {
_id,
title,
slug
}
- Limit results:
*[_type == "post"][0...10]
- Filter efficiently:
*[_type == "post" && publishedAt < now()][0...10]
- Avoid deep references:
# Instead of multiple levels
author->company->address->city
# Fetch directly
"companyCity": author->company->address->city
Image Optimization
Use Sanity's image pipeline:
import imageUrlBuilder from '@sanity/image-url'
const builder = imageUrlBuilder(client)
function urlFor(source) {
return builder.image(source)
}
// Usage
<img
src={urlFor(post.mainImage)
.width(800)
.height(600)
.fit('crop')
.auto('format')
.url()}
alt={post.mainImage.alt}
loading="lazy"
/>
Next.js Image component:
import Image from 'next/image'
import {useNextSanityImage} from 'next-sanity-image'
function SanityImage({ asset }) {
const imageProps = useNextSanityImage(client, asset)
return (
<Image
{...imageProps}
layout="responsive"
sizes="(max-width: 800px) 100vw, 800px"
/>
)
}
When to Contact Support
Sanity Support
Contact when:
- API issues or downtime
- Billing questions
- Security concerns
- Feature requests
Support channels:
- Slack Community: https://slack.sanity.io
- GitHub Discussions: https://github.com/sanity-io/sanity/discussions
- Email support (paid plans)
- Documentation: https://www.sanity.io/docs
When to Hire a Developer
Complex scenarios:
- Custom Studio plugins
- Advanced schema architectures
- Real-time collaboration features
- Custom input components
- Complex GROQ queries
- Performance optimization at scale
- Migration from other CMS
- Custom deployment workflows
Advanced Troubleshooting
Development Environment Setup
Complete .env.local:
# Required
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"
# For authenticated requests
SANITY_API_TOKEN="your-token"
# For preview mode
SANITY_PREVIEW_SECRET="your-secret"
# For webhooks
SANITY_WEBHOOK_SECRET="your-webhook-secret"
Generate types:
# Install schema codegen
npm install -D sanity-codegen
# Generate types
npx sanity-codegen
# Use in TypeScript
import type {Post} from '../generated/schema'
const post: Post = await client.fetch(...)
Webhook Debugging
Test webhook:
// pages/api/revalidate.js
export default async function handler(req, res) {
// Validate secret
if (req.body.secret !== process.env.SANITY_WEBHOOK_SECRET) {
return res.status(401).json({ message: 'Invalid secret' })
}
console.log('Webhook received:', {
type: req.body._type,
id: req.body._id
})
try {
// Revalidate specific path
await res.revalidate(`/posts/${req.body.slug.current}`)
// Track webhook
if (typeof gtag !== 'undefined') {
gtag('event', 'webhook_revalidate', {
'content_type': req.body._type,
'content_id': req.body._id
})
}
return res.json({ revalidated: true })
} catch (err) {
return res.status(500).send('Error revalidating')
}
}
Related Global Guides
For platform-agnostic troubleshooting, see: