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

267 lines
8.5 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,
}
};
}
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 safeProductJsonLd = JSON.stringify(productJsonLd).replace(/</g, '\\u003c');
const safeBreadcrumbJsonLd = JSON.stringify(breadcrumbJsonLd).replace(/</g, '\\u003c');
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeProductJsonLd }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeBreadcrumbJsonLd }}
/>
<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>
</>
);
}