Blog - Next.js - Chapter #9 - Tags
In this chapter I add the tags 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 tag as well as the page with list of all tags.
9.1 Component Tags
I create new Component to show tags for each post, blog/components/Tags.tsx
:
// blog/components/Tags.tsx
import Link from 'next/link';
export default function Tags({ tags }: any) {
if (!tags) {
return <></>;
}
return (
<ul className="post-tags">
{tags?.map((postTag: any) => (
<li key={`${postTag}`}>
<Link href={`/tags/${postTag}`}>
<a>{postTag}</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';
import Tags from '../components/Tags';
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} />
<Tags tags={post.tags} />
</li>
)
);
})}
</ul>
);
}
To show tags 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 Tags from '../../components/Tags';
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} />
<Tags tags={postData.tags} />
</div>
<div className="excerpt">
{newLinesIntoParagraphs(postData.excerpt)}
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</article>
</Layout>
);
}
/* Keep the existing code here */
9.2 Tags page with posts list
I want one page for each tag showing list of all posts with this one tag.
At this point I find small thing to refactor and that is; in blog/lib/posts.tsx
function getPosts
now has optionally one param to specify one single category and now I want to specify, optionally, one single category or one single tag, so I will refactor this function as follows:
export function getPosts(params?: any) {
if (!params?.category && !params?.tag) {
return posts;
}
const getPosts: any = [];
posts.map((post: any) => {
if (
(params?.category && post.categories.includes(params?.category)) ||
(params?.tag && post.tags.includes(params?.tag))
) {
getPosts.push(post);
}
});
return getPosts;
}
Still in the same blog/lib/posts.tsx
file:
// count number of posts for each category
const categories = allCategories.map((category: any) => {
return {
id: category,
// posts: getPosts(category).length,
posts: getPosts({ category: category }).length,
};
});
And in blog/pages/categories/[id].tsx
:
export async function getStaticProps({ params }: any) {
const postsByCategoryData = {
id: params.id,
// allPostsData: getPosts(params.id),
allPostsData: getPosts({ category: params.id }),
};
return {
props: {
postsByCategoryData,
},
};
}
Now I can add following code to blog/lib/posts.tsx
to get all tags:
/*********************
Tags
*/
const allTags: any = [];
// get all tags like this: [ 'nextjs', 'test' ]
posts.map((post: any) => {
post.tags.map((postTag: any) => {
if (!allTags.includes(postTag)) {
allTags.push(postTag);
}
});
});
export function getAllTagIds() {
// 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/tags/[id].tsx will fail.
*/
return allTags.map((tag: any) => {
return {
params: {
id: tag,
},
};
});
}
Now I can create new blog/pages/tags/[id].tsx
:
// blog/pages/tags/[id].tsx
import Layout from '../../components/Layout';
import MetaData from '../../components/MetaData';
import Posts from '../../components/Posts';
import { getAllTagIds, getPosts } from '../../lib/posts';
export default function Tag({ postsByTagData }: any) {
return (
<Layout>
<MetaData
title={`Tag: ${postsByTagData.id}`}
description={`Posts by tag ${postsByTagData.id}`}
/>
<h2 className="h1">Tag: {postsByTagData.id}</h2>
<div className="entry-meta posted-on">
{postsByTagData.allPostsData.length == 1
? postsByTagData.allPostsData.length + ' post'
: postsByTagData.allPostsData.length + ' posts'}
</div>
<section className="all-post-data">
<Posts posts={postsByTagData.allPostsData} />
</section>
</Layout>
);
}
export async function getStaticPaths() {
const paths = getAllTagIds();
return {
paths,
fallback: false,
};
}
export async function getStaticProps({ params }: any) {
const postsByTagData = {
id: params.id,
allPostsData: getPosts({ tag: params.id }),
};
return {
props: {
postsByTagData,
},
};
}
9.3 Tags page with all tags list
I also want one page to list all tags, this will be localhost:3000/tags which now is one page not found.
First I will add some functions to blog/lib/posts.tsx
to count number of posts for each tag, sort by number of posts for each tag and get all tags list:
/* Keep the existing code here */
/*********************
Tags
*/
const allTags: any = [];
// get all tags like this: [ 'nextjs', 'test' ]
posts.map((post: any) => {
post.tags.map((postTag: any) => {
if (!allTags.includes(postTag)) {
allTags.push(postTag);
}
});
});
// count number of posts for each category
const tags = allTags.map((tag: any) => {
return {
id: tag,
posts: getPosts({ tag: tag }).length,
};
});
// sort by number of posts for each category
tags.sort(({ posts: a }: any, { posts: b }: any) => {
if (a < b) {
return 1;
} else if (a > b) {
return -1;
} else {
return 0;
}
});
export function getTags() {
return tags;
}
export function getAllTagIds() {
/* Keep the existing code here */
And blog/pages/tags/index.tsx
:
// blog/pages/tags/index.tsx
import Layout from '../../components/Layout';
import MetaData from '../../components/MetaData';
import { getTags } from '../../lib/posts';
import Link from 'next/link';
export default function catHome({ allTagIds }: any) {
return (
<Layout>
<MetaData title="List of tags" description="List of tags" />
<h2 className="h1">List of tags</h2>
<div className="entry-meta posted-on">
{allTagIds.length == 1
? allTagIds.length + ' tag'
: allTagIds.length + ' tags'}
</div>
<section className="all-post-data">
<ul>
{allTagIds?.map((postTag: any) => (
<li key={`${postTag.id}`}>
<h2 className="h4">
<Link href={`/tags/${postTag.id}`}>
<a>{postTag.id}</a>
</Link>
</h2>
<div className="posted-on">
{postTag.posts == 1
? postTag.posts + ' post'
: postTag.posts + ' posts'}
</div>
</li>
))}
</ul>
</section>
</Layout>
);
}
export async function getStaticProps() {
const allTagIds = getTags();
return {
props: {
allTagIds,
},
};
}