canina/frontend/application/app/blog/[slug]/page.tsx
parsaaghayi bd49fbc4a0
Some checks failed
Deploy Canina / deploy (push) Successful in 2m25s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 7s
fix(security): remediate audit vulnerabilities SEC-001 through SEC-023
2026-09-23 23:00:25 +03:30

264 lines
8.0 KiB
TypeScript

import React from "react";
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import BlogPostClient from "../../../components/BlogPostClient";
import { getSeoConfig, formatPageTitle } from "../../../lib/seo";
import { safeJsonLd } from "../../../lib/schema";
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';
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.warn(`[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 = safeIso(blog.publishedAt) || safeIso(blog.createdAt);
const modifiedTime = safeIso(blog.updatedAt) || 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 || title,
},
],
url: canonicalUrl,
siteName: config.brandNameFa,
type: "article",
publishedTime,
modifiedTime,
authors: [authorName],
tags: blog.tags?.map((t: any) => typeof t === 'string' ? t : t?.name).filter(Boolean) || [],
},
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) {
notFound();
}
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 = safeLocalDate(blog.publishedAt) || safeLocalDate(blog.createdAt);
const canonicalUrl = blog.canonicalUrl
? blog.canonicalUrl.replace(/\/+$/, '')
: `${siteUrl}/blog/${slug}`;
const publishedIso = safeIso(blog.publishedAt) || safeIso(blog.createdAt) || new Date().toISOString();
const modifiedIso = safeIso(blog.updatedAt) || publishedIso;
const schemaType = blog.schemaType || 'BlogPosting';
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 || (Array.isArray(blog.tags) ? blog.tags.map((t: any) => typeof t === 'string' ? t : t?.name).filter(Boolean).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,
},
]),
],
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeJsonLd(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbJsonLd) }}
/>
<BlogPostClient
blog={blog}
publishedDate={publishedDate}
authorName={authorName}
/>
</>
);
}