How to filter blog posts by category in NextJS with Sanity Studio?
Great question! Filtering blog posts by category is a super common need, and the good news is it's totally doable with GROQ and Next.js. Let me walk you through a couple of approaches.
The Key: Using the references() Function
Since your blog posts reference category documents, you'll use GROQ's references() function to filter posts. Here's the basic pattern:
*[_type == "post" && references($categoryId)]This query finds all posts that reference a specific category ID that you pass in as a parameter.
Approach 1: Dynamic Route (Recommended)
Create a category page using Next.js dynamic routes. For App Router, create app/category/[slug]/page.tsx:
import { client } from '@/sanity/lib/client'
import { defineQuery } from 'next-sanity'
const POSTS_BY_CATEGORY_QUERY = defineQuery(`
*[_type == "post" && references(*[_type == "category" && slug.current == $slug][0]._id)]
| order(publishedAt desc) {
_id,
title,
slug,
publishedAt,
excerpt,
category->{ title, slug }
}
`)
export default async function CategoryPage({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const posts = await client.fetch(POSTS_BY_CATEGORY_QUERY, { slug })
// Render your filtered posts
return (
<div>
<h1>Posts in {slug}</h1>
{posts.map(post => (
<article key={post._id}>{post.title}</article>
))}
</div>
)
}Your category links would then be /category/basics, /category/how-to, etc.
Approach 2: Query Parameters
Alternatively, use query parameters on your main blog page:
// app/blog/page.tsx
export default async function BlogPage({
searchParams
}: {
searchParams: Promise<{ category?: string }>
}) {
const { category } = await searchParams
const query = category
? `*[_type == "post" && references(*[_type == "category" && slug.current == $category][0]._id)] | order(publishedAt desc)`
: `*[_type == "post"] | order(publishedAt desc)`
const posts = await client.fetch(query, { category })
// Render posts...
}Then your category links use query params: /blog?category=basics
Getting Your Category List
You'll also want to fetch all categories to display as filter options:
*[_type == "category"] | order(title asc) {
_id,
title,
slug,
"postCount": count(*[_type == "post" && references(^._id)])
}The count() function with references(^._id) gives you the number of posts per category, which is handy for showing post counts.
Rendering Category Links
<nav className="category-filters">
<Link href="/blog">All Posts</Link>
{categories.map((cat) => (
<Link key={cat._id} href={`/category/${cat.slug.current}`}>
{cat.title} ({cat.postCount})
</Link>
))}
</nav>Pro Tips
- Exclude drafts: Add
&& !(_id in path("drafts.*"))to your filters if you want to exclude draft posts - Use the
->operator: When you need to dereference and access properties of the referenced category, usecategory->{ title, slug } - TypeScript support: Wrap your queries with
defineQuery()for better type inference
The Sanity community answers have some great additional examples of these patterns in action!
Show original thread4 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.