Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Failing after 1m57s
Build & Deploy / 🧪 QA (push) Failing after 2m3s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
564 lines
23 KiB
TypeScript
564 lines
23 KiB
TypeScript
import JsonLd from '@/components/JsonLd';
|
|
import { SITE_URL } from '@/lib/schema';
|
|
import ProductSidebar from '@/components/ProductSidebar';
|
|
import ExcelDownload from '@/components/ExcelDownload';
|
|
import RelatedProducts from '@/components/RelatedProducts';
|
|
import DatasheetDownload from '@/components/DatasheetDownload';
|
|
import { Badge, Card, Container, Heading, Section } from '@/components/ui';
|
|
import { getDatasheetPath, getExcelDatasheetPath } from '@/lib/datasheets';
|
|
import { getAllProducts, getProductBySlug } from '@/lib/products';
|
|
import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
|
|
import { Metadata } from 'next';
|
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
|
import { getProductOGImageMetadata } from '@/lib/metadata';
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import { notFound, redirect } from 'next/navigation';
|
|
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
|
import PayloadRichText from '@/components/PayloadRichText';
|
|
|
|
interface ProductPageProps {
|
|
params: Promise<{
|
|
locale: string;
|
|
slug: string[];
|
|
}>;
|
|
}
|
|
|
|
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
|
|
const { locale, slug } = await params;
|
|
const productSlug = slug[slug.length - 1];
|
|
const t = await getTranslations('Products');
|
|
|
|
// Check if it's a category page
|
|
const categories = [
|
|
'low-voltage-cables',
|
|
'medium-voltage-cables',
|
|
'high-voltage-cables',
|
|
'solar-cables',
|
|
];
|
|
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
|
|
if (categories.includes(fileSlug)) {
|
|
const categoryKey = fileSlug
|
|
.replace(/-cables$/, '')
|
|
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
const categoryTitle = t.has(`categories.${categoryKey}.title`)
|
|
? t(`categories.${categoryKey}.title`)
|
|
: fileSlug;
|
|
const categoryDesc = t.has(`categories.${categoryKey}.description`)
|
|
? t(`categories.${categoryKey}.description`)
|
|
: '';
|
|
|
|
return {
|
|
title: categoryTitle,
|
|
description: categoryDesc,
|
|
alternates: {
|
|
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${await mapFileSlugToTranslated(fileSlug, locale)}`,
|
|
languages: {
|
|
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(fileSlug, 'de')}`,
|
|
en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(fileSlug, 'en')}`,
|
|
'x-default': `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(fileSlug, 'en')}`,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const fileSlugs = await Promise.all(slug.map((s) => mapSlugToFileSlug(s, locale)));
|
|
const getLocalizedPath = async (lang: string) => {
|
|
const parts = await Promise.all([
|
|
mapFileSlugToTranslated('products', lang),
|
|
...fileSlugs.map((fs) => mapFileSlugToTranslated(fs, lang)),
|
|
]);
|
|
return parts.join('/');
|
|
};
|
|
|
|
const product = await getProductBySlug(productSlug, locale);
|
|
if (!product) return {};
|
|
|
|
const currentLocalePath = await getLocalizedPath(locale);
|
|
|
|
return {
|
|
title: product.frontmatter.title,
|
|
description: product.frontmatter.description,
|
|
alternates: {
|
|
canonical: `${SITE_URL}/${locale}/${currentLocalePath}`,
|
|
languages: {
|
|
de: `${SITE_URL}/de/${await getLocalizedPath('de')}`,
|
|
en: `${SITE_URL}/en/${await getLocalizedPath('en')}`,
|
|
'x-default': `${SITE_URL}/en/${await getLocalizedPath('en')}`,
|
|
},
|
|
},
|
|
openGraph: {
|
|
title: product.frontmatter.title,
|
|
description: product.frontmatter.description,
|
|
type: 'website',
|
|
url: `${SITE_URL}/${locale}/${currentLocalePath}`,
|
|
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: product.frontmatter.title,
|
|
description: product.frontmatter.description,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function ProductPage({ params }: ProductPageProps) {
|
|
const { locale, slug } = await params;
|
|
setRequestLocale(locale);
|
|
const productSlug = slug[slug.length - 1];
|
|
const t = await getTranslations('Products');
|
|
const productsSlug = await mapFileSlugToTranslated('products', locale);
|
|
|
|
const categories = [
|
|
'low-voltage-cables',
|
|
'medium-voltage-cables',
|
|
'high-voltage-cables',
|
|
'solar-cables',
|
|
];
|
|
|
|
const fileSlugs = await Promise.all(slug.map((s) => mapSlugToFileSlug(s, locale)));
|
|
const translatedSlugsForLocale = await Promise.all(
|
|
fileSlugs.map((fs) => mapFileSlugToTranslated(fs, locale)),
|
|
);
|
|
|
|
// If the requested slugs don't exactly match the translated slugs for the current locale
|
|
// (i.e. if the user used the static language switcher but kept the original locale's slugs)
|
|
if (slug.join('/') !== translatedSlugsForLocale.join('/')) {
|
|
redirect(`/${locale}/${productsSlug}/${translatedSlugsForLocale.join('/')}`);
|
|
}
|
|
|
|
const fileSlug = fileSlugs[fileSlugs.length - 1];
|
|
|
|
if (categories.includes(fileSlug)) {
|
|
const allProducts = await getAllProducts(locale);
|
|
const categoryKey = fileSlug
|
|
.replace(/-cables$/, '')
|
|
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
const categoryTitle = t.has(`categories.${categoryKey}.title`)
|
|
? t(`categories.${categoryKey}.title`)
|
|
: fileSlug;
|
|
|
|
const filteredProducts = allProducts.filter((p) => {
|
|
const firstCat = p.frontmatter.categories[0] || '';
|
|
const normalizedCat = firstCat.toLowerCase().replace(/\s+/g, '-');
|
|
let pFileSlug = 'low-voltage-cables';
|
|
if (normalizedCat === 'hochspannungskabel' || normalizedCat === 'high-voltage-cables')
|
|
pFileSlug = 'high-voltage-cables';
|
|
else if (
|
|
normalizedCat === 'mittelspannungskabel' ||
|
|
normalizedCat === 'medium-voltage-cables'
|
|
)
|
|
pFileSlug = 'medium-voltage-cables';
|
|
else if (
|
|
normalizedCat === 'solarkabel' ||
|
|
normalizedCat === 'solar-cables' ||
|
|
normalizedCat === 'solar'
|
|
)
|
|
pFileSlug = 'solar-cables';
|
|
|
|
return pFileSlug === fileSlug;
|
|
});
|
|
|
|
const productsWithTranslatedSlugs = await Promise.all(
|
|
filteredProducts.map(async (p) => ({
|
|
...p,
|
|
translatedSlug: await mapFileSlugToTranslated(p.slug, locale),
|
|
})),
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-screen bg-white">
|
|
<section className="relative min-h-[50vh] flex items-center pt-32 pb-20 overflow-hidden bg-primary-dark">
|
|
<Container className="relative z-10">
|
|
<div className="max-w-4xl animate-slide-up">
|
|
<nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest">
|
|
<Link
|
|
href={`/${locale}/${productsSlug}`}
|
|
className="hover:text-accent transition-colors"
|
|
>
|
|
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
|
</Link>
|
|
<span className="mx-3 opacity-30">/</span>
|
|
<span className="text-white/90">{categoryTitle}</span>
|
|
</nav>
|
|
<Heading level={1} className="text-white mb-8">
|
|
{categoryTitle}
|
|
</Heading>
|
|
<div className="h-1.5 w-24 bg-accent rounded-full" />
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
|
|
<Section className="bg-neutral-light relative">
|
|
<Container>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
{productsWithTranslatedSlugs.map((product) => (
|
|
<Link
|
|
key={product.slug}
|
|
href={`/${locale}/${productsSlug}/${productSlug}/${product.translatedSlug}`}
|
|
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
|
>
|
|
<Card tag="article" className="premium-card-reset">
|
|
<div className="aspect-[4/3] relative bg-neutral-light/30 p-12 overflow-hidden">
|
|
{product.frontmatter.images?.[0] && (
|
|
<>
|
|
<Image
|
|
src={product.frontmatter.images[0]}
|
|
alt={product.frontmatter.title}
|
|
fill
|
|
className="object-contain p-8 transition-transform duration-700 group-hover:scale-110 z-10"
|
|
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
/>
|
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-2/3 h-4 bg-black/5 blur-xl rounded-full" />
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="p-8 md:p-10">
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
{product.frontmatter.categories.map((cat, i) => (
|
|
<span
|
|
key={i}
|
|
className="text-[10px] font-bold uppercase tracking-widest text-primary/40"
|
|
>
|
|
{cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<h2 className="text-2xl md:text-3xl font-bold text-text-primary group-hover:text-primary transition-colors mb-4 leading-tight">
|
|
{product.frontmatter.title}
|
|
</h2>
|
|
<p className="text-text-secondary line-clamp-2 text-base leading-relaxed mb-8">
|
|
{product.frontmatter.description}
|
|
</p>
|
|
<div className="flex items-center text-primary font-bold group-hover:text-accent-dark transition-colors">
|
|
<span className="border-b-2 border-primary/10 group-hover:border-accent-dark transition-colors pb-1">
|
|
{t('details')}
|
|
</span>
|
|
<svg
|
|
className="w-5 h-5 ml-3 transition-transform group-hover:translate-x-1"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M17 8l4 4m0 0l-4 4m4-4H3"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</Container>
|
|
</Section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const product = await getProductBySlug(productSlug, locale);
|
|
|
|
if (!product) {
|
|
notFound();
|
|
}
|
|
|
|
// Extract technical data natively from the Lexical AST for Schema.org
|
|
let technicalItems = [];
|
|
if (product.content?.root?.children) {
|
|
const productTabsBlock = product.content.root.children.find(
|
|
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs',
|
|
);
|
|
if (productTabsBlock && productTabsBlock.fields?.technicalItems) {
|
|
technicalItems = productTabsBlock.fields.technicalItems;
|
|
}
|
|
}
|
|
|
|
const datasheetPath = getDatasheetPath(productSlug, locale);
|
|
const excelPath = getExcelDatasheetPath(productSlug, locale);
|
|
const isFallback = (product.frontmatter as any).isFallback;
|
|
const categorySlug = slug[0];
|
|
const categoryFileSlug = await mapSlugToFileSlug(categorySlug, locale);
|
|
const categoryKey = categoryFileSlug
|
|
.replace(/-cables$/, '')
|
|
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
const categoryTitle = t.has(`categories.${categoryKey}.title`)
|
|
? t(`categories.${categoryKey}.title`)
|
|
: categoryFileSlug;
|
|
|
|
// Split content into Description and Technical Data
|
|
const rootChildren = product.content?.root?.children || [];
|
|
const technicalBlocks = rootChildren.filter(
|
|
(node: any) =>
|
|
node.type === 'block' &&
|
|
(node.fields?.blockType === 'productTabs' ||
|
|
node.fields?.blockType === 'productTechnicalData'),
|
|
);
|
|
let descriptionChildren = rootChildren.filter(
|
|
(node: any) =>
|
|
!(
|
|
node.type === 'block' &&
|
|
(node.fields?.blockType === 'productTabs' ||
|
|
node.fields?.blockType === 'productTechnicalData')
|
|
),
|
|
);
|
|
|
|
// If no standalone description nodes, extract from the productTabs block's embedded content
|
|
if (descriptionChildren.length === 0) {
|
|
const tabsBlock = rootChildren.find(
|
|
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs',
|
|
);
|
|
if (tabsBlock?.fields?.content?.root?.children) {
|
|
descriptionChildren = tabsBlock.fields.content.root.children.filter((node: any) => {
|
|
// Filter out MDX parsing artifacts like `}>`
|
|
if (node.type === 'paragraph' && node.children?.length === 1) {
|
|
const text = node.children[0]?.text?.trim();
|
|
return text !== '}>' && text !== '{' && text !== '}';
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log(`[DEBUG PAGE] Slug: ${productSlug}, children count: ${descriptionChildren.length}`);
|
|
|
|
const descriptionContent = {
|
|
root: {
|
|
...product.content.root,
|
|
children: descriptionChildren,
|
|
},
|
|
};
|
|
|
|
const technicalContent = {
|
|
root: {
|
|
...product.content.root,
|
|
children: technicalBlocks,
|
|
},
|
|
};
|
|
|
|
const sidebar = (
|
|
<ProductSidebar
|
|
productName={product.frontmatter.title}
|
|
productImage={product.frontmatter.images?.[0]}
|
|
datasheetPath={datasheetPath}
|
|
excelPath={excelPath}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-screen bg-white relative">
|
|
{/* Product Hero */}
|
|
<ProductEngagementTracker
|
|
productName={product.frontmatter.title}
|
|
productSlug={productSlug}
|
|
categories={product.frontmatter.categories}
|
|
sku={product.frontmatter.sku}
|
|
/>
|
|
<section className="relative pt-28 md:pt-40 pb-12 md:pb-24 overflow-hidden bg-primary-dark">
|
|
{/* Background Decorative Elements */}
|
|
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/5 to-transparent pointer-events-none" />
|
|
<div className="absolute -top-24 -right-24 w-96 h-96 bg-accent/10 rounded-full blur-3xl pointer-events-none" />
|
|
|
|
<Container className="relative z-10">
|
|
<div className="max-w-4xl animate-slide-up">
|
|
<nav className="flex flex-wrap items-center gap-y-1 mb-6 md:mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
|
<Link
|
|
href={`/${locale}/${productsSlug}`}
|
|
className="hover:text-accent transition-colors shrink-0"
|
|
>
|
|
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
|
</Link>
|
|
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
|
<Link
|
|
href={`/${locale}/${productsSlug}/${categorySlug}`}
|
|
className="hover:text-accent transition-colors shrink-0 max-w-[140px] truncate"
|
|
>
|
|
{categoryTitle}
|
|
</Link>
|
|
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
|
<span className="text-white/90 truncate max-w-[140px] md:max-w-none">
|
|
{product.frontmatter.title}
|
|
</span>
|
|
</nav>
|
|
|
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
|
|
<div className="flex-1">
|
|
{isFallback && (
|
|
<div className="mb-8 inline-flex items-center px-4 py-2 rounded-full bg-accent/10 border border-accent/20 text-accent text-[10px] font-black uppercase tracking-[0.2em] backdrop-blur-md">
|
|
<span className="w-2 h-2 rounded-full bg-accent mr-3 animate-pulse" />
|
|
{t('englishVersion')}
|
|
</div>
|
|
)}
|
|
<div className="flex flex-wrap gap-2 mb-4 md:mb-8">
|
|
{product.frontmatter.categories.map((cat, idx) => (
|
|
<Badge
|
|
key={idx}
|
|
variant="accent"
|
|
className="bg-white/5 text-white/80 border-white/10 backdrop-blur-md px-5 py-2 text-[10px] font-black tracking-[0.15em]"
|
|
>
|
|
{cat}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
<Heading level={1} className="text-white mb-4 md:mb-8 uppercase">
|
|
{product.frontmatter.title}
|
|
</Heading>
|
|
<p className="text-base md:text-xl lg:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
|
|
{product.frontmatter.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
|
|
<Section className="bg-white relative">
|
|
<Container className="relative">
|
|
{/* Large Product Image Section */}
|
|
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
|
<div
|
|
className="relative md:-mt-32 mb-8 md:mb-32 animate-slide-up"
|
|
style={{ animationDelay: '200ms' }}
|
|
>
|
|
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[24px] md:rounded-[48px] border border-neutral-dark/5 overflow-hidden p-6 md:p-20 lg:p-24">
|
|
<div className="relative w-full aspect-[4/3] md:aspect-[21/9]">
|
|
<Image
|
|
src={product.frontmatter.images[0]}
|
|
alt={product.frontmatter.title}
|
|
fill
|
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
className="object-contain transition-transform duration-1000 hover:scale-105"
|
|
priority
|
|
/>
|
|
{/* Subtle reflection/shadow effect */}
|
|
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 h-12 bg-black/5 blur-3xl rounded-[100%]" />
|
|
</div>
|
|
|
|
{product.frontmatter.images.length > 1 && (
|
|
<div className="flex justify-center gap-8 mt-20">
|
|
{product.frontmatter.images.slice(0, 5).map((img, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="relative w-24 h-24 md:w-32 md:h-32 border-2 border-neutral-dark/5 rounded-3xl overflow-hidden bg-neutral-light/30 hover:border-accent transition-all duration-500 cursor-pointer group p-4"
|
|
>
|
|
<Image
|
|
src={img}
|
|
alt=""
|
|
fill
|
|
sizes="128px"
|
|
className="object-contain p-4 transition-transform duration-700 group-hover:scale-110"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-20">
|
|
{/* Description Area Next to Sidebar */}
|
|
<div className="lg:col-span-8">
|
|
<div className="max-w-none prose prose-primary prose-base md:prose-lg xl:prose-xl mb-8 md:mb-16 pb-8 md:pb-16 border-b border-neutral-dark/5">
|
|
{descriptionChildren.length > 0 ? (
|
|
<PayloadRichText data={descriptionContent} />
|
|
) : product.frontmatter.description ? (
|
|
<p className="text-lg md:text-xl text-text-secondary leading-relaxed">
|
|
{product.frontmatter.description}
|
|
</p>
|
|
) : null}
|
|
|
|
{product.application?.root?.children?.length > 0 && (
|
|
<div className="mt-12">
|
|
<PayloadRichText data={product.application} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar Column */}
|
|
<div className="lg:col-span-4 lg:sticky lg:top-32 h-fit">{sidebar}</div>
|
|
</div>
|
|
|
|
{/* Full-width Technical Data Below */}
|
|
<div className="mt-8 md:mt-16 pt-8 md:pt-16 border-t-0">
|
|
<div className="max-w-none prose prose-primary prose-lg md:prose-xl">
|
|
<PayloadRichText data={technicalContent} />
|
|
</div>
|
|
|
|
{/* Datasheet Download Section */}
|
|
{categoryFileSlug === 'medium-voltage-cables' && datasheetPath && (
|
|
<div className="mt-16 pt-16 border-t-2 border-neutral-dark/5">
|
|
<div className="mb-8">
|
|
<h2 className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-4">
|
|
{t('downloadDatasheet')}
|
|
</h2>
|
|
<div className="h-1.5 w-24 bg-accent rounded-full" />
|
|
</div>
|
|
<div className="flex flex-row flex-wrap items-center gap-4 max-w-2xl">
|
|
<DatasheetDownload
|
|
datasheetPath={datasheetPath}
|
|
className="mt-0 w-full sm:w-auto"
|
|
/>
|
|
{excelPath && (
|
|
<ExcelDownload excelPath={excelPath} className="mt-0 w-full sm:w-auto" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Structured Data (Hidden) */}
|
|
<JsonLd
|
|
id={`jsonld-${product.slug}`}
|
|
data={
|
|
{
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Product',
|
|
name: product.frontmatter.title,
|
|
description: product.frontmatter.description,
|
|
sku: product.frontmatter.sku || product.slug.toUpperCase(),
|
|
image: product.frontmatter.images?.[0]
|
|
? `${SITE_URL}${product.frontmatter.images[0]}`
|
|
: undefined,
|
|
brand: {
|
|
'@type': 'Brand',
|
|
name: 'KLZ Cables',
|
|
},
|
|
offers: {
|
|
'@type': 'Offer',
|
|
availability: 'https://schema.org/InStock',
|
|
priceCurrency: 'EUR',
|
|
url: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
|
|
itemCondition: 'https://schema.org/NewCondition',
|
|
},
|
|
additionalProperty: technicalItems.map((item: any) => ({
|
|
'@type': 'PropertyValue',
|
|
name: item.label,
|
|
value: item.value,
|
|
})),
|
|
category: product.frontmatter.categories.join(', '),
|
|
mainEntityOfPage: {
|
|
'@type': 'WebPage',
|
|
'@id': `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
|
|
},
|
|
} as any
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Related Products Section */}
|
|
<div className="mt-10 md:mt-16 pt-10 md:pt-16 border-t border-neutral-dark/5">
|
|
<RelatedProducts
|
|
currentSlug={productSlug}
|
|
categories={product.frontmatter.categories}
|
|
locale={locale}
|
|
/>
|
|
</div>
|
|
</Container>
|
|
</Section>
|
|
</div>
|
|
);
|
|
}
|