How to filter blog posts by category in NextJS with Sanity Studio?
Any tips or links to some how-to's that I can follow?
Thanks!
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
Was this answer helpful?
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.