import { notFound } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; import { marked } from "marked"; import { apiFetch } from "@/lib/api"; import { BlogPost, PaginatedResult, TagCloudItem } from "@/lib/types"; import { ViewTracker } from "./view-tracker"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { Eye, TrendingUp, ArrowLeft, Calendar } from "lucide-react"; import { Button } from "@/components/ui/button"; interface Props { params: Promise<{ slug: string }>; } export async function generateMetadata({ params }: Props) { const { slug } = await params; try { const { post } = await apiFetch<{ post: BlogPost }>( `/blog-posts/public/${slug}` ); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: post.featuredImageUrl ? [post.featuredImageUrl] : [], }, }; } catch { return { title: "Post not found" }; } } async function getPost(slug: string): Promise { try { const data = await apiFetch<{ success: boolean; post: BlogPost }>( `/blog-posts/public/${slug}` ); return data.post; } catch { return null; } } async function getRelatedPosts(post: BlogPost): Promise { try { // Get posts with shared category const cat = post.categories[0]; if (cat) { const result = await apiFetch>( `/blog-posts/public?category=${encodeURIComponent(cat)}&pageSize=4` ); const filtered = result.items.filter((p) => p.id !== post.id); if (filtered.length > 0) return filtered.slice(0, 4); } // Fallback: latest const result = await apiFetch>( "/blog-posts/public?pageSize=4" ); return result.items.filter((p) => p.id !== post.id).slice(0, 4); } catch { return []; } } async function getPopularPosts(excludeId: string): Promise { try { const result = await apiFetch>( "/blog-posts/public?sort=most_viewed&pageSize=6" ); return result.items.filter((p) => p.id !== excludeId).slice(0, 5); } catch { return []; } } async function getTopTags(): Promise { try { const result = await apiFetch>( "/blog-posts/public?pageSize=100" ); const counts: Record = {}; for (const post of result.items) { for (const tag of post.tags || []) { const t = tag.trim(); if (t) counts[t] = (counts[t] || 0) + 1; } } return Object.entries(counts) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count) .slice(0, 20); } catch { return []; } } export default async function BlogDetailPage({ params }: Props) { const { slug } = await params; const post = await getPost(slug); if (!post) return notFound(); const [relatedPosts, popularPosts, topTags] = await Promise.all([ getRelatedPosts(post), getPopularPosts(post.id), getTopTags(), ]); const contentHtml = post.contentFormat === "markdown" ? await marked(post.content) : post.content; const publishedDate = new Date(post.createdAt).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); return (
{/* ── Back button ────────────────────────────────────────────────────── */} {/* ── Article ────────────────────────────────────────────────────────── */}
{/* Categories */} {post.categories.length > 0 && (
{post.categories.map((cat) => ( {cat} ))}
)}

{post.title}

{post.excerpt && (

{post.excerpt}

)} {/* Meta */}
{publishedDate} {post.views.toLocaleString()} views {post.author && ( by {post.author.name || post.author.email} )}
{/* Featured image */} {post.featuredImageUrl && (
{post.featuredImageAlt
)} {/* Tags */} {post.tags.length > 0 && (
{post.tags.map((tag) => ( #{tag} ))}
)} {/* Content */}
{/* ── Related + Sidebar ──────────────────────────────────────────────── */}
{/* Related posts */}

More from the blog

{relatedPosts.length > 0 ? (
{relatedPosts.map((related) => (
{related.categories.length > 0 && (
{related.categories.slice(0, 2).map((cat) => ( {cat} ))}
)}

{related.title}

{related.excerpt}

{related.views.toLocaleString()}

))}
) : (

No related posts yet.

)}
{/* Sidebar */}
); }