Migrating my blog to Astro - Tags
How to display and organise blog post tags on my new Astro-based blog
This is part of a series of posts on how I migrated my blog to Astro
Tags were something that never really worked properly with Jekyll. This was something I was hoping to implement much better this time around.
My goals:
- Use tags as defined in each post’s frontmatter.
- Highlight tags for each post on the home page and on each blog post page.
- Create a single
/tags
index page that lists all the available tags. - Create a separate file underneath this path for each tag. eg.
/tags/Azure
,/tags/.NET
etc. that include a list of all posts referencing that tag.
1 and 2 are pretty straightforward. As you can see in the MarkdownPostLayout.astro layout component, we can get the tags of the current page from frontmatter, and then loop over them using map
to render them out:
---
import DateTimeComponent from "../components/DateTimeComponent.astro";
import BaseLayout from "./BaseLayout.astro";
import { Disqus } from "astro-disqus";
interface Props {
frontmatter: {
title: string;
date: string;
description?: undefined | string;
tags: string[];
};
}
const { frontmatter } = Astro.props;
---
<BaseLayout pageTitle={frontmatter.title}>
<slot name="head" slot="head" />
{frontmatter.description && (
<meta name="description" content={frontmatter.description} slot="head" />
)}
<h1>{frontmatter.title}</h1>
<p><DateTimeComponent date={frontmatter.date} /></p>
<p><em>{frontmatter.description}</em></p>
<div class="tags">
{
frontmatter.tags.map((tag: string) => (
<p class="tag">
<a href={`/tags/${tag}`}>{tag}</a>
</p>
))
}
</div>
<slot />
<Disqus
embed="https://davidgardiner.disqus.com/embed.js"
url={Astro.url.toString()}
/>
</BaseLayout>
The /tags page
This is defined by src/pages/tags/index.astro
.
The logic here is to make use of the existing blog content collection, and collate all the tags (and calculate totals for each tag).
---
import { getCollection } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
import onlyCurrent from "../../scripts/filters";
const pageTitle = "Tags";
const allPosts = (await getCollection("blog")).filter(onlyCurrent);
// Collate all tags from all posts and count posts per tag
const tagCounts = allPosts
.flatMap((post: any) => post.data.tags)
.reduce((acc: Record<string, number>, tag: string) => {
acc[tag] = (acc[tag] || 0) + 1;
return acc;
}, {});
const tags = Object.entries(tagCounts).sort(([tagA], [tagB]) =>
tagA.localeCompare(tagB)
);
---
<BaseLayout pageTitle={pageTitle}>
<div class="tags">
{
tags.map(([tag, count]) => (
<p class="tag">
<a href={`/tags/${tag}`}>{tag}</a> ({count})
</p>
))
}
</div>
</BaseLayout>
...
You can see the end result at /tags.
Individual tag summary pages
But how do we create individual pages for each tag? It feels similar to what we did with blog posts, but in that case there was a one-to-one relationship between the .md
file and the generated .html
file.
The solution is again to to use Astro’s dynamic routes. By creating a file named src/pages/tags/[tag].astro
, we can then use the [tag]
placeholder to inject in the tag name.
As we do for posts, we provide an implementation of getStaticPaths
to return the list of tags and for each tag the list of posts that have that tag.
---
import BlogPost from "../../components/BlogPost.astro";
import BaseLayout from "../../layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import DateTimeComponent from "../../components/DateTimeComponent.astro";
import type {
InferGetStaticParamsType,
InferGetStaticPropsType,
GetStaticPaths,
} from "astro";
import onlyCurrent from "../../scripts/filters";
export const getStaticPaths = (async () => {
const allPosts = (await getCollection("blog")).filter(onlyCurrent);
const uniqueTags = [
...new Set(allPosts.map((post: any) => post.data.tags).flat()),
];
return uniqueTags.map((tag) => {
const filteredPosts = allPosts
.filter((post: any) => post.data.tags.includes(tag))
.sort(
(a: any, b: any) =>
new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
return {
params: { tag },
props: { posts: filteredPosts },
};
});
}) satisfies GetStaticPaths;
type Params = InferGetStaticParamsType<typeof getStaticPaths>;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { tag } = Astro.params as Params;
const { posts } = Astro.props as Props;
---
<BaseLayout pageTitle={tag}>
<p>Posts tagged with {tag}</p>
<ul>
{
posts.map((post: any) => (
<li>
<BlogPost url={`/${post.id}`} title={post.data.title} /> (
<DateTimeComponent date={post.data.date} />)
</li>
))
}
</ul>
</BaseLayout>
Notice that we get the current tag value from the params
property.
And so this is how we get pages like /tags/.NET