Why show Related Posts?
A good article often leads to another useful article.
Related posts give readers a simple next step after they finish reading. They also make it easier to discover older content without changing the main navigation.
For a small Astro blog, tags are a practical starting point. If two posts share a tag, they probably cover a similar topic.
What We Will Build
This tutorial adds a RelatedPosts component that:
- reads the existing posts collection
- excludes the current post
- compares post tags
- shows up to three matching posts
- renders nothing when no matching post exists
The component can be placed directly below the article content.
Step 1 - Add Tags to Your Posts
Each post needs tags in its frontmatter. They describe the topics used for matching.
---
title: 'Astro - Add a Time to Read Label to Your Posts'
tags: ['astro']
---Make sure that your content collection accepts an optional string array:
tags: z.array(z.string()).optional(),Posts without tags are still valid. They simply will not receive related-post suggestions.
Step 2 - Reuse Your Post Loader
This blog keeps the collection access in src/types/collections.ts.
export async function getPosts(): Promise<Post[]> {
const posts = await getCollection('posts', ({ data }) => import.meta.env.DEV || !data.draft);
return posts.sort((a, b) => {
return new Date(b.data.date).getTime() - new Date(a.data.date).getTime();
});
}This is useful because the related-post component automatically uses the same published-post rule and date order as the rest of the site.
Step 3 - Create the RelatedPosts Component
Create src/components/RelatedPosts/RelatedPosts.astro.
The component receives the current post, counts the shared tags of every other post, and limits the result to three entries.
---
import { getPosts, type Post } from '../../types/collections';
import LatestPosts from '../LatestPosts/LatestPosts.astro';
interface Props {
post: Post;
}
const { post } = Astro.props;
const postTags = new Set(post.data.tags ?? []);
const relatedPosts = (await getPosts())
.map(candidate => ({
post: candidate,
sharedTags: candidate.data.tags?.filter(tag => postTags.has(tag)).length ?? 0,
}))
.filter(({ post: candidate, sharedTags }) => candidate.id !== post.id && sharedTags > 0)
.sort((a, b) => b.sharedTags - a.sharedTags)
.slice(0, 3)
.map(({ post: candidate }) => candidate);
---
{relatedPosts.length > 0 && (
<section class="mt-5" aria-labelledby="related-posts-title">
<h2 id="related-posts-title" class="h4 mb-3">
Related posts
</h2>
<LatestPosts posts={relatedPosts} showGrid />
</section>
)}Set makes the tag lookup simple. The sharedTags value also means a post sharing two tags is shown before a post sharing only one tag. Since getPosts() already sorts posts by date, equally relevant posts keep that order.
Step 4 - Render It Under the Content
Import the component in your post layout and place it after the article content card.
---
import RelatedPosts from '../components/RelatedPosts/RelatedPosts.astro';
---
<div class="post-content">
<Card class="shadow-sm">
<slot />
</Card>
<RelatedPosts post={post} />
</div>Using the existing LatestPosts component keeps the cards consistent with the home, category, and tag pages. It also avoids duplicating markup for a second type of post list.
Step 5 - Check Empty and Matching States
Build the site after adding the component:
npm run buildCheck a post with a shared tag and one without tags. A matching post should show the section below its content. A post without matches should not render an empty heading or wrapper.
Optional Improvements
You can extend the matching rule later with:
- category as a fallback when no tags match
- a manual
relatedfrontmatter field for editorial choices - a score that gives selected tags more weight
- an image or excerpt variant for the related-post cards
Start with tags first. They are transparent for authors, easy to maintain, and work well for a static Astro site.
Related posts
Astro - Add a Time to Read Label to Your Posts
Learn how to calculate reading time in Astro and display a clean Time to Read label on your blog posts