refactor: Replace hardcoded domain with SITE_URL constant across metadata and schema definitions for improved configurability.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m30s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m14s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 42s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 5m0s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m30s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m14s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 42s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 5m0s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
This commit is contained in:
@@ -31,12 +31,23 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
||||
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 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`) : '';
|
||||
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,
|
||||
@@ -44,15 +55,15 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
||||
alternates: {
|
||||
canonical: `/${locale}/products/${productSlug}`,
|
||||
languages: {
|
||||
'de': `/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||
'en': `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||
de: `/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||
en: `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||
'x-default': `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: `${categoryTitle} | KLZ Cables`,
|
||||
description: categoryDesc,
|
||||
url: `https://klz-cables.com/${locale}/products/${productSlug}`,
|
||||
url: `${SITE_URL}/${locale}/products/${productSlug}`,
|
||||
images: getProductOGImageMetadata(fileSlug, categoryTitle, locale),
|
||||
},
|
||||
twitter: {
|
||||
@@ -72,8 +83,8 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
||||
alternates: {
|
||||
canonical: `/${locale}/products/${slug.join('/')}`,
|
||||
languages: {
|
||||
'de': `/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||
'en': `/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||
de: `/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||
en: `/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||
'x-default': `/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||
},
|
||||
},
|
||||
@@ -81,7 +92,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
||||
title: `${product.frontmatter.title} | KLZ Cables`,
|
||||
description: product.frontmatter.description,
|
||||
type: 'website',
|
||||
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
|
||||
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
||||
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
|
||||
},
|
||||
twitter: {
|
||||
@@ -95,20 +106,36 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
||||
const components = {
|
||||
ProductTechnicalData,
|
||||
ProductTabs,
|
||||
p: (props: any) => <p {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed mb-8 font-medium" />,
|
||||
p: (props: any) => (
|
||||
<p
|
||||
{...props}
|
||||
className="text-lg md:text-xl text-text-secondary leading-relaxed mb-8 font-medium"
|
||||
/>
|
||||
),
|
||||
h2: (props: any) => (
|
||||
<div className="relative mb-16">
|
||||
<h2 {...props} className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-6" />
|
||||
<h2
|
||||
{...props}
|
||||
className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-6"
|
||||
/>
|
||||
<div className="w-20 h-1.5 bg-accent rounded-full" />
|
||||
</div>
|
||||
),
|
||||
h3: (props: any) => <h3 {...props} className="text-2xl md:text-3xl font-black text-primary mb-10 tracking-tight uppercase" />,
|
||||
h3: (props: any) => (
|
||||
<h3
|
||||
{...props}
|
||||
className="text-2xl md:text-3xl font-black text-primary mb-10 tracking-tight uppercase"
|
||||
/>
|
||||
),
|
||||
ul: (props: any) => <ul {...props} className="list-none pl-0 mb-10" />,
|
||||
section: (props: any) => <div {...props} className="block" />,
|
||||
li: (props: any) => (
|
||||
<li className="flex items-start gap-4 group mb-4 last:mb-0">
|
||||
<div className="mt-2.5 w-2 h-2 rounded-full bg-accent flex-shrink-0 group-hover:scale-125 transition-transform" />
|
||||
<span {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed font-medium" />
|
||||
<span
|
||||
{...props}
|
||||
className="text-lg md:text-xl text-text-secondary leading-relaxed font-medium"
|
||||
/>
|
||||
</li>
|
||||
),
|
||||
strong: (props: any) => <strong {...props} className="font-black text-primary" />,
|
||||
@@ -117,13 +144,26 @@ const components = {
|
||||
<table {...props} className="min-w-full divide-y divide-neutral-dark/10" />
|
||||
</div>
|
||||
),
|
||||
th: (props: any) => <th {...props} className="px-8 py-6 bg-neutral-light/50 text-left text-[10px] font-black uppercase tracking-[0.25em] text-primary/60" />,
|
||||
td: (props: any) => <td {...props} className="px-8 py-6 text-text-secondary border-t border-neutral-dark/5 text-lg md:text-xl font-medium" />,
|
||||
th: (props: any) => (
|
||||
<th
|
||||
{...props}
|
||||
className="px-8 py-6 bg-neutral-light/50 text-left text-[10px] font-black uppercase tracking-[0.25em] text-primary/60"
|
||||
/>
|
||||
),
|
||||
td: (props: any) => (
|
||||
<td
|
||||
{...props}
|
||||
className="px-8 py-6 text-text-secondary border-t border-neutral-dark/5 text-lg md:text-xl font-medium"
|
||||
/>
|
||||
),
|
||||
hr: () => <hr className="my-24 border-t-2 border-neutral-dark/5" />,
|
||||
blockquote: (props: any) => (
|
||||
<div className="my-20 p-10 md:p-16 bg-primary-dark rounded-[40px] relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl group-hover:bg-accent/20 transition-colors duration-700" />
|
||||
<div className="relative z-10 italic text-2xl md:text-3xl text-white/90 leading-relaxed font-black tracking-tight" {...props} />
|
||||
<div
|
||||
className="relative z-10 italic text-2xl md:text-3xl text-white/90 leading-relaxed font-black tracking-tight"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -134,28 +174,36 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
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 categories = [
|
||||
'low-voltage-cables',
|
||||
'medium-voltage-cables',
|
||||
'high-voltage-cables',
|
||||
'solar-cables',
|
||||
];
|
||||
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
|
||||
|
||||
|
||||
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 categoryKey = fileSlug
|
||||
.replace(/-cables$/, '')
|
||||
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
const categoryTitle = t.has(`categories.${categoryKey}.title`)
|
||||
? t(`categories.${categoryKey}.title`)
|
||||
: fileSlug;
|
||||
|
||||
// Filter products for this category
|
||||
const filteredProducts = allProducts.filter(p =>
|
||||
p.frontmatter.categories.some(cat =>
|
||||
cat.toLowerCase().replace(/\s+/g, '-') === fileSlug ||
|
||||
cat === categoryTitle
|
||||
)
|
||||
const filteredProducts = allProducts.filter((p) =>
|
||||
p.frontmatter.categories.some(
|
||||
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === fileSlug || cat === categoryTitle,
|
||||
),
|
||||
);
|
||||
|
||||
// Get translated product slugs
|
||||
const productsWithTranslatedSlugs = await Promise.all(
|
||||
filteredProducts.map(async (p) => ({
|
||||
...p,
|
||||
translatedSlug: await mapFileSlugToTranslated(p.slug, locale)
|
||||
}))
|
||||
translatedSlug: await mapFileSlugToTranslated(p.slug, locale),
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -164,7 +212,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<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}/products`} className="hover:text-accent transition-colors">{t('title')}</Link>
|
||||
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">
|
||||
{t('title')}
|
||||
</Link>
|
||||
<span className="mx-3 opacity-30">/</span>
|
||||
<span className="text-white/90">{categoryTitle}</span>
|
||||
</nav>
|
||||
@@ -202,7 +252,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<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">
|
||||
<span
|
||||
key={i}
|
||||
className="text-[10px] font-bold uppercase tracking-widest text-primary/40"
|
||||
>
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
@@ -217,8 +270,18 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<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
|
||||
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>
|
||||
@@ -238,7 +301,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
}
|
||||
|
||||
// Extract technical data for schema
|
||||
const technicalDataMatch = product.content.match(/technicalData=\{<ProductTechnicalData data=\{(.*?)\}\s*\/>\}/s);
|
||||
const technicalDataMatch = product.content.match(
|
||||
/technicalData=\{<ProductTechnicalData data=\{(.*?)\}\s*\/>\}/s,
|
||||
);
|
||||
let technicalItems = [];
|
||||
if (technicalDataMatch) {
|
||||
try {
|
||||
@@ -253,11 +318,15 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
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;
|
||||
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;
|
||||
|
||||
const sidebar = (
|
||||
<ProductSidebar
|
||||
<ProductSidebar
|
||||
productName={product.frontmatter.title}
|
||||
productImage={product.frontmatter.images?.[0]}
|
||||
datasheetPath={datasheetPath}
|
||||
@@ -287,17 +356,24 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{/* 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 items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
||||
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">{t('title')}</Link>
|
||||
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">
|
||||
{t('title')}
|
||||
</Link>
|
||||
<span className="mx-4 opacity-20">/</span>
|
||||
<Link href={`/${locale}/products/${categorySlug}`} className="hover:text-accent transition-colors">{categoryTitle}</Link>
|
||||
<Link
|
||||
href={`/${locale}/products/${categorySlug}`}
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{categoryTitle}
|
||||
</Link>
|
||||
<span className="mx-4 opacity-20">/</span>
|
||||
<span className="text-white/90">{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 && (
|
||||
@@ -308,7 +384,11 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
)}
|
||||
<div className="flex flex-wrap gap-3 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]">
|
||||
<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>
|
||||
))}
|
||||
@@ -329,11 +409,14 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<Container className="relative">
|
||||
{/* Large Product Image Section */}
|
||||
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
||||
<div className="relative -mt-32 mb-32 animate-slide-up" style={{ animationDelay: '200ms' }}>
|
||||
<div
|
||||
className="relative -mt-32 mb-32 animate-slide-up"
|
||||
style={{ animationDelay: '200ms' }}
|
||||
>
|
||||
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[48px] border border-neutral-dark/5 overflow-hidden p-12 md:p-20 lg:p-24">
|
||||
<div className="relative w-full aspect-[21/9]">
|
||||
<Image
|
||||
src={product.frontmatter.images[0]}
|
||||
<Image
|
||||
src={product.frontmatter.images[0]}
|
||||
alt={product.frontmatter.title}
|
||||
fill
|
||||
className="object-contain transition-transform duration-1000 hover:scale-105"
|
||||
@@ -342,12 +425,20 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{/* 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 className="object-contain p-4 transition-transform duration-700 group-hover:scale-110" />
|
||||
<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
|
||||
className="object-contain p-4 transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -360,7 +451,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<div className="w-full">
|
||||
{/* Main Content Area */}
|
||||
<div className="max-w-none">
|
||||
<MDXRemote source={processedContent} components={productComponents} />
|
||||
<MDXRemote source={processedContent} components={productComponents} />
|
||||
</div>
|
||||
|
||||
{/* Datasheet Download Section - Only for Medium Voltage for now */}
|
||||
@@ -379,45 +470,49 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{/* Structured Data */}
|
||||
<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] ? `https://klz-cables.com${product.frontmatter.images[0]}` : undefined,
|
||||
brand: {
|
||||
'@type': 'Brand',
|
||||
name: 'KLZ Cables',
|
||||
},
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
availability: 'https://schema.org/InStock',
|
||||
priceCurrency: 'EUR',
|
||||
url: `https://klz-cables.com/${locale}/products/${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': `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
|
||||
},
|
||||
} as any}
|
||||
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}/products/${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}/products/${slug.join('/')}`,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Related Products Section */}
|
||||
<div className="mt-16 pt-16 border-t border-neutral-dark/5">
|
||||
<RelatedProducts
|
||||
currentSlug={productSlug}
|
||||
categories={product.frontmatter.categories}
|
||||
locale={locale}
|
||||
<RelatedProducts
|
||||
currentSlug={productSlug}
|
||||
categories={product.frontmatter.categories}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||
import { getOGImageMetadata } from '@/lib/metadata';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
|
||||
interface ProductsPageProps {
|
||||
params: {
|
||||
@@ -14,7 +15,9 @@ interface ProductsPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params: { locale } }: ProductsPageProps): Promise<Metadata> {
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: ProductsPageProps): Promise<Metadata> {
|
||||
const t = await getTranslations({ locale, namespace: 'Products' });
|
||||
const title = t('meta.title') || t('title');
|
||||
const description = t('meta.description') || t('subtitle');
|
||||
@@ -24,15 +27,15 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps
|
||||
alternates: {
|
||||
canonical: `/${locale}/products`,
|
||||
languages: {
|
||||
'de': '/de/products',
|
||||
'en': '/en/products',
|
||||
de: '/de/products',
|
||||
en: '/en/products',
|
||||
'x-default': '/en/products',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: `${title} | KLZ Cables`,
|
||||
description,
|
||||
url: `https://klz-cables.com/${locale}/products`,
|
||||
url: `${SITE_URL}/${locale}/products`,
|
||||
images: getOGImageMetadata('products', title, locale),
|
||||
},
|
||||
twitter: {
|
||||
@@ -58,29 +61,29 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
desc: t('categories.lowVoltage.description'),
|
||||
img: '/uploads/2024/11/low-voltage-category.webp',
|
||||
icon: '/uploads/2024/11/Low-Voltage.svg',
|
||||
href: `/${params.locale}/products/${lowVoltageSlug}`
|
||||
href: `/${params.locale}/products/${lowVoltageSlug}`,
|
||||
},
|
||||
{
|
||||
title: t('categories.mediumVoltage.title'),
|
||||
desc: t('categories.mediumVoltage.description'),
|
||||
img: '/uploads/2024/11/medium-voltage-category.webp',
|
||||
icon: '/uploads/2024/11/Medium-Voltage.svg',
|
||||
href: `/${params.locale}/products/${mediumVoltageSlug}`
|
||||
href: `/${params.locale}/products/${mediumVoltageSlug}`,
|
||||
},
|
||||
{
|
||||
title: t('categories.highVoltage.title'),
|
||||
desc: t('categories.highVoltage.description'),
|
||||
img: '/uploads/2024/11/high-voltage-category.webp',
|
||||
icon: '/uploads/2024/11/High-Voltage.svg',
|
||||
href: `/${params.locale}/products/${highVoltageSlug}`
|
||||
href: `/${params.locale}/products/${highVoltageSlug}`,
|
||||
},
|
||||
{
|
||||
title: t('categories.solar.title'),
|
||||
desc: t('categories.solar.description'),
|
||||
img: '/uploads/2024/11/solar-category.webp',
|
||||
icon: '/uploads/2024/11/Solar.svg',
|
||||
href: `/${params.locale}/products/${solarSlug}`
|
||||
}
|
||||
href: `/${params.locale}/products/${solarSlug}`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -89,7 +92,10 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
<section className="relative min-h-[50vh] md:min-h-[70vh] flex items-center pt-32 pb-20 md:pt-40 md:pb-32 overflow-hidden bg-primary-dark">
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-4xl animate-slide-up">
|
||||
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5">
|
||||
<Badge
|
||||
variant="saturated"
|
||||
className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5"
|
||||
>
|
||||
{t('heroSubtitle')}
|
||||
</Badge>
|
||||
<Heading level={1} className="text-white mb-4 md:mb-8">
|
||||
@@ -97,16 +103,24 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
green: (chunks) => (
|
||||
<span className="relative inline-block">
|
||||
<span className="relative z-10 text-accent italic">{chunks}</span>
|
||||
<Scribble variant="circle" className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block" />
|
||||
<Scribble
|
||||
variant="circle"
|
||||
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
),
|
||||
})}
|
||||
</Heading>
|
||||
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 md:gap-6">
|
||||
<Button href="#categories" variant="accent" size="lg" className="group w-full md:w-auto">
|
||||
<Button
|
||||
href="#categories"
|
||||
variant="accent"
|
||||
size="lg"
|
||||
className="group w-full md:w-auto"
|
||||
>
|
||||
{t('viewProducts')}
|
||||
<span className="ml-3 transition-transform group-hover:translate-y-1">↓</span>
|
||||
</Button>
|
||||
@@ -123,8 +137,8 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
<Link key={idx} href={category.href} className="group block">
|
||||
<Card className="h-full border-none shadow-sm hover:shadow-2xl transition-all duration-500 rounded-[24px] md:rounded-[48px] overflow-hidden bg-white active:scale-[0.98]">
|
||||
<div className="relative h-[200px] md:h-[400px] overflow-hidden">
|
||||
<Image
|
||||
src={category.img}
|
||||
<Image
|
||||
src={category.img}
|
||||
alt={category.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 group-hover:scale-105"
|
||||
@@ -132,13 +146,22 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
unoptimized
|
||||
/>
|
||||
<div className="absolute inset-0 image-overlay-gradient opacity-80 group-hover:opacity-90 transition-opacity duration-500" />
|
||||
|
||||
|
||||
<div className="absolute top-3 right-3 md:top-8 md:right-8 w-10 h-10 md:w-20 md:h-20 bg-white/10 backdrop-blur-md rounded-xl md:rounded-[24px] flex items-center justify-center border border-white/20 shadow-2xl transition-all duration-500 group-hover:scale-110 group-hover:bg-white/20">
|
||||
<Image src={category.icon} alt="" width={24} height={24} className="w-6 h-6 md:w-12 md:h-12 brightness-0 invert opacity-80" />
|
||||
<Image
|
||||
src={category.icon}
|
||||
alt=""
|
||||
width={24}
|
||||
height={24}
|
||||
className="w-6 h-6 md:w-12 md:h-12 brightness-0 invert opacity-80"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 left-4 md:bottom-10 md:left-10 right-4 md:right-10">
|
||||
<Badge variant="accent" className="mb-2 md:mb-4 shadow-lg bg-accent text-primary-dark border-none text-[10px] md:text-xs">
|
||||
<Badge
|
||||
variant="accent"
|
||||
className="mb-2 md:mb-4 shadow-lg bg-accent text-primary-dark border-none text-[10px] md:text-xs"
|
||||
>
|
||||
{t('categoryLabel')}
|
||||
</Badge>
|
||||
<h2 className="text-xl md:text-4xl font-bold text-white leading-tight transition-transform duration-500 group-hover:translate-x-1">
|
||||
@@ -155,8 +178,18 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
{t('viewProducts')}
|
||||
</span>
|
||||
<div className="ml-3 md:ml-4 w-8 h-8 md:w-10 md:h-10 rounded-full bg-primary-light flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm">
|
||||
<svg className="w-4 h-4 md:w-5 md:h-5 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
|
||||
className="w-4 h-4 md:w-5 md:h-5 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>
|
||||
@@ -168,7 +201,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
|
||||
{/* Technical Support CTA */}
|
||||
<Reveal>
|
||||
<Section className="bg-white py-12 md:py-28">
|
||||
@@ -177,14 +210,23 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
<div className="absolute top-0 right-0 w-1/2 h-full bg-accent/5 -skew-x-12 translate-x-1/4" />
|
||||
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-6 md:gap-12">
|
||||
<div className="max-w-2xl text-center lg:text-left">
|
||||
<h2 className="text-2xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-8 tracking-tight">{t('cta.title')}</h2>
|
||||
<h2 className="text-2xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-8 tracking-tight">
|
||||
{t('cta.title')}
|
||||
</h2>
|
||||
<p className="text-base md:text-xl text-white/70 leading-relaxed">
|
||||
{t('cta.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button href={`/${params.locale}/contact`} variant="accent" size="lg" className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl">
|
||||
<Button
|
||||
href={`/${params.locale}/contact`}
|
||||
variant="accent"
|
||||
size="lg"
|
||||
className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl"
|
||||
>
|
||||
{t('cta.button')}
|
||||
<span className="ml-4 transition-transform group-hover:translate-x-2">→</span>
|
||||
<span className="ml-4 transition-transform group-hover:translate-x-2">
|
||||
→
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user