From 088f8e20bbfe126839ee114010fef5a9b5954b60 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Tue, 25 Aug 2026 10:21:50 +0330 Subject: [PATCH] fix(blog): resolve 404 and 500 error on blog post slug navigation --- backend/src/blogs/blogs.service.ts | 158 +- frontend/application/app/blog/[slug]/page.tsx | 153 +- frontend/application/components/BlogPage.tsx | 4 +- .../components/BlogPreviewSection.tsx | 8 +- graphify-out/.graphify_labels.json | 54 +- graphify-out/.graphify_labels.json.sig | 2 +- .../2026-08-25/.graphify_analysis.json | 4948 + graphify-out/2026-08-25/.graphify_labels.json | 316 + .../2026-08-25/.graphify_semantic_marker | 1 + graphify-out/2026-08-25/GRAPH_REPORT.md | 1131 + graphify-out/2026-08-25/graph.json | 129221 +++++++++++++++ graphify-out/2026-08-25/manifest.json | 3560 + graphify-out/GRAPH_REPORT.md | 301 +- ...64c68c8912277ad9645310f7e8116fbcba741.json | 1 + ...1fcbe9f74db7901cf204b5058da3132d3c1d2.json | 1 + graphify-out/cache/stat-index.json | 2 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 9510 +- graphify-out/manifest.json | 1168 +- 19 files changed, 145201 insertions(+), 5346 deletions(-) create mode 100644 graphify-out/2026-08-25/.graphify_analysis.json create mode 100644 graphify-out/2026-08-25/.graphify_labels.json create mode 100644 graphify-out/2026-08-25/.graphify_semantic_marker create mode 100644 graphify-out/2026-08-25/GRAPH_REPORT.md create mode 100644 graphify-out/2026-08-25/graph.json create mode 100644 graphify-out/2026-08-25/manifest.json create mode 100644 graphify-out/cache/ast/v0.9.44/c564f4aba35d32a733a5837c33664c68c8912277ad9645310f7e8116fbcba741.json create mode 100644 graphify-out/cache/ast/v0.9.44/d6cc5e861e43bf82dcffe1ad7121fcbe9f74db7901cf204b5058da3132d3c1d2.json diff --git a/backend/src/blogs/blogs.service.ts b/backend/src/blogs/blogs.service.ts index 9e99b1c..ccfe175 100644 --- a/backend/src/blogs/blogs.service.ts +++ b/backend/src/blogs/blogs.service.ts @@ -137,10 +137,28 @@ export class BlogsService { return { mode: 'newest', blogs: newestBlogs }; } - async findOneBySlug(slug: string) { - const now = new Date(); - const blog = await this.prisma.blog.findUnique({ - where: { slug }, + async findOneBySlug(rawSlug: string) { + if (!rawSlug) { + throw new NotFoundException('مقاله یافت نشد'); + } + + let decodedSlug = rawSlug; + try { + decodedSlug = decodeURIComponent(rawSlug); + } catch { + decodedSlug = rawSlug; + } + + const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(decodedSlug); + + const blog = await this.prisma.blog.findFirst({ + where: { + OR: [ + { slug: decodedSlug }, + { slug: rawSlug }, + ...(isUuid ? [{ id: decodedSlug }] : []), + ], + }, include: { author: { select: { firstName: true, lastName: true }, @@ -161,68 +179,90 @@ export class BlogsService { throw new NotFoundException('مقاله یافت نشد'); } - // Fetch related products if any + // Fetch related products safely let relatedProducts: any[] = []; - if (blog.relatedProductIds && blog.relatedProductIds.length > 0) { - relatedProducts = await this.prisma.product.findMany({ - where: { id: { in: blog.relatedProductIds } }, - select: { - id: true, - nameFa: true, - slug: true, - priceValue: true, - priceDisplay: true, - imageUrl: true, - categorySlug: true, - unit: true, - }, - }); + if (blog.relatedProductIds && Array.isArray(blog.relatedProductIds) && blog.relatedProductIds.length > 0) { + const validProductUuids = blog.relatedProductIds.filter( + (id) => typeof id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) + ); + if (validProductUuids.length > 0) { + try { + relatedProducts = await this.prisma.product.findMany({ + where: { id: { in: validProductUuids } }, + select: { + id: true, + nameFa: true, + slug: true, + priceValue: true, + priceDisplay: true, + imageUrl: true, + categorySlug: true, + unit: true, + }, + }); + } catch (err) { + console.warn('[BlogsService] Error fetching related products:', err); + } + } } - // Fetch related blogs if any or fallback to same category + // Fetch related blogs safely let relatedBlogs: any[] = []; - if (blog.relatedBlogIds && blog.relatedBlogIds.length > 0) { - relatedBlogs = await this.prisma.blog.findMany({ - where: { - id: { in: blog.relatedBlogIds }, - isPublished: true, - status: 'PUBLISHED', - }, - select: { - id: true, - title: true, - slug: true, - imageUrl: true, - readingTime: true, - publishedAt: true, - createdAt: true, - category: { select: { name: true, slug: true } }, - }, - }); + if (blog.relatedBlogIds && Array.isArray(blog.relatedBlogIds) && blog.relatedBlogIds.length > 0) { + const validBlogUuids = blog.relatedBlogIds.filter( + (id) => typeof id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) + ); + if (validBlogUuids.length > 0) { + try { + relatedBlogs = await this.prisma.blog.findMany({ + where: { + id: { in: validBlogUuids }, + isPublished: true, + status: 'PUBLISHED', + }, + select: { + id: true, + title: true, + slug: true, + imageUrl: true, + readingTime: true, + publishedAt: true, + createdAt: true, + category: { select: { name: true, slug: true } }, + }, + }); + } catch (err) { + console.warn('[BlogsService] Error fetching related blogs:', err); + } + } } else if (blog.categoryId) { - relatedBlogs = await this.prisma.blog.findMany({ - where: { - categoryId: blog.categoryId, - id: { not: blog.id }, - isPublished: true, - status: 'PUBLISHED', - }, - take: 3, - select: { - id: true, - title: true, - slug: true, - imageUrl: true, - readingTime: true, - publishedAt: true, - createdAt: true, - category: { select: { name: true, slug: true } }, - }, - }); + try { + relatedBlogs = await this.prisma.blog.findMany({ + where: { + categoryId: blog.categoryId, + id: { not: blog.id }, + isPublished: true, + status: 'PUBLISHED', + }, + take: 3, + select: { + id: true, + title: true, + slug: true, + imageUrl: true, + readingTime: true, + publishedAt: true, + createdAt: true, + category: { select: { name: true, slug: true } }, + }, + }); + } catch (err) { + console.warn('[BlogsService] Error fetching fallback related blogs:', err); + } } // Increment view count asynchronously - await this.prisma.blog + this.prisma.blog .update({ where: { id: blog.id }, data: { viewCount: { increment: 1 } }, @@ -231,7 +271,7 @@ export class BlogsService { return { ...blog, - viewCount: blog.viewCount + 1, + viewCount: (blog.viewCount || 0) + 1, relatedProducts, relatedBlogs, }; diff --git a/frontend/application/app/blog/[slug]/page.tsx b/frontend/application/app/blog/[slug]/page.tsx index b76998c..83c1956 100644 --- a/frontend/application/app/blog/[slug]/page.tsx +++ b/frontend/application/app/blog/[slug]/page.tsx @@ -10,16 +10,37 @@ import { Sparkles, ShoppingBag, ArrowLeft, - Heart, Send, Eye, } from "lucide-react"; import Link from 'next/link'; -import Image from 'next/image'; +import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; +import SafeImage from "../../../components/SafeImage"; import { getSeoConfig, formatPageTitle } from "../../../lib/seo"; +function safeIso(val: any, fallback?: string): string | undefined { + if (!val) return fallback; + try { + const d = new Date(val); + return isNaN(d.getTime()) ? fallback : d.toISOString(); + } catch { + return fallback; + } +} + +function safeLocalDate(val: any): string { + if (!val) return ''; + try { + const d = new Date(val); + return isNaN(d.getTime()) ? '' : d.toLocaleDateString('fa-IR'); + } catch { + return ''; + } +} + async function getBlog(rawSlug: string) { + if (!rawSlug) return null; try { const slug = decodeURIComponent(rawSlug); const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001/api'; @@ -31,7 +52,7 @@ async function getBlog(rawSlug: string) { }, }); if (!res.ok) { - console.error(`[Blog] Failed to fetch blog ${slug}: ${res.status}`); + console.warn(`[Blog] Failed to fetch blog ${slug}: ${res.status}`); return null; } const b = await res.json(); @@ -58,7 +79,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str const rawSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || config.canonicalBaseUrl || 'https://canina.ir'; const siteUrl = rawSiteUrl.replace(/\/+$/, ''); - const metaTitle = blog.metaTitle || blog.title; + const metaTitle = blog.metaTitle || blog.title || 'مقاله آموزشی و تخصصی کنینا'; const title = formatPageTitle(metaTitle, config.brandNameFa, config.titleSeparator, config.titlePosition, false); const description = blog.metaDescription || @@ -74,8 +95,8 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str ? `${blog.author.firstName || ''} ${blog.author.lastName || ''}`.trim() || 'نویسنده کنینا' : 'نویسنده و پژوهشگر کنینا'; - const publishedTime = blog.publishedAt ? new Date(blog.publishedAt).toISOString() : blog.createdAt ? new Date(blog.createdAt).toISOString() : undefined; - const modifiedTime = blog.updatedAt ? new Date(blog.updatedAt).toISOString() : publishedTime; + const publishedTime = safeIso(blog.publishedAt) || safeIso(blog.createdAt); + const modifiedTime = safeIso(blog.updatedAt) || publishedTime; const keywordsList = blog.focusKeywords ? blog.focusKeywords.split(',').map((k: string) => k.trim()) @@ -109,7 +130,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str url: ogImageUrl, width: 1200, height: 630, - alt: blog.imageAlt || blog.title, + alt: blog.imageAlt || blog.title || title, }, ], url: canonicalUrl, @@ -118,7 +139,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str publishedTime, modifiedTime, authors: [authorName], - tags: blog.tags?.map((t: any) => t.name) || [], + tags: blog.tags?.map((t: any) => typeof t === 'string' ? t : t?.name).filter(Boolean) || [], }, twitter: { card: "summary_large_image", @@ -135,46 +156,23 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: const config = await getSeoConfig(); if (!blog) { - return ( -
-
- -
-

مقاله مورد نظر یافت نشد

-

احتمالاً این مقاله حذف یا به آدرس دیگری منتقل شده است.

- - - بازگشت به مجله سلامت - -
- ); + notFound(); } - const rawSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || config.canonicalBaseUrl || 'https://canina.ir'; + const rawSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || config.canonicalBaseUrl || 'https://canina.ir'; const siteUrl = rawSiteUrl.replace(/\/+$/, ''); const authorName = blog.author ? `${blog.author.firstName || ''} ${blog.author.lastName || ''}`.trim() || 'نویسنده کنینا' : 'نویسنده و پژوهشگر کنینا'; - const publishedDate = blog.publishedAt - ? new Date(blog.publishedAt).toLocaleDateString('fa-IR') - : blog.createdAt - ? new Date(blog.createdAt).toLocaleDateString('fa-IR') - : ''; + const publishedDate = safeLocalDate(blog.publishedAt) || safeLocalDate(blog.createdAt); const canonicalUrl = blog.canonicalUrl ? blog.canonicalUrl.replace(/\/+$/, '') : `${siteUrl}/blog/${slug}`; - const publishedIso = blog.publishedAt - ? new Date(blog.publishedAt).toISOString() - : blog.createdAt - ? new Date(blog.createdAt).toISOString() - : new Date().toISOString(); - const modifiedIso = blog.updatedAt ? new Date(blog.updatedAt).toISOString() : publishedIso; + const publishedIso = safeIso(blog.publishedAt) || safeIso(blog.createdAt) || new Date().toISOString(); + const modifiedIso = safeIso(blog.updatedAt) || publishedIso; // JSON-LD Structured Data Schema (BlogPosting & BreadcrumbList) const schemaType = blog.schemaType || 'BlogPosting'; @@ -216,7 +214,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: }, }, articleSection: blog.category?.name || 'دانشنامه سلامت حیوانات', - keywords: blog.focusKeywords || blog.tags?.map((t: any) => t.name).join(', ') || undefined, + keywords: blog.focusKeywords || (Array.isArray(blog.tags) ? blog.tags.map((t: any) => typeof t === 'string' ? t : t?.name).filter(Boolean).join(', ') : undefined), }; const breadcrumbJsonLd = { @@ -261,9 +259,9 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: ], }; - const relatedProducts = blog.relatedProducts || []; - const relatedBlogs = blog.relatedBlogs || []; - const comments = blog.comments || []; + const relatedProducts = Array.isArray(blog.relatedProducts) ? blog.relatedProducts : []; + const relatedBlogs = Array.isArray(blog.relatedBlogs) ? blog.relatedBlogs : []; + const comments = Array.isArray(blog.comments) ? blog.comments : []; return (
@@ -302,14 +300,13 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: {/* Featured Image */} {blog.imageUrl && (
- {blog.imageAlt {blog.imageCaption && (
@@ -326,10 +323,12 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: {blog.category.name} )} -
- - {publishedDate} -
+ {publishedDate && ( +
+ + {publishedDate} +
+ )}
{blog.readingTime || 4} دقیقه مطالعه @@ -359,21 +358,26 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: /> {/* Tags Chip Footer */} - {blog.tags && blog.tags.length > 0 && ( + {Array.isArray(blog.tags) && blog.tags.length > 0 && (
برچسب‌های مرتبط: - {blog.tags.map((tag: any) => ( - - #{tag.name} - - ))} + {blog.tags.map((tag: any, idx: number) => { + const tagName = typeof tag === 'string' ? tag : tag?.name || ''; + const tagId = tag?.id || idx; + if (!tagName) return null; + return ( + + #{tagName} + + ); + })}
)} @@ -393,11 +397,13 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: