GROQ query for blogPosts referencing draft category documents

18 replies
Last updated: Jan 28, 2026
Hey all, I have an issue with a GROQ query.
There are 2 schemas:
blogPost
and
category
. Each document of type
blogPost
references to a
category
.Now, if I edit a category, a new copy of the published document is created with id
drafts.[id_of_published_doc]

I want to be able to display all the
blogPost
documents for a draft category — what is the best way to query all
blogPost
that reference a category, even when the current category is a draft and not the published document?
Before I found out about this "draft" issue, the query was relying on matching the
_ref
with the
_id


*[_type == "category"] {
	name,
	"allBlogPosts": *[_type == "blogPost" && category._ref == ^._id ] {
		title,
	}
}
So far I tried to rely of draft and published versions to have the same
slug
, but the query doesn't seem to work:
*[_type == "category"] {
	name,
	"allBlogPosts": *[_type == "blogPost" && category->slug.current == ^.slug.current ] {
		title,
	}
}

AI Update

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 published perspective to show only published content
  • Preview/Draft: Use the previewDrafts perspective 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.

Show original thread
18 replies

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.

Was this answer helpful?