canina/frontend/application/app/shop/[slug]/page.tsx
parsa aghaei f81aabb479
Some checks failed
Deploy Canina / deploy (push) Successful in 2m30s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s
fix(seo): prevent duplicate brand affixes in page titles and add checkout success metadata
2026-09-05 19:43:28 +03:30

325 lines
11 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 = product.metaTitle || `${nameFa}${nameEn}`;
const title = formatPageTitle(rawTitle, config.brandNameFa, config.titleSeparator, config.titlePosition, false);
const ogTitle = `${title} ${config.titleSeparator} ${config.brandNameFa}`;
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: ogTitle,
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 only if faqs exist and are configured
if (product.showFaqs !== false && Array.isArray(product.faqs) && product.faqs.length > 0) {
const validFaqs = product.faqs.filter(f => f && f.question?.trim() && f.answer?.trim());
if (validFaqs.length > 0) {
graphEntities.push({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: validFaqs.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>
</>
);
}