How to filter blog posts by category in NextJS with Sanity Studio?

4 replies
Last updated: Dec 14, 2021
So I've managed to get my blog posts and data from Studio into my NextJS blog. I've created various categories for blog posts and would like to allow my users to both sort and filter by category. Hiwev, I'm stumped as to how! The blog page has a nice list of categories (Basics, How to, Book, etc.) I'd like users to be able to click, say, "Basics" and only see those posts. I've tried searching for tutorials and guidance but am coming up short.
Any tips or links to some how-to's that I can follow?

Thanks!
AI Update

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.

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.

<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

  1. Exclude drafts: Add && !(_id in path("drafts.*")) to your filters if you want to exclude draft posts
  2. Use the -> operator: When you need to dereference and access properties of the referenced category, use category->{ title, slug }
  3. 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 thread
4 replies
Hey Rich! How are you indicating which category a blog post belongs to? Is it referencing a category document?
Yes, it's referencing a category document. I have a (small for now) set of categories and select one or more within each post, referencing the category document.
Got it. Here's an example of checking for other posts that reference the same category of the current post:
*[_type == 'post']{
  ...,
  'relatedPosts': *[_type == 'post' && ^.category._ref match category._ref]
}
Thanks,
user M
. I probably won't get to this today but I'll try it out tomorrow. Appreciate the help.

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?