How to create a list of blog categories using custom types in nextjs

i tried creating a list of categories for my blog using custom types and then creating a relation between my blog_post page type, i've been trying to fetch posts under a category im not sure of how im supposed to achieve that, ideally when each of this category links are clicked it should route it should route to a page like: blog/category/slug and also fetch a list of all the blog posts attached to the custom tye categories

Btw i just started off with using prismic yesterday, pls any help? Thanks

//blog_post page type

"blog_category": {
        "type": "Link",
        "config": {
          "label": "Blog Category",
          "select": "document",
          "customtypes": ["categories"]
        }
      }

//categories custom types

{
  "format": "custom",
  "id": "categories",
  "label": "Categories",
  "repeatable": true,
  "status": true,
  "json": {
    "Main": {
      "uid": {
        "config": {
          "label": "Slug",
          "placeholder": ""
        },
        "type": "UID"
      },
      "name": {
        "type": "Text",
        "config": {
          "label": "Name",
          "placeholder": ""
        }
      },
      "description": {
        "type": "Text",
        "config": {
          "label": "Description",
          "placeholder": ""
        }
      },
      "color": {
        "type": "Color",
        "config": {
          "label": "Color",
          "placeholder": ""
        }
      }
    }
  }
}

//supposed dynamic category page

export async function getStaticProps({ previewData }) {
  const client = createClient({ previewData });

  const categories = await client.getAllByType('categories', {
   fetchLinks: ['blog_post.body'],
  });

  return {
    props: { categories },
  };
}

Hi @kehindephilip15 and welcome to the Prismic community. I think you'll be happy with your experience.

I have recently done this with one of my projects (I called them Tags rather than categories, but it's the same principle). I learned from the FAQ that Prismic has on this.

Hi, @nf_mastroianni
Thanks for your response, pls do you mind sharing a code sample of how you made the query to fetch the documents linked to your tags

Not a problem @kehindephilip15 , I'd be happy to.

So here's my structure, please adjust to your "categories" vs my "tags."

/*
* src/app/blog/tag/[uid]/page.tsx
*/
import { createClient } from '@/prismicio'
import * as prismic from '@prismicio/client'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'

import BlogCard from '@/components/BlogCard'
import Heading from '@/components/Heading'
import Pagination from '@/components/Pagination'
import Section from '@/components/Section'
import { HiTag } from 'react-icons/hi'

type Params = { uid: string }
/**
 * SearchParams are used for pagination ex: ?page=2
 */
type SearchParams = {
  [key: string]: string | string[] | undefined
}

export default async function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const client = createClient()
  // set pageNumber to the searchParam page number or 1
  const pageNumber = Number(searchParams['page']) || 1
  const page = await client.getByUID('tag', params.uid).catch(() => notFound())
  /**
   * get posts and then filter them based on the page.id
   */
  const posts = await client.getByType('post', {
    orderings: {
      field: 'document.first_publication_date',
      direction: 'desc',
    },
    graphQuery: `
    {
      post {
        custom_tags {
          custom_tag
        }
        date_published
        excerpt
        meta_image
        title
        slices
      }
    }
    `,
    filters: [prismic.filter.at('my.post.custom_tags.custom_tag', page.id)],
    page: pageNumber,
    pageSize: 5,
  })

  return (
    <>
      <Section width="md">
        <Heading
          as="h1"
          size="4xl"
          className="text-color-primary lg:text-center"
        >
          <HiTag className="inline h-8 w-8" /> Posts tagged as{' '}
          {prismic.asText(page.data.title)}
        </Heading>
        {prismic.isFilled.keyText(page.data.meta_description) ? (
          <p>{page.data.meta_description}</p>
        ) : null}
        {posts && posts.results.length > 0 ? (
          <ul className="px-4 lg:px-0">
            {posts.results.map(post => {
              return (
                <BlogCard
                  key={post.id}
                  post={post}
                  className="mx-auto max-w-xl"
                />
              )
            })}
          </ul>
        ) : (
          <div className="my-8 text-center">
            No posts have been tagged with this tag yet. Please try back another
            day.
          </div>
        )}
        {(posts?.next_page !== null || posts?.prev_page !== null) && (
          <Pagination
            hasNextPage={posts?.next_page !== null}
            hasPrevPage={posts?.prev_page !== null}
            totalPages={Number(posts?.total_pages)}
          />
        )}
      </Section>
    </>
  )
}

export async function generateMetadata({
  params,
}: {
  params: Params
}): Promise<Metadata> {
  const client = createClient()
  const page = await client.getByUID('tag', params.uid).catch(() => notFound())
  const settings = await client.getSingle('settings')

  return {
    title: `${prismic.asText(page.data.title)} • ${settings.data.site_title}`,
    description:
      page.data.meta_description || settings.data.site_meta_description,
    openGraph: {
      images: [settings.data.site_meta_image.url || ''],
    },
  }
}

export async function generateStaticParams() {
  const client = createClient()
  const pages = await client.getAllByType('tag')

  return pages.map(page => {
    return { uid: page.uid }
  })
}

I hope that helps.

Thanks a bunch, i'll try this out with the page router

1 Like

