Querying for specific categories and subcategories in Sanity.io

12 replies
Last updated: Jan 17, 2023
Hey guys! I need to get all posts related to a specific category and its subcategoriesI do not understand how to do this in one request, now it only works if I hardcode the value of the subcategory in
references(…)
AI Update

I see you're trying to get all posts related to a specific category and its subcategories in one query. This is a common challenge with hierarchical data in Sanity!

The issue is that GROQ doesn't have built-in recursive queries, so you need to handle the hierarchy manually. Here are a couple of approaches:

Approach 1: Two-step query with subcategory IDs

First, fetch the parent category and collect all its subcategory IDs, then use those to filter posts:

*[_type == "blogPostTag" && key.current == "digital-marketing"][0]{
  _id,
  title,
  "subcategoryIds": [_id] + *[_type == "blogPostTag" && references(^._id)]._id,
  "posts": *[_type == "localizedBlogPostEntry" && count((tags[]->_id)[@ in ^.subcategoryIds]) > 0]
}

This query:

  1. Gets your parent category
  2. Creates an array of IDs including the parent (_id) and all subcategories that reference it
  3. Filters posts where any of their tags match those IDs

Approach 2: Using a subquery in the filter

You can also inline the subcategory lookup directly in the posts filter:

*[_type == "blogPostTag" && key.current == "digital-marketing"][0]{
  _id,
  title,
  "posts": *[_type == "localizedBlogPostEntry" && (
    references(^._id) || 
    count((tags[]->_id)[@ in *[_type == "blogPostTag" && references(^._id)]._id]) > 0
  )]
}

This checks if posts reference either the parent category directly OR any of its subcategories.

Important notes:

  • Since your schema has parentCategory as a reference field on subcategories pointing to parents, you're querying "bottom-up" (subcategories reference parents)
  • The references() function checks if a document references another, which works perfectly for your use case
  • The count() and array filtering syntax [@ in array] lets you check if any tag matches your subcategory list

The key insight from the reference fields documentation is that references are bidirectional when queried, so you can traverse them from either direction even though the reference field only appears in the Studio where it's defined.

If you need to go deeper than one level (subcategories of subcategories), you'd need to either flatten your data structure or handle multiple levels explicitly in your query, as GROQ doesn't support recursive traversal natively.

*[_type == "blogPostTag" && key.current == "digital-marketing"][0]{
  "posts": *[_type == "localizedBlogPostEntry" && references(^._id, "427f4454-22f7-40a0-8d88-db31fb20ad7b")],
  "subCategories": *[_type == "blogPostTag" && references(^._id)]._id
}
Another way but still with hardcoded value…
It's a little difficult without seeing how you have your schema structured. Check the groq cheatsheet to see if it helps or post your schema and I can take a look.
https://www.sanity.io/docs/query-cheat-sheet
BlogPostTag (category and sub-category)
export default {
  name: 'blogPostTag',
  title: 'Blog tag',
  type: 'document',
  preview: {
    select: {
      title: 'title',
      key: 'key',
      isParent: 'isParent',
      parentCategory: 'parentCategory.title'
    },
    prepare({ title, key, isParent, parentCategory }) {
      return {
        title: title,
        subtitle: `${
          isParent
            ? `/blog/tag/${key.current}`
            : parentCategory
              ? `Sub-category of ${parentCategory}`
              : `The parent category is not set!`
        }`,
        media: isParent ? ParentCategoryIcon : AiOutlineTag
      };
    }
  },
  fieldsets: [
    {
      title: 'SEO & metadata',
      name: 'metadata',
      options: { collapsible: true, collapsed: true }
    }
  ],
  fields: [
    {
      name: 'title',
      title: 'Title',
      type: 'string'
    },
    { ...keyFieldOptions },
    {
      name: 'description',
      title: 'description',
      type: 'text'
    },
    {
      title: 'Cover Image',
      name: 'coverImage',
      type: 'image'
    },
    {
      title: 'Icon Image',
      name: 'iconImage',
      type: 'image'
    },
    {
      title: 'Parent category?',
      name: 'isParent',
      type: 'boolean'
    },
    {
      title: 'Link to parent category',
      name: 'parentCategory',
      type: 'reference',
      to: [{ type: 'blogPostTag' }],
      hidden: ({ parent }) => !!parent.isParent
    },
    {
      title: 'Meta title',
      name: 'metaTitle',
      type: 'string',
      description: 'This title populates meta-tags on the webpage',
      fieldset: 'metadata'
    },
    {
      title: 'Meta description',
      name: 'metaDescription',
      type: 'text',
      description: 'This description populates meta-tags on the webpage',
      fieldset: 'metadata'
    },
    {
      title: 'Open Graph Image',
      name: 'openGraphImage',
      type: 'image',
      description: 'Image for sharing previews on Facebook, Twitter etc.',
      fieldset: 'metadata'
    }
  ]
};
Blog post schema
export default {
  name: 'localizedBlogPostEntry',
  title: 'Blog Post',
  type: 'document',
  preview: createPreview(),
  fieldsets: [
    {
      title: 'Blog taxonomy',
      name: 'taxonomy'
    },
    {
      title: 'SEO & Social media',
      name: 'metadata',
      options: { collapsible: true, collapsed: true }
    }
  ],
  fields: [
    {
      title: 'Title',
      name: 'textTitle',
      type: 'string'
    },
    {
      ...keyFieldOptions
    },
    {
      title: 'Content (New)',
      name: 'rte',
      ...richTextEditor
    },
    {
      title: 'Release date',
      name: 'releaseDate',
      type: 'datetime',
      validation: (Rule) => Rule.required()
    },
    {
      title: 'Author',
      name: 'author',
      type: 'reference',
      to: { type: 'blogPostAuthor' },
      validation: (Rule) => Rule.required()
    },
    {
      title: 'Category',
      name: 'tags',
      type: 'array',
      of: [{ type: 'reference', to: { type: 'blogPostTag' } }],
      validation: (Rule) => Rule.required().min(1),
      fieldset: 'taxonomy'
    }
  ]
};
*[_type == localizedBlogPostEntry]{
  "categories": tags[] ->

I'm not sure why you used different names than the titles. This makes it very confusing because I'm not sure what you are calling categories and what you are calling subcategories. The query is called on the
name
of the document.
I think that will work for what you want though.
It is legacy names. Category and sub-category are same document type - blogPostTag
No, your query will return all the posts
I need to fetch specific category with all posts related to that specific category and its subcategories
*[_type == category && categoryName == "name"] {
"subcategory": subcategoryName -> {
"posts": postName[ ] -> }}
Thanks! I will check if it works for me

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?