import { Request, Response } from 'express'; import prisma from '../models/prisma'; // Type for blog post with author type BlogPostWithAuthor = { id: string; title: string; slug: string; content: string; excerpt: string | null; featuredImage: string | null; tags: string | null; status: string; publishedAt: Date | null; createdAt: Date; updatedAt: Date; authorId: string; author: { name: string | null } | null; }; // GET /api/blog/:slug/related (Public) export const getRelatedPosts = async (req: Request, res: Response) => { try { const { slug } = req.params; // Find the current post to get its tags const currentPost = await prisma.blogPost.findFirst({ where: { slug } }); if (!currentPost) { return res.status(404).json({ error: 'Post not found' }); } // Parse tags let tags: string[] = []; try { if (currentPost.tags) tags = JSON.parse(currentPost.tags); } catch (e) { // ignore } // If no tags, just return 3 latest posts excluding current if (tags.length === 0) { const latest = await prisma.blogPost.findMany({ where: { status: 'PUBLISHED', slug: { not: slug }, publishedAt: { lte: new Date() } }, orderBy: { publishedAt: 'desc' }, take: 3, include: { author: { select: { name: true } } } }); return res.status(200).json(latest); } // SQLite doesn't natively support querying inside JSON arrays easily without raw queries or specific extensions, // so we'll fetch recently published posts and manually filter/score them by tag overlap. // For a production app with Postgres, you'd use a raw query or better JSON filtering. const allOtherPosts = await prisma.blogPost.findMany({ where: { status: 'PUBLISHED', slug: { not: slug }, publishedAt: { lte: new Date() } }, orderBy: { publishedAt: 'desc' }, take: 50, // Limit to recent 50 to avoid massive memory usage include: { author: { select: { name: true } } } }); // Score based on shared tags const scoredPosts: { post: BlogPostWithAuthor; score: number }[] = allOtherPosts.map((post: BlogPostWithAuthor) => { let postTags: string[] = []; try { if (post.tags) postTags = JSON.parse(post.tags); } catch (e) { } const intersection = tags.filter(t => postTags.includes(t)); return { post, score: intersection.length }; }); // Sort by score (desc), then date (desc) implicitly from the DB query scoredPosts.sort((a: { post: BlogPostWithAuthor; score: number }, b: { post: BlogPostWithAuthor; score: number }) => b.score - a.score); // Take top 3 const related = scoredPosts.slice(0, 3).map((s: { post: BlogPostWithAuthor; score: number }) => s.post); return res.status(200).json(related); } catch (error) { console.error('Error fetching related posts:', error); return res.status(500).json({ error: 'Internal server error' }); } }