Blog Post tagging system
Tags are a great way to add extra information to blog posts. Let's try to achieve the following:
- Allow for post tagging + display tags on post page
- Clicking on a tag will show a page with posts with that tag
- Aggregate all tags and display them on the index page in a tag cloud
Here's the starting point for this endeavour.
Post tagging
YAML is often prepended to markdown files to provide structured metadata. This blog uses this in order to store the post title, publish date and a snippet. This looks like a good place to also store the tags.
title: Holy Grail Layout using CSS Grid via Tailwind
published_at: 2022-12-18T17:16:15.329Z
snippet: Add a layout to the blog post page.
tags: ['css-grid', 'tailwind', 'blog'] # Look, ma! Tagsies
Let's create a new component that will output the tags below the post title:
// components/PostTags.tsx
import { Post } from "@/utils/posts.ts";
export default function PostTags({ tags }: { tags: Post["tags"] }) {
return (
<ul class="list-none mb-2">
{tags?.map((tag) => (
<li class="inline-block">
<a
href={`/tag/${tag}`}
class="inline-block bg-indigo-300 text-[#494949] mr-1 px-4 py-1 rounded-full"
>
#{tag}
</a>
</li>
))}
</ul>
);
}
We're beginning to see the power of Tailwind here. With just a few fairly readable classes, we have built some lovely tag "pills".
Let's now use the component in the Blog Post page:
// routes/[slug].tsx
{/* ... */}
<Layout>
<div class="py-8">
<h1 class="text-5xl font-bold mb-2">{post.title}</h1>
<PostTags tags={post.tags} />
{/* content */}
</div>
</Layout>;
{/* ... */}
As a small aside, I chose the colors by stumbling upon bg-indigo
in the
Tailwind CSS docs
and figuring out an accessible text color using
Accessible Colors.
Tag page
The tag page will look very similar to the Blog Index, but I do want to keep them separate.
// routes/tag/[slug].tsx
export const handler: Handlers<Post[]> = {
async GET(_req, ctx) {
const posts = await getPosts({ tag: ctx.params.slug });
if (posts.length === 0) return ctx.renderNotFound();
return ctx.render(posts);
},
};
The getPosts
function now accepts a tag
in an options parameter. Here is how
we are using that:
// utils/posts.ts
export async function getPosts({ tag }: { tag?: string }): Promise<Post[]> {
const files = Deno.readDir("./posts");
const promises: Promise<Post | null>[] = [];
for await (const file of files) {
const slug = file.name.replace(".md", "");
promises.push(getPost(slug));
}
const posts = (await Promise.all(promises)).filter(isPost);
posts
.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
return tag ? posts.filter((post) => post.tags?.includes(tag)) : posts;
}
If we pass a tag
when calling the function, the posts returned will be
filtered so only posts with that tag are returned.
And here is the relevant part of the page component:
// routes/tag/[slug].tsx
{/* ... */}
<Layout>
<div class="py-8">
<h1 class="text-5xl font-bold mb-2">#{props.params.slug}</h1>
<div>
{posts.map((post) => <PostCard post={post} />)}
</div>
</div>
</Layout>;
{/* ... */}
#exciting
Index tag cloud
We already built the PostTags
component earlier, so this is a perfect case to
employ reusability. Let's add it to the Index page below the list of posts.
We feed uniqueTags
into it, which is a list of all the tags of all the posts,
from which we remove duplicates using the old Array
+ Set
dance off.
// index.tsx
export default function BlogIndexPage(props: PageProps<Post[]>) {
const posts = props.data;
const tags = posts.reduce((tags, post) => {
return post.tags ? tags.concat(post.tags) : tags;
}, [] as Tag[]);
const uniqueTags = Array.from(new Set(tags)); // DANCE!
return (
<>
{/* ... */}
<Layout>
<div class="py-8">
<h1 class="text-5xl font-bold sr-only">Blog</h1>
<section class="mb-8">
<h2 class="mb-2 sr-only">Posts</h2>
{posts.map((post) => <PostCard post={post} />)}
</section>
<section>
<h2 class="text-xl mb-4">Tag cloud</h2>
<PostTags tags={uniqueTags} />
</section>
</div>
</Layout>
</>
);
}
Yes, this approach does not scale that well. If we have 200 tags, we show 200 tags in the tag cloud. This could be improved. On the other hand, the Blog Index has no pagination. We just show all the posts. I will tackle these when they actually become problems. #under-engineering
Bonus round
In my previous post, I left a <meta>
tag commented out. It was
related to article tags. Let's quckly fix that:
// routes/[slug.tsx]
<Head>
{/* ... */}
<meta property="article:tag" content={post.tags?.join(", ")} />
{/* ... */}
</Head>;
Wrapping up
When Twitter goes down, you can (now) come to my Blog for your hashtag fix.