Repository: https://github.com/qbreis/blog/tree/dev-chapter-8-categories

Blog - Next.js - Chapter #8 - Categories

In this chapter I add the categories for each single post in list of posts as well as in every single post page after the title. I also add page to list all post for one single category as well as the page with list of all categories.

8.1 Component Categories

I create new Component to show categories for each post, blog/components/Categories.tsx:

// blog/components/Categories.tsx

import Link from 'next/link';

export default function Categories({ categories }: any) {
  if (!categories) {
    return <></>;
  }
  return (
    <ul className="post-categories">
      {categories?.map((postCategory: any) => (
        <li key={`${postCategory}`}>
          <Link href={`/categories/${postCategory}`}>
            <a>{postCategory}</a>
          </Link>
        </li>
      ))}
    </ul>
  );
}

And then I update blog/components/Posts.tsx:

// blog/components/Posts.tsx

import Link from 'next/link';
import Date from '../components/Date';
import Categories from '../components/Categories';

export default function Posts({ posts }: any) {
  return (
    <ul>
      {posts.map((post: any) => {
        return (
          post.id && (
            <li className="sinle-post-item" key={post.id}>
              <h2 className="h4">
                <Link href={`/posts/${post.id}`}>
                  <a>{post.title}</a>
                </Link>
              </h2>
              <Date dateString={post.date} />
              <Categories categories={post.categories} />
            </li>
          )
        );
      })}
    </ul>
  );
}

To show categories in single post page, I update blog/pages/posts/[id].tsx:

// blog/pages/posts/[id].tsx

import Link from 'next/link';
import Layout from '../../components/Layout';
import MetaData from '../../components/MetaData';
import Date from '../../components/Date';
import Categories from '../../components/Categories';
import { getAllPostIds, getPostData } from '../../lib/posts';
import { newLinesIntoParagraphs } from '../../lib/functions';

export default function Post({ postData }: any) {
  return (
    <Layout>
      <article>
        <MetaData title={postData.title} description={postData.excerpt} />
        {postData.repository && (
          <>
            <span style={{ fontSize: '0.7em' }}>Repository: </span>
            <Link href={postData.repository}>
              <a
                target="_blank"
                rel="noopener noreferrer"
                style={{ fontSize: '0.7em', textDecoration: 'none' }}
              >
                {postData.repository}
              </a>
            </Link>
          </>
        )}
        <h1>{postData.title}</h1>
        <div className="entry-meta">
          <Date dateString={postData.date} />
          <Categories categories={postData.categories} />
        </div>
        <div className="excerpt">
          {newLinesIntoParagraphs(postData.excerpt)}
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </Layout>
  );
}

/* Keep the existing code here */

8.2 Category page with posts list

I want one page for each category showing list of all posts with this one category.

I start adding following code to blog/lib/posts.tsx to get all categories:

/* Keep the existing code here */

/*********************
Categories
*/

const allCategories: any = [];

// get all categories like this: [ 'nextjs', 'test' ]
posts.map((post: any) => {
  post.categories.map((postCategory: any) => {
    if (!allCategories.includes(postCategory)) {
      allCategories.push(postCategory);
    }
  });
});

export function getAllCategoryIds() {
  // Returns an array of possible value for id that looks like this:
  // [
  //   {
  //     params: {
  //       id: 'nextjs'
  //     }
  //   },
  //   {
  //     params: {
  //       id: 'test'
  //     }
  //   }
  // ]
  /*
  Important: The returned list is not just an array of strings —
  it must be an array of objects that look like the comment above.
  Each object must have the params key and contain an object with
  the id key (because we’re using [id] in the file name).
  Otherwise, getStaticPaths in pages/categories/[id].tsx will fail.
  */
  return allCategories.map((category: any) => {
    return {
      params: {
        id: category,
      },
    };
  });
}

Now I create new file and folder pages\categories\[id].tsx:

// blog/pages/categories/[id].tsx

import Layout from '../../components/Layout';
import MetaData from '../../components/MetaData';

import { getAllCategoryIds } from '../../lib/posts';

export default function Category({ postsByCategoryData }: any) {
  return (
    <Layout>
      <MetaData
        title={`Category: ${postsByCategoryData.id}`}
        description={`Posts by category ${postsByCategoryData.id}`}
      />
      <h2 className="h1">Category: {postsByCategoryData.id}</h2>
    </Layout>
  );
}

export async function getStaticPaths() {
  const paths = getAllCategoryIds();
  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }: any) {
  const postsByCategoryData = {
    id: params.id,
  };
  return {
    props: {
      postsByCategoryData,
    },
  };
}

