Querying for draft versions of referenced documents in Sanity.io
The issue you're running into is a classic challenge with Sanity's draft/published document architecture. When a category is edited, it creates a separate draft document with the ID drafts.category-123, but your blogPost references still point to the published ID (category-123). Your original query breaks because you're trying to match drafts.category-123 with references pointing to category-123.
The best solution: Use the previewDrafts perspective
Instead of trying to manipulate IDs in your GROQ query, leverage Sanity's perspectives feature. The previewDrafts perspective is designed specifically for this use case—it automatically prioritizes draft documents over published ones when both exist.
When you query with the previewDrafts perspective, Sanity handles the draft/published logic for you. Your original simple query will work perfectly:
*[_type == "category"] {
name,
"allBlogPosts": *[_type == "blogPost" && category._ref == ^._id] {
title,
}
}To use this perspective, add it to your API request:
// Using @sanity/client
const query = `*[_type == "category"] { ... }`
const result = await client.fetch(query, {}, { perspective: 'previewDrafts' })With previewDrafts, when you query for categories, Sanity returns the draft version if it exists (with ID drafts.category-123), but the magic is that references automatically resolve correctly. The blogPost's category._ref pointing to category-123 will properly match the draft category because Sanity understands the draft/published relationship.
Why your slug-based query didn't work
Your second attempt using category->slug.current == ^.slug.current was close conceptually, but the dereferencing operator (->) follows the actual reference stored in the blogPost. Since references point to the published document ID, you're dereferencing to the published category, not the draft you're currently iterating over. This creates a mismatch.
Alternative: Manual ID matching (if you can't use perspectives)
If you're in a situation where you can't use perspectives (like in certain webhook contexts), you can manually handle the ID matching by stripping the drafts. prefix:
*[_type == "category"] {
name,
"publishedId": select(
_id match "drafts.*" => string::split(_id, "drafts.")[1],
_id
),
"allBlogPosts": *[_type == "blogPost" && category._ref == ^.publishedId] {
title,
}
}This works because references in Sanity always point to the published document ID (without the drafts. prefix), even when referencing documents that only exist as drafts.
For production vs. preview environments
Use different perspectives based on your environment:
- Production: Use the
publishedperspective to show only published content - Preview/Draft: Use the
previewDraftsperspective to show draft content
This gives you a clean separation without complex query logic, and it's the approach Sanity recommends for presenting and previewing content.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.