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
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
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 since I already have one for the regular articles.