In blog/lib/posts.tsx, function getPosts now retrieves all posts, I want, optionally, to return all posts for one category. So I update blog/lib/posts.tsx:

export function getPosts(categoryId?: any) {
  if (!categoryId) {
    return posts;
  }
  const getPosts: any = [];
  posts.map((post: any) => {
    if (post.categories.includes(categoryId)) {
      getPosts.push(post);
    }
  });
  return getPosts;
}

Now in blog/pages/categories/[id].tsx:

// blog/pages/categories/[id].tsx

import Layout from '../../components/Layout';
import MetaData from '../../components/MetaData';
import Posts from '../../components/Posts'; /* 1 */

import { getAllCategoryIds /* 2 */, getPosts /* 3 */ } from '../../lib/posts';

export default function Category({ postsByCategoryData }: any) {
  return (
    <Layout>
      <MetaData
        title={`Category: ${postsByCategoryData.id}`}
        description={`Posts by category ${postsByCategoryData.id}`}
      />
      <h2 className="h1">Category: {postsByCategoryData.id}</h2>

      <div className="entry-meta posted-on">
        {postsByCategoryData.allPostsData /* 4 */.length == 1
          ? postsByCategoryData.allPostsData /* 4 */.length + ' post'
          : postsByCategoryData.allPostsData /* 4 */.length + ' posts'}
      </div>
      <section className="all-post-data">
        <Posts /* 1 */ posts={postsByCategoryData.allPostsData /* 4 */} />
      </section>
    </Layout>
  );
}

export async function getStaticPaths() {
  const paths = getAllCategoryIds(); /* 2 */
  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }: any) {
  const postsByCategoryData = {
    id: params.id,
    allPostsData /* 4 */: getPosts(params.id) /* 3 */,
  };
  return {
    props: {
      postsByCategoryData,
    },
  };
}

8.3 Category page with all categories list

I also want one page to list all categories, this will be localhost:3000/categories which now is one page not found.

First I will add some functions to blog/lib/posts.tsx:

/* Keep the existing code here */

/*********************
Categories
*/

const allCategories: any = [];

// get all categories like this: [ 'nextjs', 'test' ]
posts.map((post: any) => {
  post.categories.map((postCategory: any) => {
    if (!allCategories.includes(postCategory)) {
      allCategories.push(postCategory);
    }
  });
});

// count number of posts for each category
const categories = allCategories.map((category: any) => {
  return {
    id: category,
    posts: getPosts(category).length,
  };
});

// sort by number of posts for each category
categories.sort(({ posts: a }: any, { posts: b }: any) => {
  if (a < b) {
    return 1;
  } else if (a > b) {
    return -1;
  } else {
    return 0;
  }
});

export function getCategories() {
  return categories;
}

export function getAllCategoryIds() {

/* Keep the existing code here */

And blog/pages/categories/index.tsx:

// blog/pages/categories/index.tsx

import Layout from '../../components/Layout';
import MetaData from '../../components/MetaData';
import { getCategories } from '../../lib/posts';
import Link from 'next/link';

export default function catHome({ allCategoryIds }: any) {
  return (
    <Layout>
      <MetaData title="List of categories" description="List of categories" />
      <h2 className="h1">List of categories</h2>

      <div className="entry-meta posted-on">
        {allCategoryIds.length == 1
          ? allCategoryIds.length + ' category'
          : allCategoryIds.length + ' categories'}
      </div>

      <section className="all-post-data">
        <ul>
          {allCategoryIds?.map((postCategory: any) => (
            <li key={`${postCategory.id}`}>
              <h2 className="h4">
                <Link href={`/categories/${postCategory.id}`}>
                  <a>{postCategory.id}</a>
                </Link>
              </h2>
              <div className="posted-on">
                {postCategory.posts == 1
                  ? postCategory.posts + ' post'
                  : postCategory.posts + ' posts'}
              </div>
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  );
}

export async function getStaticProps() {
  const allCategoryIds = getCategories();
  return {
    props: {
      allCategoryIds,
    },
  };
}

8.4 Index page with all posts list

Similarly, now I realize, route localhost:3000/posts is also a 404 page not found. I can solve that by creating a new index file in blog/pages/posts/index.tsx as follows:

// blog/pages/posts/index.tsx

import Home from '../index';
import { getPosts } from '../../lib/posts';

export async function getStaticProps() {
  const posts = getPosts();
  return {
    props: {
      posts,
    },
  };
}

export default Home;
Back to home