Ah,
I see. I wasn't aware you were using the Pages router. If you're just starting out, maybe you'd consider using the App router? But, now that I look back and see getStaticProps, it makes sense. I missed that.

Oh i've actually gone a long way, the codebase already exists in page router (just adding a blog feature) i could try and convert your code to be suitable for a page router, thanks

1 Like

Hi @nf_mastroianni

export async function getStaticProps({ params, previewData }) {
  const client = createClient({ previewData });
  console.log(params, 'params');

  const posts = await client.getAllByType('blog_post', {
    orderings: [
      { field: 'my.blog_post.publication_date', direction: 'desc' },
      { field: 'document.first_publication_date', direction: 'desc' },
    ],
    fetchLinks: ['author.fullname', 'blog_categories.name'],
  });

  const filterPostsBasedOnCategory = (category) => {
    return posts.filter((post) => {
      return post.data.blog_category.data.name === category;
    });
  };

  const categoryPost = filterPostsBasedOnCategory(params.uid);

  return {
    props: { categories, categoryPosts: categoryPost },
  };
}

export async function getStaticPaths() {
  const client = createClient();

  const pages = await client.getAllByType('blog_categories');

  return {
    paths: pages.map((page) => {
      return asLink(page);
    }),
    fallback: false,
  };
}

TypeError: Cannot convert undefined or null to object
is there something im missing here

I think I could better assist if you provided a little more information regarding the specific TypeError location. Which part (const/etc) is TS barking at?

Still trying to debug where it's coming from exactly, i'll feedback

i think it comes from here @nf_mastroianni

export async function getStaticPaths() {
  const client = createClient();

  const pages = await client.getAllByType('blog_categories');

  return {
    paths: pages.map((page) => {
      return asLink(page);
    }),
    fallback: false,
  };
}```

If I had to guess, my money is on this being where TypeScript is being grumpy...

// const categoryPost is potentially undefined or an empty array
const categoryPost: BlogPostDocument[] | null = filterPostsBasedOnCategory(params.uid);

  if(categoryPost.length > 0) {
    return {
      props: { categories, categoryPosts: categoryPost }
    }
  }
  return {
    props: { categories }
  }
  const categoryPosts =
    params && params.uid ? filterPostsBasedOnCategory(params.uid) : [];

im making a check here, :smiling_face_with_tear: it's a js codebase (sadly)

can you attach a screenshot of the TS error?
Oh, ok. Well, JS will make sure you have less typescript headaches.

This bit here TypeError: Cannot convert undefined or null to object leads me to believe you should be console.log(ging) to find what item is undefined or null

I don't think the problem is in your getStaticPaths. My gut says it's in your getStaticProps. Let's simplify it and log to find the undefined or null item

console.log('posts -----> ', posts)
console.log('categoryPost -----> ', categoryPost)
if(categoryPost.length > 0) {
    return {
      props: { categories, categoryPosts: categoryPost }
    }
  }
  return {
    props: { categories }
  }

It's kind of not descriptive enough

const routes = [
  {
    type: 'blog_post',
    path: '/blog/post/:uid',
  },
//
   {
  type: 'blog_categories',
   path: '/blog/category/:uid',
 },
];

does it have anything to with the route, do i have to define this @nf_mastroianni thanks

The routes are important, but I'm 99.9% confident the type error is not related to your route resolver. Can you take a look at my suggestion on logging and conditional return from getStaticProps? I'd love to know if you can find that undefined or null item that way.

okay i'd try this out

Also, consider using Prismic's query filter so you don't have to use your own custom filter function:

i'd check this out

Oops, here @nf_mastroianni

undefined 'posts'
undefined categoryPosts

can't exactly explain why it's undefined

i cant send replies again, until 21hrs,

I tried to follow the first recommendation on my nextjs 14 app router typescript setup. I be able to display the category name but had a hard time displaying the posts with filtered by a specific category. In my footer I loop the categories like so

Footer.tsx

const categories = await client.getAllByType('category')

{categories.map((category, index) => (
  <li key={index}>
    <Link
      href={`/categories/${category.uid}`}
      className="hover:text-gray-400"
    >
      {category.data.name}
    </Link>
  </li>
))}

and here's my /categories/[category]/page.tsx

import { createClient } from '@/prismicio'
import { notFound } from 'next/navigation'
import * as prismic from '@prismicio/client'

type Params = { category: string }

export default async function Page({ params }: { params: Params }) {
  const client = createClient()

  const page = await client
    .getByUID('category', params.category)
    .catch(() => notFound())

  const posts = await client.getAllByType('blog_post', {
    orderings: {
      field: 'document.first_publication_date',
      direction: 'desc',
    },
    filters: [prismic.filter.at('my.blog_post.category', page.id)],
    pageSize: 10,
  })

  return (
    <div>
      <h1>Category: {page.uid}</h1>
      {posts && (
        <ul>
          {posts.map((post) => (
            <li key={post.uid}>{post.data.title}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

export async function generateStaticParams() {
  const client = createClient()

  const pages = await client.getAllByType('category')

  return pages.map((page) => {
    return { uid: page.uid }
  })
}

I searched thru the docs but can't seem to find what I need.