336 lines
12 KiB
TypeScript
336 lines
12 KiB
TypeScript
import ProductPage from "../../../components/ProductPage";
|
||
import { productService } from "../../../lib/services/productService";
|
||
import type { Metadata } from 'next';
|
||
import { Suspense } from 'react';
|
||
import { getSeoConfig, formatPageTitle } from "../../../lib/seo";
|
||
|
||
export const revalidate = 3600;
|
||
|
||
export async function generateMetadata(
|
||
{ params }: { params: Promise<{ slug: string }> }
|
||
): Promise<Metadata> {
|
||
const resolvedParams = await params;
|
||
const product = await productService.getProductBySlug(resolvedParams.slug);
|
||
const config = await getSeoConfig();
|
||
|
||
if (!product) {
|
||
return {
|
||
title: formatPageTitle('مکمل درمانی یافت نشد', config.brandNameFa, config.titleSeparator, config.titlePosition),
|
||
};
|
||
}
|
||
|
||
const nameFa = product.nameFa || product.name;
|
||
const nameEn = product.nameEn ? ` (${product.nameEn})` : '';
|
||
const rawTitle = `${nameFa}${nameEn}`;
|
||
const title = product.metaTitle || formatPageTitle(rawTitle, config.brandNameFa, config.titleSeparator, config.titlePosition, false);
|
||
const description = product.metaDescription || (product.shortDescription || product.description || '').substring(0, 155).trim() || config.defaultMetaDescription;
|
||
|
||
const rawKeywords = product.keywords;
|
||
const keywords = typeof rawKeywords === 'string'
|
||
? rawKeywords.split(',').map(k => k.trim())
|
||
: (product.main_ingredients?.length ? product.main_ingredients : config.keywords);
|
||
|
||
const baseCanonical = config.canonicalBaseUrl.replace(/\/$/, '');
|
||
const canonicalUrl = product.canonicalUrl?.startsWith('http')
|
||
? product.canonicalUrl
|
||
: `${baseCanonical}${product.canonicalUrl?.startsWith('/') ? product.canonicalUrl : `/shop/${resolvedParams.slug}`}`;
|
||
|
||
const primaryImage = product.ogImage || product.image || config.ogImageUrl;
|
||
const imageAlt = product.featuredImageAlt || nameFa;
|
||
|
||
const galleryImages = [
|
||
{
|
||
url: primaryImage,
|
||
width: 1200,
|
||
height: 630,
|
||
alt: imageAlt,
|
||
},
|
||
...(product.images || [])
|
||
.filter(img => img && img !== primaryImage)
|
||
.map(img => ({
|
||
url: img,
|
||
width: 800,
|
||
height: 800,
|
||
alt: `${nameFa} - گالری تصویر`,
|
||
}))
|
||
];
|
||
|
||
return {
|
||
title,
|
||
description,
|
||
keywords,
|
||
robots: {
|
||
index: !product.noIndex,
|
||
follow: !product.noFollow,
|
||
googleBot: {
|
||
index: !product.noIndex,
|
||
follow: !product.noFollow,
|
||
'max-video-preview': -1,
|
||
'max-image-preview': 'large',
|
||
'max-snippet': -1,
|
||
},
|
||
},
|
||
openGraph: {
|
||
title,
|
||
description,
|
||
images: galleryImages,
|
||
url: canonicalUrl,
|
||
siteName: config.brandNameFa,
|
||
type: "website",
|
||
},
|
||
twitter: {
|
||
card: "summary_large_image",
|
||
title,
|
||
description,
|
||
images: [primaryImage],
|
||
},
|
||
alternates: {
|
||
canonical: canonicalUrl,
|
||
},
|
||
other: {
|
||
product_id: String(product.artNo || product.id || resolvedParams.slug),
|
||
product_name: title,
|
||
product_price: String(product.priceValue || 0),
|
||
product_old_price: String(product.priceValue || 0),
|
||
availability: (product.stockStatus === 'DISCONTINUED' || String(product.stockStatus || '').toLowerCase().includes('out')) ? 'outofstock' : 'instock',
|
||
guarantee: 'ضمانت اصالت ۱۰۰٪ کمپانی Canina آلمان و اصالت کالا',
|
||
'og:image': primaryImage.startsWith('http') ? primaryImage : `${baseCanonical}${primaryImage.startsWith('/') ? primaryImage : `/${primaryImage}`}`,
|
||
}
|
||
};
|
||
}
|
||
|
||
export default async function ShopProductPage({ params }: { params: Promise<{ slug: string }> }) {
|
||
const resolvedParams = await params;
|
||
const product = await productService.getProductBySlug(resolvedParams.slug);
|
||
|
||
if (!product) {
|
||
return <ProductPage productSlug={resolvedParams.slug} />;
|
||
}
|
||
|
||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canina.ir';
|
||
const productUrl = `${siteUrl}/shop/${resolvedParams.slug}`;
|
||
const priceInRial = product.priceValue * 10;
|
||
|
||
// Map stock status to Schema.org availability
|
||
let availabilityUrl = 'https://schema.org/InStock';
|
||
if (product.stockStatus === 'OUT_OF_STOCK') {
|
||
availabilityUrl = 'https://schema.org/OutOfStock';
|
||
} else if (product.stockStatus === 'PRE_ORDER') {
|
||
availabilityUrl = 'https://schema.org/PreOrder';
|
||
} else if (product.stockStatus === 'DISCONTINUED') {
|
||
availabilityUrl = 'https://schema.org/Discontinued';
|
||
}
|
||
|
||
// Collect unique images
|
||
const allImages = Array.from(new Set([
|
||
product.image,
|
||
product.ogImage,
|
||
...(product.images || [])
|
||
])).filter(Boolean) as string[];
|
||
|
||
// 1 year valid price offer based on product timestamp
|
||
const baseTime = product.updatedAt ? new Date(product.updatedAt).getTime() : 1771939200000;
|
||
const priceValidUntil = new Date(baseTime + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||
|
||
// Process approved reviews and ratings
|
||
const approvedReviews = (product.reviews || []).filter(r => r.status === 'APPROVED' || !r.status);
|
||
const totalReviewsCount = approvedReviews.length;
|
||
const averageRating = totalReviewsCount > 0
|
||
? Number((approvedReviews.reduce((sum, r) => sum + (Number(r.rating) || 5), 0) / totalReviewsCount).toFixed(1))
|
||
: 5;
|
||
|
||
const productJsonLd: Record<string, unknown> = {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'Product',
|
||
name: product.nameEn ? `${product.nameFa || product.name} (${product.nameEn})` : (product.nameFa || product.name),
|
||
image: allImages.length > 0 ? allImages : [product.image],
|
||
description: product.metaDescription || product.shortDescription || product.description,
|
||
sku: product.artNo,
|
||
mpn: product.artNo,
|
||
...(product.barcode ? { gtin: product.barcode, gtin13: product.barcode } : {}),
|
||
brand: {
|
||
'@type': 'Brand',
|
||
name: 'Canina pharma GmbH',
|
||
url: 'https://canina.de'
|
||
},
|
||
category: product.category,
|
||
offers: {
|
||
'@type': 'Offer',
|
||
url: productUrl,
|
||
priceCurrency: 'IRR',
|
||
price: priceInRial,
|
||
priceValidUntil,
|
||
availability: availabilityUrl,
|
||
itemCondition: 'https://schema.org/NewCondition',
|
||
seller: {
|
||
'@type': 'Organization',
|
||
name: 'کنینا ایران'
|
||
}
|
||
}
|
||
};
|
||
|
||
// Add aggregateRating & reviews only when reviews exist
|
||
if (totalReviewsCount > 0) {
|
||
productJsonLd.aggregateRating = {
|
||
'@type': 'AggregateRating',
|
||
ratingValue: averageRating,
|
||
reviewCount: totalReviewsCount,
|
||
bestRating: 5,
|
||
worstRating: 1
|
||
};
|
||
|
||
productJsonLd.review = approvedReviews.slice(0, 10).map(rev => ({
|
||
'@type': 'Review',
|
||
author: {
|
||
'@type': 'Person',
|
||
name: rev.userName || 'کاربر کنینا'
|
||
},
|
||
datePublished: rev.createdAt ? new Date(rev.createdAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
||
reviewRating: {
|
||
'@type': 'Rating',
|
||
ratingValue: rev.rating || 5,
|
||
bestRating: 5,
|
||
worstRating: 1
|
||
},
|
||
reviewBody: rev.comment
|
||
}));
|
||
}
|
||
|
||
// Multi-level BreadcrumbList Schema
|
||
const breadcrumbElements = [
|
||
{
|
||
'@type': 'ListItem',
|
||
position: 1,
|
||
name: 'صفحه اصلی',
|
||
item: siteUrl
|
||
},
|
||
{
|
||
'@type': 'ListItem',
|
||
position: 2,
|
||
name: 'فروشگاه مکملها',
|
||
item: `${siteUrl}/shop`
|
||
}
|
||
];
|
||
|
||
if (product.category) {
|
||
breadcrumbElements.push({
|
||
'@type': 'ListItem',
|
||
position: 3,
|
||
name: product.category,
|
||
item: `${siteUrl}/shop?category=${product.categorySlug || 'all'}`
|
||
});
|
||
breadcrumbElements.push({
|
||
'@type': 'ListItem',
|
||
position: 4,
|
||
name: product.nameFa || product.name,
|
||
item: productUrl
|
||
});
|
||
} else {
|
||
breadcrumbElements.push({
|
||
'@type': 'ListItem',
|
||
position: 3,
|
||
name: product.nameFa || product.name,
|
||
item: productUrl
|
||
});
|
||
}
|
||
|
||
const breadcrumbJsonLd = {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'BreadcrumbList',
|
||
itemListElement: breadcrumbElements
|
||
};
|
||
|
||
const graphEntities: Record<string, unknown>[] = [productJsonLd, breadcrumbJsonLd];
|
||
|
||
// 1. FAQPage Schema if faqs exist or derived
|
||
const productFaqs = (product.faqs && product.faqs.length > 0) ? product.faqs : [
|
||
{
|
||
question: `آیا لیسیدن ${product.nameFa || product.name} توسط حیوان خطرناک است؟`,
|
||
answer: `خیر، ترکیبات این محصول با استانداردهای دارویی آلمان فرموله شده و فاقد مواد نگهدارنده شیمیایی مضر است. لیسیدن محدود آن باعث مسمومیت نمیشود؛ اما جهت اثربخشی و جذب حداکثری، پیشنهاد میشود ۱۰ تا ۱۵ دقیقه حواس حیوان پرت شود یا از پاپوش/محافظ استفاده گردد.`
|
||
},
|
||
{
|
||
question: `مدت زمان ماندگاری و شرایط نگهداری ${product.nameFa || product.name} چگونه است؟`,
|
||
answer: `این محصول در جای خشک، خنک (دمای زیر ۲۵ درجه سانتیگراد) و دور از تابش مستقیم نور خورشید نگهداری شود. مدت زمان مجاز مصرف پس از باز شدن درب (PAO) ۱۲ ماه میباشد.`
|
||
},
|
||
{
|
||
question: `آیا برای تولهها و سگهای باردار یا شیرده قابل استفاده است؟`,
|
||
answer: `بله، با توجه به فرمولاسیون طبیعی و عدم وجود هورمون یا مواد شیمیایی سنگین، استفاده از این محصول برای تمامی ردههای سنی (تولهها، سگهای بالغ و مسن) و دوران حساس بارداری و شیردهی طبق دستورالعمل ایمن است.`
|
||
}
|
||
];
|
||
|
||
if (productFaqs.length > 0) {
|
||
graphEntities.push({
|
||
'@context': 'https://schema.org',
|
||
'@type': 'FAQPage',
|
||
mainEntity: productFaqs.map(f => ({
|
||
'@type': 'Question',
|
||
name: f.question,
|
||
acceptedAnswer: {
|
||
'@type': 'Answer',
|
||
text: f.answer
|
||
}
|
||
}))
|
||
});
|
||
}
|
||
|
||
// 2. AudioObject Schema for AI Podcast
|
||
if (product.podcastUrl) {
|
||
graphEntities.push({
|
||
'@context': 'https://schema.org',
|
||
'@type': 'AudioObject',
|
||
name: `پادکست تحلیل علمی ${product.nameFa || product.name}`,
|
||
description: `بررسی جامع و علمی مشخصات، مواد مؤثره و نحوه عملکرد ${product.nameFa || product.name} توسط هوش مصنوعی و کادر تخصصی کنینا`,
|
||
contentUrl: product.podcastUrl,
|
||
encodingFormat: 'audio/mpeg',
|
||
inLanguage: 'fa-IR'
|
||
});
|
||
}
|
||
|
||
// 3. VideoObject Schema for Educational Video
|
||
if (product.videoUrl) {
|
||
graphEntities.push({
|
||
'@context': 'https://schema.org',
|
||
'@type': 'VideoObject',
|
||
name: product.videoTitle || `ویدیو راهنما و معرفی ${product.nameFa || product.name}`,
|
||
description: product.videoDescription || `ویدیو بررسی و نحوه مصرف ${product.nameFa || product.name} - Canina Germany`,
|
||
thumbnailUrl: [product.videoCover || product.image || `${siteUrl}/images/canina-default-thumb.jpg`],
|
||
uploadDate: product.updatedAt ? new Date(product.updatedAt).toISOString() : '2026-01-01T00:00:00Z',
|
||
contentUrl: product.videoUrl,
|
||
inLanguage: 'fa-IR'
|
||
});
|
||
}
|
||
|
||
const unifiedJsonLd = {
|
||
'@context': 'https://schema.org',
|
||
'@graph': graphEntities
|
||
};
|
||
|
||
const safeUnifiedJsonLd = JSON.stringify(unifiedJsonLd).replace(/</g, '\\u003c');
|
||
|
||
return (
|
||
<>
|
||
<script
|
||
type="application/ld+json"
|
||
dangerouslySetInnerHTML={{ __html: safeUnifiedJsonLd }}
|
||
/>
|
||
<Suspense fallback={
|
||
<div className="min-h-screen bg-medical-gray-50 p-6 max-w-7xl mx-auto font-vazir" dir="rtl">
|
||
<div className="animate-pulse space-y-6">
|
||
<div className="h-8 bg-medical-gray-200 rounded-2xl w-48" />
|
||
<div className="grid md:grid-cols-12 gap-8">
|
||
<div className="md:col-span-5 h-80 bg-white border border-medical-gray-200 rounded-[2.5rem]" />
|
||
<div className="md:col-span-7 space-y-4">
|
||
<div className="h-10 bg-medical-gray-200 rounded-2xl w-3/4" />
|
||
<div className="h-6 bg-medical-gray-100 rounded-xl w-1/2" />
|
||
<div className="h-24 bg-medical-gray-100 rounded-2xl w-full" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}>
|
||
<ProductPage productSlug={resolvedParams.slug} />
|
||
</Suspense>
|
||
</>
|
||
);
|
||
}
|