canina/frontend/application/app/blog/[slug]/page.tsx
parsa aghaei fe6a900665
All checks were successful
Deploy Canina / deploy (push) Successful in 1m48s
feat(perf): implement comprehensive Core Web Vitals, ISR caching, and media optimizations
2026-08-24 17:23:43 +03:30

589 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from "react";
import {
ChevronRight,
Calendar,
User,
Clock,
Tag,
Share2,
MessageSquare,
Sparkles,
ShoppingBag,
ArrowLeft,
Heart,
Send,
Eye,
} from "lucide-react";
import Link from 'next/link';
import Image from 'next/image';
import type { Metadata } from 'next';
import { getSeoConfig, formatPageTitle } from "../../../lib/seo";
async function getBlog(rawSlug: string) {
try {
const slug = decodeURIComponent(rawSlug);
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001/api';
const apiBase = apiUrl.endsWith('/api') ? apiUrl : `${apiUrl.replace(/\/$/, '')}/api`;
const res = await fetch(`${apiBase}/blogs/${encodeURIComponent(slug)}`, {
next: {
tags: ['blogs', `blog-${slug}`],
revalidate: 3600,
},
});
if (!res.ok) {
console.error(`[Blog] Failed to fetch blog ${slug}: ${res.status}`);
return null;
}
const b = await res.json();
return b;
} catch (error) {
console.error('[Blog] Error fetching blog detail:', error);
return null;
}
}
export const revalidate = 3600;
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const blog = await getBlog(slug);
const config = await getSeoConfig();
if (!blog) {
return {
title: formatPageTitle('مقاله یافت نشد', config.brandNameFa, config.titleSeparator, config.titlePosition),
};
}
const rawSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || config.canonicalBaseUrl || 'https://canina.ir';
const siteUrl = rawSiteUrl.replace(/\/+$/, '');
const metaTitle = blog.metaTitle || blog.title;
const title = formatPageTitle(metaTitle, config.brandNameFa, config.titleSeparator, config.titlePosition, false);
const description =
blog.metaDescription ||
blog.excerpt ||
(typeof blog.content === 'string' ? blog.content.substring(0, 155).replace(/<[^>]+>/g, '') : '') ||
config.defaultMetaDescription;
const canonicalUrl = blog.canonicalUrl ? blog.canonicalUrl.replace(/\/+$/, '') : `${siteUrl}/blog/${slug}`;
const ogImageUrl = blog.ogImage || blog.imageUrl || config.ogImageUrl;
const ogTitle = blog.ogTitle || title;
const ogDesc = blog.ogDescription || description;
const authorName = blog.author
? `${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 keywordsList = blog.focusKeywords
? blog.focusKeywords.split(',').map((k: string) => k.trim())
: blog.keywords
? blog.keywords.split(',').map((k: string) => k.trim())
: undefined;
return {
title,
description,
keywords: keywordsList,
alternates: {
canonical: canonicalUrl,
},
robots: {
index: !blog.noIndex,
follow: !blog.noFollow,
googleBot: {
index: !blog.noIndex,
follow: !blog.noFollow,
'max-image-preview': 'large',
'max-snippet': -1,
'max-video-preview': -1,
},
},
openGraph: {
title: ogTitle,
description: ogDesc,
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: blog.imageAlt || blog.title,
},
],
url: canonicalUrl,
siteName: config.brandNameFa,
type: "article",
publishedTime,
modifiedTime,
authors: [authorName],
tags: blog.tags?.map((t: any) => t.name) || [],
},
twitter: {
card: "summary_large_image",
title: ogTitle,
description: ogDesc,
images: [ogImageUrl],
},
};
}
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const blog = await getBlog(slug);
const config = await getSeoConfig();
if (!blog) {
return (
<div className="min-h-[60vh] flex flex-col items-center justify-center font-vazir text-center px-4" dir="rtl">
<div className="w-16 h-16 rounded-3xl bg-red-100 text-red-600 flex items-center justify-center mb-4">
<MessageSquare className="w-8 h-8" />
</div>
<h1 className="text-2xl sm:text-3xl font-black text-medical-gray-900 mb-2">مقاله مورد نظر یافت نشد</h1>
<p className="text-sm text-medical-gray-500 mb-6">احتمالاً این مقاله حذف یا به آدرس دیگری منتقل شده است.</p>
<Link
href="/blog"
className="px-6 py-3 rounded-2xl bg-canina-blue text-white text-xs font-black flex items-center gap-2 hover:bg-canina-blue/90 shadow-md"
>
<ChevronRight className="w-4 h-4" />
<span>بازگشت به مجله سلامت</span>
</Link>
</div>
);
}
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 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;
// JSON-LD Structured Data Schema (BlogPosting & BreadcrumbList)
const schemaType = blog.schemaType || 'BlogPosting';
// Robust Author object for Google Rich Results
const authorSchema = blog.author && (blog.author.firstName || blog.author.lastName)
? {
'@type': 'Person',
name: authorName,
url: `${siteUrl}/about`,
}
: {
'@type': 'Organization',
name: config.brandNameFa || 'کنینا ایران',
url: siteUrl,
};
const articleJsonLd = {
'@context': 'https://schema.org',
'@type': schemaType,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': canonicalUrl,
},
headline: blog.title,
description: blog.excerpt || blog.metaDescription || blog.title,
image: blog.imageUrl
? [blog.imageUrl.startsWith('http') ? blog.imageUrl : `${siteUrl}${blog.imageUrl.startsWith('/') ? '' : '/'}${blog.imageUrl}`]
: [config.ogImageUrl.startsWith('http') ? config.ogImageUrl : `${siteUrl}${config.ogImageUrl.startsWith('/') ? '' : '/'}${config.ogImageUrl}`],
datePublished: publishedIso,
dateModified: modifiedIso,
author: authorSchema,
publisher: {
'@type': 'Organization',
name: config.brandNameFa || 'کنینا ایران',
logo: {
'@type': 'ImageObject',
url: `${siteUrl}/assets/logo.png`,
},
},
articleSection: blog.category?.name || 'دانشنامه سلامت حیوانات',
keywords: blog.focusKeywords || blog.tags?.map((t: any) => t.name).join(', ') || undefined,
};
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'خانه',
item: siteUrl,
},
{
'@type': 'ListItem',
position: 2,
name: 'مجله سلامت',
item: `${siteUrl}/blog`,
},
...(blog.category
? [
{
'@type': 'ListItem',
position: 3,
name: blog.category.name,
item: `${siteUrl}/blog?categoryId=${blog.category.id}`,
},
{
'@type': 'ListItem',
position: 4,
name: blog.title,
item: canonicalUrl,
},
]
: [
{
'@type': 'ListItem',
position: 3,
name: blog.title,
item: canonicalUrl,
},
]),
],
};
const relatedProducts = blog.relatedProducts || [];
const relatedBlogs = blog.relatedBlogs || [];
const comments = blog.comments || [];
return (
<div className="min-h-screen bg-medical-gray-50 pt-8 pb-24 px-4 font-vazir" dir="rtl">
{/* Inject JSON-LD Schema */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<div className="max-w-7xl mx-auto">
{/* Breadcrumbs */}
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400 mb-8 pb-4 border-b border-medical-gray-200/60 overflow-x-auto">
<Link href="/" className="hover:text-canina-blue transition-colors whitespace-nowrap">خانه</Link>
<ChevronRight className="w-3 h-3 text-medical-gray-300 shrink-0" />
<Link href="/blog" className="hover:text-canina-blue transition-colors whitespace-nowrap">مجله سلامت</Link>
{blog.category && (
<>
<ChevronRight className="w-3 h-3 text-medical-gray-300 shrink-0" />
<Link href={`/blog?categoryId=${blog.category.id}`} className="hover:text-canina-blue transition-colors whitespace-nowrap">
{blog.category.name}
</Link>
</>
)}
<ChevronRight className="w-3 h-3 text-medical-gray-300 shrink-0" />
<span className="text-canina-blue font-black truncate max-w-xs sm:max-w-md">{blog.title}</span>
</div>
<div className="grid lg:grid-cols-12 gap-8 lg:gap-12">
{/* Main Article Content Column */}
<article className="lg:col-span-8 space-y-8">
<div className="bg-white rounded-[3rem] border border-medical-gray-200 shadow-xl overflow-hidden p-6 sm:p-12">
{/* Featured Image */}
{blog.imageUrl && (
<div className="aspect-video w-full overflow-hidden rounded-[2rem] relative mb-8 shadow-sm">
<Image
src={blog.imageUrl}
alt={blog.imageAlt || blog.title}
fill
priority
sizes="(max-width: 1024px) 100vw, 800px"
className="w-full h-full object-cover"
unoptimized
/>
{blog.imageCaption && (
<div className="absolute bottom-0 inset-x-0 bg-black/60 backdrop-blur-sm text-white text-xs font-medium py-2 px-4 text-center">
{blog.imageCaption}
</div>
)}
</div>
)}
{/* Meta details bar */}
<div className="flex flex-wrap items-center gap-3 text-xs font-bold text-medical-gray-400 mb-6">
{blog.category && (
<span className="bg-canina-blue/10 text-canina-blue px-3.5 py-1 rounded-full uppercase tracking-wider font-black">
{blog.category.name}
</span>
)}
<div className="flex items-center gap-1">
<Calendar className="w-3.5 h-3.5 text-canina-blue" />
<span>{publishedDate}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3.5 h-3.5 text-canina-blue" />
<span>{blog.readingTime || 4} دقیقه مطالعه</span>
</div>
<div className="flex items-center gap-1">
<Eye className="w-3.5 h-3.5 text-canina-blue" />
<span>{blog.viewCount || 0} بازدید</span>
</div>
</div>
{/* Title */}
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-black text-medical-gray-900 leading-tight mb-6">
{blog.title}
</h1>
{/* Excerpt / Summary Callout */}
{blog.excerpt && (
<div className="bg-canina-blue/5 border-r-4 border-canina-blue p-5 rounded-l-2xl mb-8 text-sm sm:text-base font-bold text-medical-gray-800 leading-relaxed italic">
{blog.excerpt}
</div>
)}
{/* Main Rich Content HTML Renderer */}
<div
className="prose prose-base sm:prose-lg max-w-none text-medical-gray-700 leading-loose space-y-5 article-html-content"
dangerouslySetInnerHTML={{ __html: blog.content || '' }}
/>
{/* Tags Chip Footer */}
{blog.tags && blog.tags.length > 0 && (
<div className="pt-8 mt-10 border-t border-medical-gray-100 flex flex-wrap items-center gap-2">
<span className="text-xs font-bold text-medical-gray-400 flex items-center gap-1">
<Tag className="w-3.5 h-3.5" />
برچسبهای مرتبط:
</span>
{blog.tags.map((tag: any) => (
<Link
key={tag.id}
href={`/blog?tag=${encodeURIComponent(tag.name)}`}
className="px-3 py-1 bg-purple-50 text-purple-700 hover:bg-purple-100 border border-purple-200 rounded-xl text-xs font-bold transition-colors"
>
#{tag.name}
</Link>
))}
</div>
)}
{/* Author Box */}
<div className="mt-10 pt-8 border-t border-medical-gray-100 flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-medical-gray-50/70 p-6 rounded-3xl">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-canina-blue/10 rounded-2xl flex items-center justify-center text-canina-blue shadow-inner">
<User className="w-7 h-7" />
</div>
<div>
<div className="font-black text-medical-gray-900 text-base">{authorName}</div>
<div className="font-bold text-xs text-medical-gray-400 mt-0.5">
تیم تحقیق و توسعه علمی کنینا آلمان (Canina Pharma)
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
if (navigator.share) {
navigator.share({ title: blog.title, url: window.location.href });
} else {
navigator.clipboard.writeText(window.location.href);
alert('لینک مقاله در کلیپ‌بورد کپی شد');
}
}}
className="p-3 bg-white rounded-2xl hover:bg-canina-blue hover:text-white transition-all text-medical-gray-600 border border-medical-gray-200 shadow-xs cursor-pointer flex items-center gap-1.5 text-xs font-bold"
>
<Share2 className="w-4 h-4" />
<span>اشتراکگذاری</span>
</button>
</div>
</div>
</div>
{/* Comments Section */}
{blog.allowComments !== false && (
<div className="bg-white rounded-[3rem] border border-medical-gray-200 shadow-xl p-6 sm:p-12 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-xl font-black text-medical-gray-900 flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-canina-blue" />
<span>دیدگاهها و پرسشهای کاربران ({comments.length})</span>
</h3>
</div>
{/* Comment Form */}
<form
onSubmit={(e) => {
e.preventDefault();
alert('دیدگاه شما با موفقیت ثبت شد و پس از تایید مدیریت نمایش داده می‌شود.');
}}
className="space-y-4 bg-medical-gray-50 p-6 rounded-3xl border border-medical-gray-100"
>
<span className="text-xs font-bold text-medical-gray-700 block">
پرسش یا تجربه خود درباره این موضوع را با ما در میان بگذارید:
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<input
type="text"
required
placeholder="نام و نام خانوادگی..."
className="px-4 py-2.5 bg-white rounded-xl border border-medical-gray-200 text-xs font-bold outline-none"
/>
<input
type="email"
required
placeholder="آدرس ایمیل..."
className="px-4 py-2.5 bg-white rounded-xl border border-medical-gray-200 text-xs font-bold outline-none"
/>
</div>
<textarea
rows={3}
required
placeholder="متن دیدگاه شما..."
className="w-full px-4 py-2.5 bg-white rounded-xl border border-medical-gray-200 text-xs font-bold outline-none"
/>
<button
type="submit"
className="px-6 py-2.5 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-blue/90 transition-all flex items-center gap-1.5 cursor-pointer shadow-md"
>
<Send className="w-3.5 h-3.5" />
<span>ارسال نظر</span>
</button>
</form>
{/* Comments List */}
{comments.length > 0 ? (
<div className="space-y-3 pt-4">
{comments.map((c: any) => (
<div key={c.id} className="p-4 rounded-2xl bg-medical-gray-50 border border-medical-gray-100 space-y-1">
<div className="flex items-center justify-between text-xs font-bold">
<span className="text-medical-gray-900">{c.authorName}</span>
<span className="text-medical-gray-400 text-[10px]">
{c.createdAt ? new Date(c.createdAt).toLocaleDateString('fa-IR') : ''}
</span>
</div>
<p className="text-xs text-medical-gray-600 leading-relaxed">{c.content}</p>
</div>
))}
</div>
) : (
<p className="text-xs text-medical-gray-400 text-center py-4">
هنوز دیدگاهی برای این مقاله ثبت نشده است. اولین نظر را شما بنویسید!
</p>
)}
</div>
)}
</article>
{/* Sidebar Column */}
<aside className="lg:col-span-4 space-y-8">
{/* Related Products Box */}
{relatedProducts.length > 0 && (
<div className="bg-canina-blue rounded-[3rem] p-8 text-white shadow-2xl relative overflow-hidden">
<div className="absolute top-0 left-0 p-8 opacity-10 pointer-events-none">
<Sparkles className="w-24 h-24" />
</div>
<div className="flex items-center gap-2 mb-6 relative z-10">
<ShoppingBag className="w-5 h-5 text-canina-gold" />
<h4 className="text-xl font-black italic text-white">مکملهای مرتبط این مقاله</h4>
</div>
<div className="space-y-4 relative z-10">
{relatedProducts.map((p: any) => (
<Link
key={p.id}
href={`/shop/${p.slug || p.id}`}
className="bg-white/10 hover:bg-white/20 border border-white/20 p-3.5 rounded-2xl flex items-center gap-3 transition-all group"
>
<div className="w-14 h-14 bg-white rounded-xl p-1.5 shrink-0 overflow-hidden relative group-hover:scale-105 transition-transform">
<Image
src={p.images?.[0]?.url || p.imageUrl || '/assets/images/product-placeholder.png'}
alt={p.nameFa}
fill
className="w-full h-full object-contain"
unoptimized
/>
</div>
<div className="flex-1 min-w-0">
<span className="text-[10px] font-bold text-white/60 block truncate">{p.category?.name}</span>
<h5 className="text-xs font-black text-white truncate group-hover:text-canina-gold transition-colors">
{p.nameFa}
</h5>
{p.prices?.[0]?.price && (
<span className="text-[11px] font-bold text-canina-gold font-mono mt-0.5 block">
{Number(p.prices[0].price).toLocaleString('fa-IR')} تومان
</span>
)}
</div>
</Link>
))}
</div>
</div>
)}
{/* Related Blogs Box */}
{relatedBlogs.length > 0 && (
<div className="bg-white rounded-[3rem] border border-medical-gray-200 p-8 shadow-xl space-y-6">
<h4 className="text-lg font-black text-medical-gray-900">مقالات پیشنهادی</h4>
<div className="space-y-4">
{relatedBlogs.map((item: any) => (
<Link
key={item.id}
href={`/blog/${item.slug}`}
className="flex items-center gap-3 group"
>
<div className="w-16 h-14 rounded-2xl overflow-hidden shrink-0 relative bg-medical-gray-100">
<Image
src={item.imageUrl || '/assets/images/blog-placeholder.png'}
alt={item.title}
fill
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
unoptimized
/>
</div>
<div className="flex-1 min-w-0">
<h5 className="text-xs font-bold text-medical-gray-900 group-hover:text-canina-blue transition-colors line-clamp-2 leading-snug">
{item.title}
</h5>
<span className="text-[10px] text-medical-gray-400 mt-1 block">
{item.readingTime || 3} دقیقه مطالعه
</span>
</div>
</Link>
))}
</div>
</div>
)}
{/* Scientific Consultation CTA */}
<div className="bg-gradient-to-br from-medical-gray-900 to-canina-blue rounded-[3rem] p-8 text-white shadow-xl space-y-4">
<div className="inline-flex p-3 rounded-2xl bg-white/10">
<Sparkles className="w-6 h-6 text-canina-gold" />
</div>
<h4 className="text-lg font-black leading-snug">نیاز به راهنمایی و دوز درمانی دارید؟</h4>
<p className="text-xs text-white/80 leading-relaxed">
مشاوران علمی و دامپزشکان داروسازی کنینا آماده پاسخگویی به سوالات شما در مورد وضعیت سلامت پت شما هستند.
</p>
<Link
href="/smart-advisor"
className="inline-flex items-center justify-center gap-2 w-full py-3 rounded-2xl bg-canina-gold text-canina-dark text-xs font-black hover:bg-white transition-all shadow-lg"
>
<span>مشاوره هوشمند دارویی</span>
<ArrowLeft className="w-4 h-4" />
</Link>
</div>
</aside>
</div>
</div>
</div>
);
}