Adding Categories with Sanity.io and Next.js 14

Category bot

Adding Categories with Sanity.io and Next.js 14

I start having a bit more content, and some upgrades are necessary. Last time, I created a"Load More" button, and now it's time to set some categories for the blog. It's more complex than it looks like, and it required several steps to be done properly. I already had a category type in Sanity, but it was not comprehensive enough for my needs.

What needs to be done

  • Add category slug property in the Sanity category

  • Update query function for getting article by slug and getting category content

  • Create category page in NextJs. This one is almost a copy/paste of the article page with an updated get all article query that also takes a category slug as optional parameter.

  • Adding category tags in article

  • Add a new sitemap endpoint

And voilà, a pretty category page:

Tags at the beginning of the article:

Code snippets

These are the parts that created the most issues for me.

Updated category schema for Sanity. Small little tweak, I also used the prepare() function so I can see which categories are currently empty from Sanity studio

typescript
import { defineField, defineType } from "sanity"; export default defineType({ name: "category", title: "Category", type: "document", fields: [ defineField({ name: "title", title: "Title", type: "string", }), defineField({ name: "description", title: "Description", type: "text", }), defineField({ name: "slug", title: "Slug", type: "slug", options: { source: "title", }, }), ], preview: { select: { title: "title", slug: "slug.current", }, prepare({ title, slug }) { return { title, subtitle: slug ? `/${slug}/` : "Missing slug", }; }, }, });

Updating the article query was a bit tricky, I had to start using Sanity expanded references. Took me a minute to get it right. Also updated getAllArticles() to accept slug as optional parameters. This function starts looking a bit fat, I will have to refactor it eventually

typescript
import { createClient } from "@sanity/client"; import imageUrlBuilder from "@sanity/image-url"; const mySanityClient = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, // you can find this in sanity.json dataset: process.env.NEXT_PUBLIC_SANITY_DATASET, // or the name you chose in step 1 useCdn: false, // `false` if you want to ensure fresh data apiVersion: "2024-01-01", // use a UTC date string }); const getArticleBySlug = async (slug: string) => { const item = await mySanityClient.fetch( `*[_type == "post" && slug.current == $slug][0]{ title, _id, publishedAt, description, body, mainImage, seo, "slug": slug.current, categories[]->{ title, description, _id, "slug": slug.current } }`, { slug } ); if (!item) { throw new Error("Item not found"); } return item; }; const getArticleById = async (id: string) => { const item = await mySanityClient.fetch( `*[_type == "post" && _id == $id][0]{ title, _id, publishedAt, description, body, mainImage, seo, "slug": slug.current, categories[]->{ title, description, _id, "slug": slug.current } }`, { id } ); return item; }; const getAllArticles = async ({ page = -1, pageSize = 0, category = "", }: { page?: number; pageSize?: number; category?: string } = {}) => { let skip: number = 0; let filter: string = ""; let categoryFilter: string = ""; if (page > 0 && pageSize > 0) { skip = (page - 1) * pageSize; // Calculate the number of items to skip filter = `[${skip}...${skip + pageSize}]`; // Add the filter to the query } if (category) { categoryFilter = ` && "${category}" in categories[]->slug.current`; } const items = await mySanityClient.fetch( `*[_type == "post" ${categoryFilter}]{ title, _id, _createdAt, publishedAt, description, body, mainImage, "slug": slug.current } | order(publishedAt desc)${filter}` ); return items; }; const getAllCategories = async () => { const categories = await mySanityClient.fetch( `*[_type == "category"]{ title, _id, "slug": slug.current, description }` ); return categories; }; const builder = imageUrlBuilder(mySanityClient); const urlFor = (source: any) => { return builder.image(source); }; export { mySanityClient, getAllCategories, getArticleBySlug, getArticleById, getAllArticles, urlFor, };

Last step was to update the sitemap and submit it on Google Console, but that's quite easy to do especially sinceI already have one for the regular articles.

Keep Reading