
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeGreat 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.
references() FunctionSince 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.
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.
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
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.
<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>&& !(_id in path("drafts.*")) to your filters if you want to exclude draft posts-> operator: When you need to dereference and access properties of the referenced category, use category->{ title, slug }defineQuery() for better type inferenceThe Sanity community answers have some great additional examples of these patterns in action!
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store