Skip to content

Commit 32651bc

Browse files
committed
feat: add RelatedPosts component to display related articles based on tags and category
1 parent 5bba2ef commit 32651bc

3 files changed

Lines changed: 151 additions & 1 deletion

File tree

src/components/Banner.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
import { Image } from 'astro:assets';
3-
import { BANNER_CONFIG, BANNER_HEIGHT_EXTEND } from '../config/banner';
3+
import { BANNER_CONFIG } from '../config/banner';
44
55
interface Props {
66
src?: string;

src/components/RelatedPosts.astro

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
import FormattedDate from './FormattedDate.astro';
3+
import { Image } from "astro:assets";
4+
import type { CollectionEntry } from 'astro:content';
5+
6+
export interface Props {
7+
relatedPosts: CollectionEntry<'blog'>[];
8+
}
9+
10+
const { relatedPosts } = Astro.props;
11+
12+
// 只顯示前兩篇相關文章
13+
const displayPosts = relatedPosts.slice(0, 2);
14+
---
15+
16+
{displayPosts.length > 0 && (
17+
<div class="mt-6 md:mt-8">
18+
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-6">
19+
{displayPosts.map((post) => {
20+
const { title, pubDate, description, category, tags } = post.data;
21+
22+
return (
23+
<div class="group bg-white dark:bg-zinc-900 rounded-xl border border-gray-100/50 dark:border-zinc-700/50 overflow-hidden transition duration-300 ease-out relative">
24+
<div class="absolute top-5 left-5 w-1 h-6 rounded-md bg-blue-600"></div>
25+
<div class="p-4 pl-8 md:pl-9 relative">
26+
<a href={`/post/${post.slug}/`}
27+
class="group/title block font-bold text-lg md:text-xl text-gray-900 dark:text-zinc-100
28+
hover:text-blue-600 dark:hover:text-blue-400
29+
transition-colors duration-200 mb-3 line-clamp-2 leading-tight relative">
30+
{title}
31+
<svg class="text-blue-600 dark:text-blue-400 text-lg hidden md:inline absolute translate-y-0.5 opacity-0 group-hover/title:opacity-100 -translate-x-1 group-hover/title:translate-x-0 transition-all duration-200 ease-out" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
32+
<path d="M8.59 16.58L13.17 12L8.59 7.41L10 6L16 12L10 18L8.59 16.58Z"/>
33+
</svg>
34+
</a>
35+
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-xs text-gray-500 dark:text-gray-400 mb-3">
36+
<div class="flex items-center">
37+
<div class="flex items-center justify-center w-4 h-4 md:w-5 md:h-5 bg-gray-100 dark:bg-gray-500/20 rounded-md mr-1">
38+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-600 dark:text-gray-400 md:w-[12px] md:h-[12px]">
39+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
40+
<line x1="16" y1="2" x2="16" y2="6"></line>
41+
<line x1="8" y1="2" x2="8" y2="6"></line>
42+
<line x1="3" y1="10" x2="21" y2="10"></line>
43+
</svg>
44+
</div>
45+
<span class="text-xs font-medium"><FormattedDate date={pubDate} /></span>
46+
</div>
47+
<div class="flex items-center">
48+
<div class="flex items-center justify-center w-4 h-4 md:w-5 md:h-5 bg-sky-100 dark:bg-sky-900/20 rounded-md mr-1">
49+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-sky-600 dark:text-sky-400 md:w-[12px] md:h-[12px]">
50+
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"></path>
51+
</svg>
52+
</div>
53+
<a href={`/archive/category/${category.replace(/\s+/g, '-')}/`}
54+
class="text-xs font-medium hover:text-sky-600 dark:hover:text-sky-400 transition-colors">
55+
{category}
56+
</a>
57+
</div>
58+
</div>
59+
<p class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2 leading-relaxed mb-3">
60+
{description}
61+
</p>
62+
{tags && tags.length > 0 && (
63+
<div class="flex items-center">
64+
<div class="flex items-center justify-center w-4 h-4 md:w-5 md:h-5 bg-green-100 dark:bg-green-900/20 rounded-md mr-1">
65+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 dark:text-green-400 md:w-[12px] md:h-[12px]" style="transform: translateY(-0.5px);">
66+
<path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"></path>
67+
<line x1="7" y1="7" x2="7.01" y2="7"></line>
68+
</svg>
69+
</div>
70+
<div class="flex flex-wrap gap-1">
71+
{tags.slice(0, 2).map((tag, i) => (
72+
<>
73+
{i > 0 && <div class="mx-1 text-neutral-400 text-xs">/</div>}
74+
<a href={`/archive/tag/${tag.replace(/\s+/g, '-')}/`}
75+
class="text-xs font-medium hover:text-green-600 dark:hover:text-green-400 transition-colors whitespace-nowrap">
76+
{tag.trim()}
77+
</a>
78+
</>
79+
))}
80+
{tags.length > 2 && (
81+
<span class="ml-1 text-xs text-neutral-400">+{tags.length - 2}</span>
82+
)}
83+
</div>
84+
</div>
85+
)}
86+
</div>
87+
</div>
88+
);
89+
})}
90+
</div>
91+
</div>
92+
)}

src/layouts/Post.astro

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getCollection } from 'astro:content';
44
import FormattedDate from '../components/FormattedDate.astro';
55
import Layout from './Layout.astro';
66
import Aside from './Aside.astro';
7+
import RelatedPosts from '../components/RelatedPosts.astro';
78
89
interface Props {
910
entry: CollectionEntry<'blog'>;
@@ -21,6 +22,41 @@ blog.forEach((post) => {
2122
});
2223
});
2324
25+
// Function to find related posts based on tags
26+
function getRelatedPosts(currentPost: CollectionEntry<'blog'>, allPosts: CollectionEntry<'blog'>[], limit = 4) {
27+
const currentTags = currentPost.data.tags || [];
28+
const currentCategory = currentPost.data.category;
29+
30+
// Filter out current post and calculate similarity score
31+
const relatedPosts = allPosts
32+
.filter(post => post.slug !== currentPost.slug)
33+
.map(post => {
34+
let score = 0;
35+
const postTags = post.data.tags || [];
36+
const postCategory = post.data.category;
37+
38+
// Calculate tag similarity (each matching tag = +1 point)
39+
const matchingTags = currentTags.filter(tag => postTags.includes(tag));
40+
score += matchingTags.length * 2;
41+
42+
// Bonus for same category (+1 point)
43+
if (postCategory === currentCategory) {
44+
score += 1;
45+
}
46+
47+
return { post, score };
48+
})
49+
.filter(item => item.score > 0) // Only include posts with some similarity
50+
.sort((a, b) => b.score - a.score) // Sort by similarity score (descending)
51+
.slice(0, limit) // Take top N posts
52+
.map(item => item.post);
53+
54+
return relatedPosts;
55+
}
56+
57+
// Get related posts
58+
const relatedPosts = getRelatedPosts(entry, blog);
59+
2460
// Get headings from the ToC
2561
const { headings } = await entry.render();
2662
---
@@ -96,9 +132,31 @@ const { headings } = await entry.render();
96132
<div class="prose max-w-none prose-headings:scroll-mt-20 prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-zinc-100 prose-p:text-gray-700 prose-p:leading-relaxed prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 prose-code:text-pink-600 prose-blockquote:border-l-4 prose-blockquote:border-blue-500 prose-blockquote:bg-blue-50/50 dark:prose-blockquote:bg-blue-900/10 prose-blockquote:py-2 prose-blockquote:px-4 prose-blockquote:rounded-r-lg">
97133
<slot />
98134
</div>
135+
136+
<!-- CC BY-SA 4.0 License -->
137+
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-zinc-700">
138+
<div class="flex items-start gap-3 p-4 bg-gray-50 dark:bg-zinc-800/50 rounded-lg">
139+
<svg class="w-6 h-6 mt-0.5 text-gray-600 dark:text-gray-400 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
140+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
141+
</svg>
142+
<div class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
143+
<p class="font-medium mb-2">Licensed under CC BY-SA 4.0</p>
144+
<p class="mb-1 text-xs text-gray-500 dark:text-gray-400">
145+
Unless otherwise noted, content on this site is licensed under
146+
<a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline">CC BY-SA 4.0</a>.
147+
You are free to share and adapt with attribution.
148+
</p>
149+
</div>
150+
</div>
151+
</div>
99152
</article>
100153
</div>
101154
</main>
155+
156+
<!-- Related Posts Section - Independent Cards Outside Container -->
157+
<div class="col-span-2 lg:col-span-1 lg:col-start-2">
158+
<RelatedPosts relatedPosts={relatedPosts} />
159+
</div>
102160
</div>
103161
</div>
104162
</Layout>

0 commit comments

Comments
 (0)