Adding Categories with Sanity.io and Next.js 14

Adding Categories with Sanity.io and Next.js 14

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.

Random Number: 0.5990853164045125