canina/frontend/application/app/wiki/[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

218 lines
9.2 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 Link from 'next/link';
import { ChevronRight, Sparkles, Package, ArrowLeft, ShieldCheck } from 'lucide-react';
import type { Metadata } from 'next';
import sanitizeHtml from 'sanitize-html';
import { getSeoConfig, formatPageTitle } from "../../../lib/seo";
import { generateMedicalWebPageSchema, generateBreadcrumbSchema, safeJsonLd } from "../../../lib/schema";
import SafeImage from "../../../components/SafeImage";
const API_URL = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001/api';
const apiBase = API_URL.endsWith('/api') ? API_URL : `${API_URL.replace(/\/$/, '')}/api`;
async function getWikiTerm(key: string) {
try {
const res = await fetch(`${apiBase}/wiki/${encodeURIComponent(key)}`, {
next: { revalidate: 3600, tags: ['wiki', `wiki-${key}`] }
});
if (!res.ok) return null;
return await res.json();
} catch (error) {
console.error('[Wiki] Error fetching term:', error);
return null;
}
}
async function getRelatedProducts(termName: string) {
try {
const res = await fetch(`${apiBase}/products?search=${encodeURIComponent(termName)}&limit=4`, {
next: { revalidate: 3600, tags: ['products'] },
});
if (!res.ok) return [];
const json = await res.json();
return json.data || json || [];
} catch (error) {
console.error('[Wiki] Error fetching related products for ingredient:', error);
return [];
}
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const term = await getWikiTerm(slug);
const config = await getSeoConfig();
if (!term) {
return {
title: formatPageTitle('ترکیب علمی یافت نشد', config.brandNameFa, config.titleSeparator, config.titlePosition),
};
}
const rawTitle = term.metaTitle || `ترکیب دارویی و خواص ${term.term}`;
const title = formatPageTitle(rawTitle, config.brandNameFa, config.titleSeparator, config.titlePosition, false);
const description =
term.metaDescription ||
(term.definition ? term.definition.replace(/<[^>]+>/g, '').substring(0, 155).trim() : '') ||
`بررسی خواص بالینی، فواید و مکمل‌های دارویی حاوی ${term.term} در دامپزشکی کنینا آلمان.`;
const canonicalUrl = `${config.canonicalBaseUrl.replace(/\/$/, '')}/wiki/${slug}`;
return {
title,
description,
alternates: {
canonical: canonicalUrl,
},
openGraph: {
title,
description,
url: canonicalUrl,
siteName: config.brandNameFa,
type: "article",
},
twitter: {
card: "summary",
title,
description,
},
};
}
export default async function WikiTermPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const term = await getWikiTerm(slug);
if (!term) {
return (
<div className="min-h-[50vh] flex flex-col items-center justify-center font-vazir text-center px-4" dir="rtl">
<h1 className="text-3xl font-black text-medical-gray-900 mb-4">ترکیب علمی یافت نشد</h1>
<Link href="/wiki" className="text-canina-blue font-bold flex items-center gap-2">
<ChevronRight className="w-4 h-4" />
بازگشت به دانشنامه
</Link>
</div>
);
}
const relatedProducts = await getRelatedProducts(term.term);
const medicalSchema = generateMedicalWebPageSchema({
title: `ترکیب درمانی ${term.term} - دانشنامه تخصصی کنینا`,
description: term.metaDescription || (term.definition ? term.definition.replace(/<[^>]+>/g, '').substring(0, 160) : ''),
url: `https://canina.ir/wiki/${slug}`,
aspects: [term.term, 'Veterinary Supplements', 'Pet Nutrition'],
});
const breadcrumbs = generateBreadcrumbSchema([
{ name: 'صفحه اصلی', url: 'https://canina.ir' },
{ name: 'دانشنامه ترکیبات', url: 'https://canina.ir/wiki' },
{ name: term.term, url: `https://canina.ir/wiki/${slug}` },
]);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeJsonLd(medicalSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbs) }}
/>
<div className="bg-medical-gray-50 min-h-screen py-12 px-4 sm:px-6 lg:px-8 font-vazir" dir="rtl">
<div className="max-w-4xl mx-auto space-y-8">
{/* Breadcrumb back link */}
<nav className="flex items-center gap-2 text-xs font-bold text-medical-gray-500">
<Link href="/" className="hover:text-canina-blue transition-colors">صفحه اصلی</Link>
<ChevronRight className="w-3.5 h-3.5 text-medical-gray-300" />
<Link href="/wiki" className="hover:text-canina-blue transition-colors">دانشنامه ترکیبات</Link>
<ChevronRight className="w-3.5 h-3.5 text-medical-gray-300" />
<span className="text-canina-blue">{term.term}</span>
</nav>
{/* Main Article Container */}
<article className="bg-white border border-medical-gray-200 rounded-[3rem] p-8 md:p-12 shadow-xl space-y-8">
<div className="space-y-3 border-b border-medical-gray-100 pb-6">
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-canina-blue/5 border border-canina-blue/10 rounded-full text-canina-blue text-xs font-black">
<Sparkles className="w-3.5 h-3.5" />
ماده موثره و ترکیب بالینی
</span>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-black text-medical-gray-900 leading-tight">
{term.term}
</h1>
</div>
<div
className="prose prose-lg max-w-none text-medical-gray-700 leading-relaxed prose-headings:font-black prose-headings:text-medical-gray-900 prose-p:leading-loose"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(term.definition || '', {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2', 'span']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
span: ['class', 'style'],
div: ['class'],
p: ['class'],
},
}),
}}
/>
</article>
{/* Related Products: Bidirectional Internal Linking */}
{relatedProducts && relatedProducts.length > 0 && (
<section className="bg-gradient-to-br from-white to-blue-50/50 border border-medical-gray-200 rounded-[2.5rem] p-8 shadow-lg space-y-6">
<div className="flex items-center justify-between">
<div>
<span className="text-xs font-black text-canina-blue uppercase tracking-wider">
مکملهای فرموله شده با این ماده
</span>
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 mt-1">
محصولات کنینا حاوی {term.term}
</h2>
</div>
<Link
href={`/shop?search=${encodeURIComponent(term.term)}`}
className="text-xs font-bold text-canina-blue hover:underline flex items-center gap-1"
>
مشاهده همه در فروشگاه
<ArrowLeft className="w-3.5 h-3.5" />
</Link>
</div>
<div className="grid sm:grid-cols-2 gap-4">
{relatedProducts.map((p: any) => (
<Link
key={p.id}
href={`/shop/${p.slug || p.artNo}`}
className="bg-white border border-medical-gray-200 rounded-2xl p-4 flex items-center gap-4 hover:shadow-md hover:border-canina-blue/40 transition-all group"
>
<div className="w-16 h-16 relative bg-medical-gray-50 rounded-xl overflow-hidden shrink-0 border border-medical-gray-100">
<SafeImage
src={p.imageUrl || '/assets/images/placeholder-product.png'}
alt={p.nameFa || p.name}
width={64}
height={64}
className="w-full h-full object-contain p-1 group-hover:scale-105 transition-transform"
/>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors truncate">
{p.nameFa || p.name}
</h3>
<p className="text-xs text-medical-gray-500 truncate mt-1">
{p.scientificTagline || p.categorySlug || 'مکمل دارویی استاندارد آلمان'}
</p>
</div>
</Link>
))}
</div>
</section>
)}
</div>
</div>
</>
);
}