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:
@@ -6,6 +6,7 @@ import { Metadata } from 'next';
|
|||||||
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
||||||
import { mdxComponents } from '@/components/blog/MDXComponents';
|
import { mdxComponents } from '@/components/blog/MDXComponents';
|
||||||
import { getOGImageMetadata } from '@/lib/metadata';
|
import { getOGImageMetadata } from '@/lib/metadata';
|
||||||
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
@@ -30,7 +31,7 @@ export async function generateStaticParams() {
|
|||||||
|
|
||||||
export async function generateMetadata({ params: { locale, slug } }: PageProps): Promise<Metadata> {
|
export async function generateMetadata({ params: { locale, slug } }: PageProps): Promise<Metadata> {
|
||||||
const pageData = await getPageBySlug(slug, locale);
|
const pageData = await getPageBySlug(slug, locale);
|
||||||
|
|
||||||
if (!pageData) return {};
|
if (!pageData) return {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -39,15 +40,15 @@ export async function generateMetadata({ params: { locale, slug } }: PageProps):
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}/${slug}`,
|
canonical: `/${locale}/${slug}`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': `/de/${slug}`,
|
de: `/de/${slug}`,
|
||||||
'en': `/en/${slug}`,
|
en: `/en/${slug}`,
|
||||||
'x-default': `/en/${slug}`,
|
'x-default': `/en/${slug}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${pageData.frontmatter.title} | KLZ Cables`,
|
title: `${pageData.frontmatter.title} | KLZ Cables`,
|
||||||
description: pageData.frontmatter.excerpt || '',
|
description: pageData.frontmatter.excerpt || '',
|
||||||
url: `https://klz-cables.com/${locale}/${slug}`,
|
url: `${SITE_URL}/${locale}/${slug}`,
|
||||||
images: getOGImageMetadata(slug, pageData.frontmatter.title, locale),
|
images: getOGImageMetadata(slug, pageData.frontmatter.title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -75,7 +76,9 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
|
|||||||
</div>
|
</div>
|
||||||
<Container className="relative z-10">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-4xl animate-slide-up">
|
<div className="max-w-4xl animate-slide-up">
|
||||||
<Badge variant="accent" className="mb-4 md:mb-6">{t('badge')}</Badge>
|
<Badge variant="accent" className="mb-4 md:mb-6">
|
||||||
|
{t('badge')}
|
||||||
|
</Badge>
|
||||||
<Heading level={1} className="text-white mb-0">
|
<Heading level={1} className="text-white mb-0">
|
||||||
{pageData.frontmatter.title}
|
{pageData.frontmatter.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -106,9 +109,14 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
|
|||||||
<div className="relative z-10 max-w-2xl">
|
<div className="relative z-10 max-w-2xl">
|
||||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>
|
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>
|
||||||
<p className="text-lg text-white/70 mb-8">{t('supportTeamAvailable')}</p>
|
<p className="text-lg text-white/70 mb-8">{t('supportTeamAvailable')}</p>
|
||||||
<a href={`/${locale}/contact`} className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group/link">
|
<a
|
||||||
{t('contactUs')}
|
href={`/${locale}/contact`}
|
||||||
<span className="ml-2 transition-transform group-hover/link:translate-x-1">→</span>
|
className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group/link"
|
||||||
|
>
|
||||||
|
{t('contactUs')}
|
||||||
|
<span className="ml-2 transition-transform group-hover/link:translate-x-1">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,4 +124,4 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import { ImageResponse } from 'next/og';
|
|||||||
import { getPostBySlug } from '@/lib/blog';
|
import { getPostBySlug } from '@/lib/blog';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||||
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug: string } }) {
|
export default async function Image({
|
||||||
|
params: { locale, slug },
|
||||||
|
}: {
|
||||||
|
params: { locale: string; slug: string };
|
||||||
|
}) {
|
||||||
const post = await getPostBySlug(slug, locale);
|
const post = await getPostBySlug(slug, locale);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
@@ -19,24 +24,21 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
|
|||||||
// but if we are in nodejs runtime, we could potentially read from disk.
|
// but if we are in nodejs runtime, we could potentially read from disk.
|
||||||
// For now, let's just make sure it's absolute.
|
// For now, let's just make sure it's absolute.
|
||||||
const featuredImage = post.frontmatter.featuredImage
|
const featuredImage = post.frontmatter.featuredImage
|
||||||
? (post.frontmatter.featuredImage.startsWith('http')
|
? post.frontmatter.featuredImage.startsWith('http')
|
||||||
? post.frontmatter.featuredImage
|
? post.frontmatter.featuredImage
|
||||||
: `https://klz-cables.com${post.frontmatter.featuredImage}`)
|
: `${SITE_URL}${post.frontmatter.featuredImage}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate
|
||||||
<OGImageTemplate
|
title={post.frontmatter.title}
|
||||||
title={post.frontmatter.title}
|
description={post.frontmatter.excerpt}
|
||||||
description={post.frontmatter.excerpt}
|
label={post.frontmatter.category || 'Blog'}
|
||||||
label={post.frontmatter.category || 'Blog'}
|
image={featuredImage}
|
||||||
image={featuredImage}
|
/>,
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ interface BlogPostProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale, slug } }: BlogPostProps): Promise<Metadata> {
|
export async function generateMetadata({
|
||||||
|
params: { locale, slug },
|
||||||
|
}: BlogPostProps): Promise<Metadata> {
|
||||||
const post = await getPostBySlug(slug, locale);
|
const post = await getPostBySlug(slug, locale);
|
||||||
|
|
||||||
if (!post) return {};
|
if (!post) return {};
|
||||||
|
|
||||||
const description = post.frontmatter.excerpt || '';
|
const description = post.frontmatter.excerpt || '';
|
||||||
@@ -32,8 +34,8 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}/blog/${slug}`,
|
canonical: `/${locale}/blog/${slug}`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': `/de/blog/${slug}`,
|
de: `/de/blog/${slug}`,
|
||||||
'en': `/en/blog/${slug}`,
|
en: `/en/blog/${slug}`,
|
||||||
'x-default': `/en/blog/${slug}`,
|
'x-default': `/en/blog/${slug}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -43,7 +45,7 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
|
|||||||
type: 'article',
|
type: 'article',
|
||||||
publishedTime: post.frontmatter.date,
|
publishedTime: post.frontmatter.date,
|
||||||
authors: ['KLZ Cables'],
|
authors: ['KLZ Cables'],
|
||||||
url: `https://klz-cables.com/${locale}/blog/${slug}`,
|
url: `${SITE_URL}/${locale}/blog/${slug}`,
|
||||||
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
|
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -66,16 +68,15 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
|
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
|
||||||
|
|
||||||
{/* Featured Image Header */}
|
{/* Featured Image Header */}
|
||||||
{post.frontmatter.featuredImage ? (
|
{post.frontmatter.featuredImage ? (
|
||||||
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
|
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100"
|
className="absolute inset-0 bg-cover bg-center transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100"
|
||||||
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
|
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
|
||||||
|
|
||||||
{/* Title overlay on image */}
|
{/* Title overlay on image */}
|
||||||
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
|
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
@@ -87,7 +88,10 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Heading level={1} className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
|
<Heading
|
||||||
|
level={1}
|
||||||
|
className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]"
|
||||||
|
>
|
||||||
{post.frontmatter.title}
|
{post.frontmatter.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
|
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
|
||||||
@@ -95,7 +99,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
|||||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric',
|
||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
||||||
@@ -123,7 +127,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
|||||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric',
|
||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
||||||
@@ -168,8 +172,18 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
|||||||
href={`/${locale}/blog`}
|
href={`/${locale}/blog`}
|
||||||
className="inline-flex items-center gap-3 text-text-secondary hover:text-primary font-bold text-sm uppercase tracking-widest transition-all group"
|
className="inline-flex items-center gap-3 text-text-secondary hover:text-primary font-bold text-sm uppercase tracking-widest transition-all group"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 transition-transform group-hover:-translate-x-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
className="w-5 h-5 transition-transform group-hover:-translate-x-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{locale === 'de' ? 'Zurück zur Übersicht' : 'Back to Overview'}
|
{locale === 'de' ? 'Zurück zur Übersicht' : 'Back to Overview'}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -188,57 +202,63 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
|||||||
{/* Structured Data */}
|
{/* Structured Data */}
|
||||||
<JsonLd
|
<JsonLd
|
||||||
id={`jsonld-${slug}`}
|
id={`jsonld-${slug}`}
|
||||||
data={{
|
data={
|
||||||
'@context': 'https://schema.org',
|
{
|
||||||
'@type': 'BlogPosting',
|
'@context': 'https://schema.org',
|
||||||
headline: post.frontmatter.title,
|
'@type': 'BlogPosting',
|
||||||
datePublished: post.frontmatter.date,
|
headline: post.frontmatter.title,
|
||||||
dateModified: post.frontmatter.date,
|
datePublished: post.frontmatter.date,
|
||||||
image: post.frontmatter.featuredImage ? `https://klz-cables.com${post.frontmatter.featuredImage}` : undefined,
|
dateModified: post.frontmatter.date,
|
||||||
author: {
|
image: post.frontmatter.featuredImage
|
||||||
'@type': 'Organization',
|
? `${SITE_URL}${post.frontmatter.featuredImage}`
|
||||||
name: 'KLZ Cables',
|
: undefined,
|
||||||
url: 'https://klz-cables.com',
|
author: {
|
||||||
logo: 'https://klz-cables.com/logo-blue.svg'
|
'@type': 'Organization',
|
||||||
},
|
name: 'KLZ Cables',
|
||||||
publisher: {
|
url: SITE_URL,
|
||||||
'@type': 'Organization',
|
logo: `${SITE_URL}/logo-blue.svg`,
|
||||||
name: 'KLZ Cables',
|
|
||||||
logo: {
|
|
||||||
'@type': 'ImageObject',
|
|
||||||
url: 'https://klz-cables.com/logo-blue.svg',
|
|
||||||
},
|
},
|
||||||
},
|
publisher: {
|
||||||
description: post.frontmatter.excerpt,
|
'@type': 'Organization',
|
||||||
mainEntityOfPage: {
|
name: 'KLZ Cables',
|
||||||
'@type': 'WebPage',
|
logo: {
|
||||||
'@id': `https://klz-cables.com/${locale}/blog/${slug}`,
|
'@type': 'ImageObject',
|
||||||
},
|
url: `${SITE_URL}/logo-blue.svg`,
|
||||||
articleSection: post.frontmatter.category,
|
},
|
||||||
wordCount: post.content.split(/\s+/).length,
|
},
|
||||||
timeRequired: `PT${getReadingTime(post.content)}M`
|
description: post.frontmatter.excerpt,
|
||||||
} as any}
|
mainEntityOfPage: {
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': `${SITE_URL}/${locale}/blog/${slug}`,
|
||||||
|
},
|
||||||
|
articleSection: post.frontmatter.category,
|
||||||
|
wordCount: post.content.split(/\s+/).length,
|
||||||
|
timeRequired: `PT${getReadingTime(post.content)}M`,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<JsonLd
|
<JsonLd
|
||||||
id={`breadcrumb-${slug}`}
|
id={`breadcrumb-${slug}`}
|
||||||
data={{
|
data={
|
||||||
'@context': 'https://schema.org',
|
{
|
||||||
'@type': 'BreadcrumbList',
|
'@context': 'https://schema.org',
|
||||||
itemListElement: [
|
'@type': 'BreadcrumbList',
|
||||||
{
|
itemListElement: [
|
||||||
'@type': 'ListItem',
|
{
|
||||||
position: 1,
|
'@type': 'ListItem',
|
||||||
name: 'Blog',
|
position: 1,
|
||||||
item: `https://klz-cables.com/${locale}/blog`,
|
name: 'Blog',
|
||||||
},
|
item: `${SITE_URL}/${locale}/blog`,
|
||||||
{
|
},
|
||||||
'@type': 'ListItem',
|
{
|
||||||
position: 2,
|
'@type': 'ListItem',
|
||||||
name: post.frontmatter.title,
|
position: 2,
|
||||||
item: `https://klz-cables.com/${locale}/blog/${slug}`,
|
name: post.frontmatter.title,
|
||||||
},
|
item: `${SITE_URL}/${locale}/blog/${slug}`,
|
||||||
],
|
},
|
||||||
} as any}
|
],
|
||||||
|
} as any
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Section, Container, Heading, Card, Badge, Button } from '@/components/u
|
|||||||
import Reveal from '@/components/Reveal';
|
import Reveal from '@/components/Reveal';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { getOGImageMetadata } from '@/lib/metadata';
|
import { getOGImageMetadata } from '@/lib/metadata';
|
||||||
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
interface BlogIndexProps {
|
interface BlogIndexProps {
|
||||||
params: {
|
params: {
|
||||||
@@ -19,15 +20,15 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}/blog`,
|
canonical: `/${locale}/blog`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': '/de/blog',
|
de: '/de/blog',
|
||||||
'en': '/en/blog',
|
en: '/en/blog',
|
||||||
'x-default': '/en/blog',
|
'x-default': '/en/blog',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${t('title')} | KLZ Cables`,
|
title: `${t('title')} | KLZ Cables`,
|
||||||
description: t('description'),
|
description: t('description'),
|
||||||
url: `https://klz-cables.com/${locale}/blog`,
|
url: `${SITE_URL}/${locale}/blog`,
|
||||||
images: getOGImageMetadata('blog', t('title'), locale),
|
images: getOGImageMetadata('blog', t('title'), locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -41,10 +42,10 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
|
|||||||
export default async function BlogIndex({ params: { locale } }: BlogIndexProps) {
|
export default async function BlogIndex({ params: { locale } }: BlogIndexProps) {
|
||||||
const t = await getTranslations('Blog');
|
const t = await getTranslations('Blog');
|
||||||
const posts = await getAllPosts(locale);
|
const posts = await getAllPosts(locale);
|
||||||
|
|
||||||
// Sort posts by date descending
|
// Sort posts by date descending
|
||||||
const sortedPosts = [...posts].sort((a, b) =>
|
const sortedPosts = [...posts].sort(
|
||||||
new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
|
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const featuredPost = sortedPosts[0];
|
const featuredPost = sortedPosts[0];
|
||||||
@@ -65,10 +66,12 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
|
|||||||
<div className="absolute inset-0 image-overlay-gradient" />
|
<div className="absolute inset-0 image-overlay-gradient" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Container className="relative z-10">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-4xl animate-slide-up">
|
<div className="max-w-4xl animate-slide-up">
|
||||||
<Badge variant="saturated" className="mb-4 md:mb-6">{t('featuredPost')}</Badge>
|
<Badge variant="saturated" className="mb-4 md:mb-6">
|
||||||
|
{t('featuredPost')}
|
||||||
|
</Badge>
|
||||||
{featuredPost && (
|
{featuredPost && (
|
||||||
<>
|
<>
|
||||||
<Heading level={1} className="text-white mb-4 md:mb-8">
|
<Heading level={1} className="text-white mb-4 md:mb-8">
|
||||||
@@ -77,9 +80,16 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
|
|||||||
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
|
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
|
||||||
{featuredPost.frontmatter.excerpt}
|
{featuredPost.frontmatter.excerpt}
|
||||||
</p>
|
</p>
|
||||||
<Button href={`/${locale}/blog/${featuredPost.slug}`} variant="accent" size="lg" className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl">
|
<Button
|
||||||
|
href={`/${locale}/blog/${featuredPost.slug}`}
|
||||||
|
variant="accent"
|
||||||
|
size="lg"
|
||||||
|
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl"
|
||||||
|
>
|
||||||
{t('readFullArticle')}
|
{t('readFullArticle')}
|
||||||
<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
<span className="ml-3 transition-transform group-hover:translate-x-2">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -97,10 +107,30 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
|
|||||||
</Heading>
|
</Heading>
|
||||||
<div className="flex flex-wrap gap-2 md:gap-4">
|
<div className="flex flex-wrap gap-2 md:gap-4">
|
||||||
{/* Category filters could go here */}
|
{/* Category filters could go here */}
|
||||||
<Badge variant="primary" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.all')}</Badge>
|
<Badge
|
||||||
<Badge variant="neutral" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.industry')}</Badge>
|
variant="primary"
|
||||||
<Badge variant="neutral" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.technical')}</Badge>
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
<Badge variant="neutral" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.sustainability')}</Badge>
|
>
|
||||||
|
{t('categories.all')}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant="neutral"
|
||||||
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
|
>
|
||||||
|
{t('categories.industry')}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant="neutral"
|
||||||
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
|
>
|
||||||
|
{t('categories.technical')}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant="neutral"
|
||||||
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
|
>
|
||||||
|
{t('categories.sustainability')}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -120,7 +150,10 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
|
|||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
{post.frontmatter.category && (
|
{post.frontmatter.category && (
|
||||||
<Badge variant="accent" className="absolute top-3 left-3 md:top-6 md:left-6 shadow-lg">
|
<Badge
|
||||||
|
variant="accent"
|
||||||
|
className="absolute top-3 left-3 md:top-6 md:left-6 shadow-lg"
|
||||||
|
>
|
||||||
{post.frontmatter.category}
|
{post.frontmatter.category}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -131,7 +164,7 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
|
|||||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric',
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg md:text-2xl font-bold text-primary mb-3 md:mb-6 group-hover:text-accent-dark transition-colors line-clamp-2 leading-tight">
|
<h3 className="text-lg md:text-2xl font-bold text-primary mb-3 md:mb-6 group-hover:text-accent-dark transition-colors line-clamp-2 leading-tight">
|
||||||
@@ -145,8 +178,18 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
|
|||||||
{t('readMore')}
|
{t('readMore')}
|
||||||
</span>
|
</span>
|
||||||
<div className="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">
|
<div className="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">
|
||||||
<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">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,13 +199,21 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination Placeholder */}
|
{/* Pagination Placeholder */}
|
||||||
<div className="mt-12 md:mt-24 flex justify-center gap-2 md:gap-4">
|
<div className="mt-12 md:mt-24 flex justify-center gap-2 md:gap-4">
|
||||||
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base" disabled>{t('prev')}</Button>
|
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base" disabled>
|
||||||
<Button variant="primary" size="sm" className="md:h-11 md:px-6 md:text-base">1</Button>
|
{t('prev')}
|
||||||
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">2</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">{t('next')}</Button>
|
<Button variant="primary" size="sm" className="md:h-11 md:px-6 md:text-base">
|
||||||
|
1
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">
|
||||||
|
2
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">
|
||||||
|
{t('next')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ interface ContactPageProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }: ContactPageProps): Promise<Metadata> {
|
export async function generateMetadata({
|
||||||
|
params: { locale },
|
||||||
|
}: ContactPageProps): Promise<Metadata> {
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||||
const title = t('meta.title') || t('title');
|
const title = t('meta.title') || t('title');
|
||||||
const description = t('meta.description') || t('subtitle');
|
const description = t('meta.description') || t('subtitle');
|
||||||
@@ -31,7 +33,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `https://klz-cables.com/${locale}/contact`,
|
canonical: `${SITE_URL}/${locale}/contact`,
|
||||||
languages: {
|
languages: {
|
||||||
'de-DE': '/de/contact',
|
'de-DE': '/de/contact',
|
||||||
'en-US': '/en/contact',
|
'en-US': '/en/contact',
|
||||||
@@ -40,7 +42,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
|
|||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${title} | KLZ Cables`,
|
title: `${title} | KLZ Cables`,
|
||||||
description,
|
description,
|
||||||
url: `https://klz-cables.com/${locale}/contact`,
|
url: `${SITE_URL}/${locale}/contact`,
|
||||||
siteName: 'KLZ Cables',
|
siteName: 'KLZ Cables',
|
||||||
images: getOGImageMetadata('contact', title, locale),
|
images: getOGImageMetadata('contact', title, locale),
|
||||||
locale: `${locale.toUpperCase()}_DE`,
|
locale: `${locale.toUpperCase()}_DE`,
|
||||||
@@ -78,7 +80,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
'@type': 'ListItem',
|
'@type': 'ListItem',
|
||||||
position: 1,
|
position: 1,
|
||||||
name: t('title'),
|
name: t('title'),
|
||||||
item: `https://klz-cables.com/${locale}/contact`,
|
item: `${SITE_URL}/${locale}/contact`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
@@ -89,9 +91,9 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'LocalBusiness',
|
'@type': 'LocalBusiness',
|
||||||
name: 'KLZ Cables',
|
name: 'KLZ Cables',
|
||||||
image: 'https://klz-cables.com/logo.png',
|
image: `${SITE_URL}/logo.png`,
|
||||||
'@id': 'https://klz-cables.com',
|
'@id': SITE_URL,
|
||||||
url: 'https://klz-cables.com',
|
url: SITE_URL,
|
||||||
address: {
|
address: {
|
||||||
'@type': 'PostalAddress',
|
'@type': 'PostalAddress',
|
||||||
streetAddress: 'Raiffeisenstraße 22',
|
streetAddress: 'Raiffeisenstraße 22',
|
||||||
@@ -107,20 +109,12 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
openingHoursSpecification: [
|
openingHoursSpecification: [
|
||||||
{
|
{
|
||||||
'@type': 'OpeningHoursSpecification',
|
'@type': 'OpeningHoursSpecification',
|
||||||
dayOfWeek: [
|
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||||
'Monday',
|
|
||||||
'Tuesday',
|
|
||||||
'Wednesday',
|
|
||||||
'Thursday',
|
|
||||||
'Friday'
|
|
||||||
],
|
|
||||||
opens: '08:00',
|
opens: '08:00',
|
||||||
closes: '17:00'
|
closes: '17:00',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
sameAs: [
|
sameAs: ['https://www.linkedin.com/company/klz-cables'],
|
||||||
'https://www.linkedin.com/company/klz-cables'
|
|
||||||
]
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
@@ -154,36 +148,71 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
<div className="space-y-4 md:space-y-8">
|
<div className="space-y-4 md:space-y-8">
|
||||||
<div className="flex items-start gap-4 md:gap-6 group">
|
<div className="flex items-start gap-4 md:gap-6 group">
|
||||||
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
|
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
|
||||||
<svg className="w-5 h-5 md:w-7 md:h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
className="w-5 h-5 md:w-7 md:h-7"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">{t('info.office')}</h4>
|
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">
|
||||||
|
{t('info.office')}
|
||||||
|
</h4>
|
||||||
<p className="text-sm md:text-lg text-text-secondary leading-relaxed whitespace-pre-line">
|
<p className="text-sm md:text-lg text-text-secondary leading-relaxed whitespace-pre-line">
|
||||||
{t('info.address')}
|
{t('info.address')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4 md:gap-6 group">
|
<div className="flex items-start gap-4 md:gap-6 group">
|
||||||
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
|
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
|
||||||
<svg className="w-5 h-5 md:w-7 md:h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
className="w-5 h-5 md:w-7 md:h-7"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">{t('info.email')}</h4>
|
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">
|
||||||
<a href="mailto:info@klz-cables.com" className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target">info@klz-cables.com</a>
|
{t('info.email')}
|
||||||
|
</h4>
|
||||||
|
<a
|
||||||
|
href="mailto:info@klz-cables.com"
|
||||||
|
className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target"
|
||||||
|
>
|
||||||
|
info@klz-cables.com
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 md:p-10 bg-white rounded-2xl md:rounded-3xl border border-neutral-medium shadow-sm animate-fade-in">
|
<div className="p-6 md:p-10 bg-white rounded-2xl md:rounded-3xl border border-neutral-medium shadow-sm animate-fade-in">
|
||||||
<Heading level={4} className="mb-4 md:mb-6">{t('hours.title')}</Heading>
|
<Heading level={4} className="mb-4 md:mb-6">
|
||||||
|
{t('hours.title')}
|
||||||
|
</Heading>
|
||||||
<ul className="space-y-2 md:space-y-4 list-none m-0 p-0">
|
<ul className="space-y-2 md:space-y-4 list-none m-0 p-0">
|
||||||
<li className="flex justify-between items-center pb-2 md:pb-4 border-b border-neutral-medium text-sm md:text-base">
|
<li className="flex justify-between items-center pb-2 md:pb-4 border-b border-neutral-medium text-sm md:text-base">
|
||||||
<span className="font-bold text-primary">{t('hours.weekdays')}</span>
|
<span className="font-bold text-primary">{t('hours.weekdays')}</span>
|
||||||
@@ -199,24 +228,28 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
|
|
||||||
{/* Contact Form */}
|
{/* Contact Form */}
|
||||||
<div className="lg:col-span-7">
|
<div className="lg:col-span-7">
|
||||||
<Suspense fallback={<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>}>
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ContactForm />
|
<ContactForm />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Map Section */}
|
{/* Map Section */}
|
||||||
<section className="h-[300px] md:h-[500px] bg-neutral-medium relative overflow-hidden grayscale hover:grayscale-0 transition-all duration-1000">
|
<section className="h-[300px] md:h-[500px] bg-neutral-medium relative overflow-hidden grayscale hover:grayscale-0 transition-all duration-1000">
|
||||||
<Suspense fallback={<div className="h-full w-full bg-neutral-medium animate-pulse flex items-center justify-center">
|
<Suspense
|
||||||
<div className="text-primary font-medium">Loading Map...</div>
|
fallback={
|
||||||
</div>}>
|
<div className="h-full w-full bg-neutral-medium animate-pulse flex items-center justify-center">
|
||||||
<LeafletMap
|
<div className="text-primary font-medium">Loading Map...</div>
|
||||||
address={t('info.address')}
|
</div>
|
||||||
lat={48.8144}
|
}
|
||||||
lng={9.4144}
|
>
|
||||||
/>
|
<LeafletMap address={t('info.address')} lat={48.8144} lng={9.4144} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,25 +20,45 @@ export default function HomePage({ params: { locale } }: { params: { locale: str
|
|||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
<JsonLd
|
<JsonLd
|
||||||
id="breadcrumb-home"
|
id="breadcrumb-home"
|
||||||
data={getBreadcrumbSchema([
|
data={getBreadcrumbSchema([{ name: 'Home', item: `/${locale}` }])}
|
||||||
{ name: 'Home', item: `/${locale}` },
|
|
||||||
])}
|
|
||||||
/>
|
/>
|
||||||
<Hero />
|
<Hero />
|
||||||
<Reveal><ProductCategories /></Reveal>
|
<Reveal>
|
||||||
<Reveal><WhatWeDo /></Reveal>
|
<ProductCategories />
|
||||||
<Reveal><RecentPosts locale={locale} /></Reveal>
|
</Reveal>
|
||||||
<Reveal><Experience /></Reveal>
|
<Reveal>
|
||||||
<Reveal><WhyChooseUs /></Reveal>
|
<WhatWeDo />
|
||||||
<Reveal><MeetTheTeam /></Reveal>
|
</Reveal>
|
||||||
<Reveal><GallerySection /></Reveal>
|
<Reveal>
|
||||||
<Reveal><VideoSection /></Reveal>
|
<RecentPosts locale={locale} />
|
||||||
<Reveal><CTA /></Reveal>
|
</Reveal>
|
||||||
|
<Reveal>
|
||||||
|
<Experience />
|
||||||
|
</Reveal>
|
||||||
|
<Reveal>
|
||||||
|
<WhyChooseUs />
|
||||||
|
</Reveal>
|
||||||
|
<Reveal>
|
||||||
|
<MeetTheTeam />
|
||||||
|
</Reveal>
|
||||||
|
<Reveal>
|
||||||
|
<GallerySection />
|
||||||
|
</Reveal>
|
||||||
|
<Reveal>
|
||||||
|
<VideoSection />
|
||||||
|
</Reveal>
|
||||||
|
<Reveal>
|
||||||
|
<CTA />
|
||||||
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }: { params: { locale: string } }): Promise<Metadata> {
|
export async function generateMetadata({
|
||||||
|
params: { locale },
|
||||||
|
}: {
|
||||||
|
params: { locale: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
// Use translations for meta where available (namespace: Index.meta)
|
// Use translations for meta where available (namespace: Index.meta)
|
||||||
// Fallback to a sensible default if translation keys are missing.
|
// Fallback to a sensible default if translation keys are missing.
|
||||||
let t;
|
let t;
|
||||||
@@ -62,15 +82,15 @@ export async function generateMetadata({ params: { locale } }: { params: { local
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}`,
|
canonical: `/${locale}`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': '/de',
|
de: '/de',
|
||||||
'en': '/en',
|
en: '/en',
|
||||||
'x-default': '/en',
|
'x-default': '/en',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${title} | KLZ Cables`,
|
title: `${title} | KLZ Cables`,
|
||||||
description,
|
description,
|
||||||
url: `https://klz-cables.com/${locale}`,
|
url: `${SITE_URL}/${locale}`,
|
||||||
images: getOGImageMetadata('', title, locale),
|
images: getOGImageMetadata('', title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
|
|||||||
@@ -31,12 +31,23 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
const t = await getTranslations('Products');
|
const t = await getTranslations('Products');
|
||||||
|
|
||||||
// Check if it's a category page
|
// 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);
|
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
|
||||||
if (categories.includes(fileSlug)) {
|
if (categories.includes(fileSlug)) {
|
||||||
const categoryKey = fileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
const categoryKey = fileSlug
|
||||||
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : fileSlug;
|
.replace(/-cables$/, '')
|
||||||
const categoryDesc = t.has(`categories.${categoryKey}.description`) ? t(`categories.${categoryKey}.description`) : '';
|
.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 {
|
return {
|
||||||
title: categoryTitle,
|
title: categoryTitle,
|
||||||
@@ -44,15 +55,15 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}/products/${productSlug}`,
|
canonical: `/${locale}/products/${productSlug}`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': `/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
de: `/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||||
'en': `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
en: `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||||
'x-default': `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
'x-default': `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${categoryTitle} | KLZ Cables`,
|
title: `${categoryTitle} | KLZ Cables`,
|
||||||
description: categoryDesc,
|
description: categoryDesc,
|
||||||
url: `https://klz-cables.com/${locale}/products/${productSlug}`,
|
url: `${SITE_URL}/${locale}/products/${productSlug}`,
|
||||||
images: getProductOGImageMetadata(fileSlug, categoryTitle, locale),
|
images: getProductOGImageMetadata(fileSlug, categoryTitle, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -72,8 +83,8 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}/products/${slug.join('/')}`,
|
canonical: `/${locale}/products/${slug.join('/')}`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': `/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
de: `/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||||
'en': `/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
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')}`,
|
'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`,
|
title: `${product.frontmatter.title} | KLZ Cables`,
|
||||||
description: product.frontmatter.description,
|
description: product.frontmatter.description,
|
||||||
type: 'website',
|
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),
|
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -95,20 +106,36 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
const components = {
|
const components = {
|
||||||
ProductTechnicalData,
|
ProductTechnicalData,
|
||||||
ProductTabs,
|
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) => (
|
h2: (props: any) => (
|
||||||
<div className="relative mb-16">
|
<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 className="w-20 h-1.5 bg-accent rounded-full" />
|
||||||
</div>
|
</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" />,
|
ul: (props: any) => <ul {...props} className="list-none pl-0 mb-10" />,
|
||||||
section: (props: any) => <div {...props} className="block" />,
|
section: (props: any) => <div {...props} className="block" />,
|
||||||
li: (props: any) => (
|
li: (props: any) => (
|
||||||
<li className="flex items-start gap-4 group mb-4 last:mb-0">
|
<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" />
|
<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>
|
</li>
|
||||||
),
|
),
|
||||||
strong: (props: any) => <strong {...props} className="font-black text-primary" />,
|
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" />
|
<table {...props} className="min-w-full divide-y divide-neutral-dark/10" />
|
||||||
</div>
|
</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" />,
|
th: (props: any) => (
|
||||||
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}
|
||||||
|
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" />,
|
hr: () => <hr className="my-24 border-t-2 border-neutral-dark/5" />,
|
||||||
blockquote: (props: any) => (
|
blockquote: (props: any) => (
|
||||||
<div className="my-20 p-10 md:p-16 bg-primary-dark rounded-[40px] relative overflow-hidden group">
|
<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="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>
|
</div>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -134,28 +174,36 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
const t = await getTranslations('Products');
|
const t = await getTranslations('Products');
|
||||||
|
|
||||||
// Check if it's a category page
|
// 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);
|
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
|
||||||
|
|
||||||
if (categories.includes(fileSlug)) {
|
if (categories.includes(fileSlug)) {
|
||||||
const allProducts = await getAllProducts(locale);
|
const allProducts = await getAllProducts(locale);
|
||||||
const categoryKey = fileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
const categoryKey = fileSlug
|
||||||
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : 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
|
// Filter products for this category
|
||||||
const filteredProducts = allProducts.filter(p =>
|
const filteredProducts = allProducts.filter((p) =>
|
||||||
p.frontmatter.categories.some(cat =>
|
p.frontmatter.categories.some(
|
||||||
cat.toLowerCase().replace(/\s+/g, '-') === fileSlug ||
|
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === fileSlug || cat === categoryTitle,
|
||||||
cat === categoryTitle
|
),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get translated product slugs
|
// Get translated product slugs
|
||||||
const productsWithTranslatedSlugs = await Promise.all(
|
const productsWithTranslatedSlugs = await Promise.all(
|
||||||
filteredProducts.map(async (p) => ({
|
filteredProducts.map(async (p) => ({
|
||||||
...p,
|
...p,
|
||||||
translatedSlug: await mapFileSlugToTranslated(p.slug, locale)
|
translatedSlug: await mapFileSlugToTranslated(p.slug, locale),
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -164,7 +212,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<Container className="relative z-10">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-4xl animate-slide-up">
|
<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">
|
<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="mx-3 opacity-30">/</span>
|
||||||
<span className="text-white/90">{categoryTitle}</span>
|
<span className="text-white/90">{categoryTitle}</span>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -202,7 +252,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<div className="p-8 md:p-10">
|
<div className="p-8 md:p-10">
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
{product.frontmatter.categories.map((cat, i) => (
|
{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}
|
{cat}
|
||||||
</span>
|
</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">
|
<span className="border-b-2 border-primary/10 group-hover:border-accent-dark transition-colors pb-1">
|
||||||
{t('details')}
|
{t('details')}
|
||||||
</span>
|
</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">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,7 +301,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract technical data for schema
|
// 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 = [];
|
let technicalItems = [];
|
||||||
if (technicalDataMatch) {
|
if (technicalDataMatch) {
|
||||||
try {
|
try {
|
||||||
@@ -253,11 +318,15 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
const isFallback = (product.frontmatter as any).isFallback;
|
const isFallback = (product.frontmatter as any).isFallback;
|
||||||
const categorySlug = slug[0];
|
const categorySlug = slug[0];
|
||||||
const categoryFileSlug = await mapSlugToFileSlug(categorySlug, locale);
|
const categoryFileSlug = await mapSlugToFileSlug(categorySlug, locale);
|
||||||
const categoryKey = categoryFileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
const categoryKey = categoryFileSlug
|
||||||
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : 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 = (
|
const sidebar = (
|
||||||
<ProductSidebar
|
<ProductSidebar
|
||||||
productName={product.frontmatter.title}
|
productName={product.frontmatter.title}
|
||||||
productImage={product.frontmatter.images?.[0]}
|
productImage={product.frontmatter.images?.[0]}
|
||||||
datasheetPath={datasheetPath}
|
datasheetPath={datasheetPath}
|
||||||
@@ -287,17 +356,24 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{/* Background Decorative Elements */}
|
{/* 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-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" />
|
<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">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-4xl animate-slide-up">
|
<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]">
|
<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>
|
<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="mx-4 opacity-20">/</span>
|
||||||
<span className="text-white/90">{product.frontmatter.title}</span>
|
<span className="text-white/90">{product.frontmatter.title}</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{isFallback && (
|
{isFallback && (
|
||||||
@@ -308,7 +384,11 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap gap-3 mb-8">
|
<div className="flex flex-wrap gap-3 mb-8">
|
||||||
{product.frontmatter.categories.map((cat, idx) => (
|
{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}
|
{cat}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
@@ -329,11 +409,14 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<Container className="relative">
|
<Container className="relative">
|
||||||
{/* Large Product Image Section */}
|
{/* Large Product Image Section */}
|
||||||
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
{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="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]">
|
<div className="relative w-full aspect-[21/9]">
|
||||||
<Image
|
<Image
|
||||||
src={product.frontmatter.images[0]}
|
src={product.frontmatter.images[0]}
|
||||||
alt={product.frontmatter.title}
|
alt={product.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="object-contain transition-transform duration-1000 hover:scale-105"
|
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 */}
|
{/* 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 className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 h-12 bg-black/5 blur-3xl rounded-[100%]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{product.frontmatter.images.length > 1 && (
|
{product.frontmatter.images.length > 1 && (
|
||||||
<div className="flex justify-center gap-8 mt-20">
|
<div className="flex justify-center gap-8 mt-20">
|
||||||
{product.frontmatter.images.slice(0, 5).map((img, idx) => (
|
{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">
|
<div
|
||||||
<Image src={img} alt="" fill className="object-contain p-4 transition-transform duration-700 group-hover:scale-110" />
|
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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +451,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="max-w-none">
|
<div className="max-w-none">
|
||||||
<MDXRemote source={processedContent} components={productComponents} />
|
<MDXRemote source={processedContent} components={productComponents} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Datasheet Download Section - Only for Medium Voltage for now */}
|
{/* Datasheet Download Section - Only for Medium Voltage for now */}
|
||||||
@@ -379,45 +470,49 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{/* Structured Data */}
|
{/* Structured Data */}
|
||||||
<JsonLd
|
<JsonLd
|
||||||
id={`jsonld-${product.slug}`}
|
id={`jsonld-${product.slug}`}
|
||||||
data={{
|
data={
|
||||||
'@context': 'https://schema.org',
|
{
|
||||||
'@type': 'Product',
|
'@context': 'https://schema.org',
|
||||||
name: product.frontmatter.title,
|
'@type': 'Product',
|
||||||
description: product.frontmatter.description,
|
name: product.frontmatter.title,
|
||||||
sku: product.frontmatter.sku || product.slug.toUpperCase(),
|
description: product.frontmatter.description,
|
||||||
image: product.frontmatter.images?.[0] ? `https://klz-cables.com${product.frontmatter.images[0]}` : undefined,
|
sku: product.frontmatter.sku || product.slug.toUpperCase(),
|
||||||
brand: {
|
image: product.frontmatter.images?.[0]
|
||||||
'@type': 'Brand',
|
? `${SITE_URL}${product.frontmatter.images[0]}`
|
||||||
name: 'KLZ Cables',
|
: undefined,
|
||||||
},
|
brand: {
|
||||||
offers: {
|
'@type': 'Brand',
|
||||||
'@type': 'Offer',
|
name: 'KLZ Cables',
|
||||||
availability: 'https://schema.org/InStock',
|
},
|
||||||
priceCurrency: 'EUR',
|
offers: {
|
||||||
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
|
'@type': 'Offer',
|
||||||
itemCondition: 'https://schema.org/NewCondition',
|
availability: 'https://schema.org/InStock',
|
||||||
},
|
priceCurrency: 'EUR',
|
||||||
additionalProperty: technicalItems.map((item: any) => ({
|
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
||||||
'@type': 'PropertyValue',
|
itemCondition: 'https://schema.org/NewCondition',
|
||||||
name: item.label,
|
},
|
||||||
value: item.value,
|
additionalProperty: technicalItems.map((item: any) => ({
|
||||||
})),
|
'@type': 'PropertyValue',
|
||||||
category: product.frontmatter.categories.join(', '),
|
name: item.label,
|
||||||
mainEntityOfPage: {
|
value: item.value,
|
||||||
'@type': 'WebPage',
|
})),
|
||||||
'@id': `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
|
category: product.frontmatter.categories.join(', '),
|
||||||
},
|
mainEntityOfPage: {
|
||||||
} as any}
|
'@type': 'WebPage',
|
||||||
|
'@id': `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Related Products Section */}
|
{/* Related Products Section */}
|
||||||
<div className="mt-16 pt-16 border-t border-neutral-dark/5">
|
<div className="mt-16 pt-16 border-t border-neutral-dark/5">
|
||||||
<RelatedProducts
|
<RelatedProducts
|
||||||
currentSlug={productSlug}
|
currentSlug={productSlug}
|
||||||
categories={product.frontmatter.categories}
|
categories={product.frontmatter.categories}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||||
import { getOGImageMetadata } from '@/lib/metadata';
|
import { getOGImageMetadata } from '@/lib/metadata';
|
||||||
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
interface ProductsPageProps {
|
interface ProductsPageProps {
|
||||||
params: {
|
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 t = await getTranslations({ locale, namespace: 'Products' });
|
||||||
const title = t('meta.title') || t('title');
|
const title = t('meta.title') || t('title');
|
||||||
const description = t('meta.description') || t('subtitle');
|
const description = t('meta.description') || t('subtitle');
|
||||||
@@ -24,15 +27,15 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}/products`,
|
canonical: `/${locale}/products`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': '/de/products',
|
de: '/de/products',
|
||||||
'en': '/en/products',
|
en: '/en/products',
|
||||||
'x-default': '/en/products',
|
'x-default': '/en/products',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${title} | KLZ Cables`,
|
title: `${title} | KLZ Cables`,
|
||||||
description,
|
description,
|
||||||
url: `https://klz-cables.com/${locale}/products`,
|
url: `${SITE_URL}/${locale}/products`,
|
||||||
images: getOGImageMetadata('products', title, locale),
|
images: getOGImageMetadata('products', title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -58,29 +61,29 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
|||||||
desc: t('categories.lowVoltage.description'),
|
desc: t('categories.lowVoltage.description'),
|
||||||
img: '/uploads/2024/11/low-voltage-category.webp',
|
img: '/uploads/2024/11/low-voltage-category.webp',
|
||||||
icon: '/uploads/2024/11/Low-Voltage.svg',
|
icon: '/uploads/2024/11/Low-Voltage.svg',
|
||||||
href: `/${params.locale}/products/${lowVoltageSlug}`
|
href: `/${params.locale}/products/${lowVoltageSlug}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('categories.mediumVoltage.title'),
|
title: t('categories.mediumVoltage.title'),
|
||||||
desc: t('categories.mediumVoltage.description'),
|
desc: t('categories.mediumVoltage.description'),
|
||||||
img: '/uploads/2024/11/medium-voltage-category.webp',
|
img: '/uploads/2024/11/medium-voltage-category.webp',
|
||||||
icon: '/uploads/2024/11/Medium-Voltage.svg',
|
icon: '/uploads/2024/11/Medium-Voltage.svg',
|
||||||
href: `/${params.locale}/products/${mediumVoltageSlug}`
|
href: `/${params.locale}/products/${mediumVoltageSlug}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('categories.highVoltage.title'),
|
title: t('categories.highVoltage.title'),
|
||||||
desc: t('categories.highVoltage.description'),
|
desc: t('categories.highVoltage.description'),
|
||||||
img: '/uploads/2024/11/high-voltage-category.webp',
|
img: '/uploads/2024/11/high-voltage-category.webp',
|
||||||
icon: '/uploads/2024/11/High-Voltage.svg',
|
icon: '/uploads/2024/11/High-Voltage.svg',
|
||||||
href: `/${params.locale}/products/${highVoltageSlug}`
|
href: `/${params.locale}/products/${highVoltageSlug}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('categories.solar.title'),
|
title: t('categories.solar.title'),
|
||||||
desc: t('categories.solar.description'),
|
desc: t('categories.solar.description'),
|
||||||
img: '/uploads/2024/11/solar-category.webp',
|
img: '/uploads/2024/11/solar-category.webp',
|
||||||
icon: '/uploads/2024/11/Solar.svg',
|
icon: '/uploads/2024/11/Solar.svg',
|
||||||
href: `/${params.locale}/products/${solarSlug}`
|
href: `/${params.locale}/products/${solarSlug}`,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-4xl animate-slide-up">
|
<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')}
|
{t('heroSubtitle')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Heading level={1} className="text-white mb-4 md:mb-8">
|
<Heading level={1} className="text-white mb-4 md:mb-8">
|
||||||
@@ -97,16 +103,24 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
|||||||
green: (chunks) => (
|
green: (chunks) => (
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<span className="relative z-10 text-accent italic">{chunks}</span>
|
<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>
|
</span>
|
||||||
)
|
),
|
||||||
})}
|
})}
|
||||||
</Heading>
|
</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">
|
<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')}
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-4 md:gap-6">
|
<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')}
|
{t('viewProducts')}
|
||||||
<span className="ml-3 transition-transform group-hover:translate-y-1">↓</span>
|
<span className="ml-3 transition-transform group-hover:translate-y-1">↓</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -123,8 +137,8 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
|||||||
<Link key={idx} href={category.href} className="group block">
|
<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]">
|
<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">
|
<div className="relative h-[200px] md:h-[400px] overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={category.img}
|
src={category.img}
|
||||||
alt={category.title}
|
alt={category.title}
|
||||||
fill
|
fill
|
||||||
className="object-cover transition-transform duration-1000 group-hover:scale-105"
|
className="object-cover transition-transform duration-1000 group-hover:scale-105"
|
||||||
@@ -132,13 +146,22 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
|||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 image-overlay-gradient opacity-80 group-hover:opacity-90 transition-opacity duration-500" />
|
<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">
|
<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>
|
||||||
|
|
||||||
<div className="absolute bottom-4 left-4 md:bottom-10 md:left-10 right-4 md:right-10">
|
<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')}
|
{t('categoryLabel')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<h2 className="text-xl md:text-4xl font-bold text-white leading-tight transition-transform duration-500 group-hover:translate-x-1">
|
<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')}
|
{t('viewProducts')}
|
||||||
</span>
|
</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">
|
<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">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +201,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Technical Support CTA */}
|
{/* Technical Support CTA */}
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<Section className="bg-white py-12 md:py-28">
|
<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="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="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">
|
<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">
|
<p className="text-base md:text-xl text-white/70 leading-relaxed">
|
||||||
{t('cta.description')}
|
{t('cta.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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')}
|
{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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ export async function generateMetadata({ params: { locale } }: TeamPageProps): P
|
|||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/${locale}/team`,
|
canonical: `/${locale}/team`,
|
||||||
languages: {
|
languages: {
|
||||||
'de': '/de/team',
|
de: '/de/team',
|
||||||
'en': '/en/team',
|
en: '/en/team',
|
||||||
'x-default': '/en/team',
|
'x-default': '/en/team',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${title} | KLZ Cables`,
|
title: `${title} | KLZ Cables`,
|
||||||
description,
|
description,
|
||||||
url: `https://klz-cables.com/${locale}/team`,
|
url: `${SITE_URL}/${locale}/team`,
|
||||||
images: getOGImageMetadata('team', title, locale),
|
images: getOGImageMetadata('team', title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -50,9 +50,7 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
<div className="flex flex-col min-h-screen bg-neutral-light">
|
<div className="flex flex-col min-h-screen bg-neutral-light">
|
||||||
<JsonLd
|
<JsonLd
|
||||||
id="breadcrumb-team"
|
id="breadcrumb-team"
|
||||||
data={getBreadcrumbSchema([
|
data={getBreadcrumbSchema([{ name: t('hero.subtitle'), item: `/team` }])}
|
||||||
{ name: t('hero.subtitle'), item: `/team` },
|
|
||||||
])}
|
|
||||||
/>
|
/>
|
||||||
<JsonLd
|
<JsonLd
|
||||||
id="person-michael"
|
id="person-michael"
|
||||||
@@ -65,10 +63,8 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
name: 'KLZ Cables',
|
name: 'KLZ Cables',
|
||||||
},
|
},
|
||||||
sameAs: [
|
sameAs: ['https://www.linkedin.com/in/michael-bodemer-33b493122/'],
|
||||||
'https://www.linkedin.com/in/michael-bodemer-33b493122/'
|
image: `${SITE_URL}/uploads/2024/12/DSC07768-Large.webp`,
|
||||||
],
|
|
||||||
image: `${SITE_URL}/uploads/2024/12/DSC07768-Large.webp`
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<JsonLd
|
<JsonLd
|
||||||
@@ -82,10 +78,8 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
name: 'KLZ Cables',
|
name: 'KLZ Cables',
|
||||||
},
|
},
|
||||||
sameAs: [
|
sameAs: ['https://www.linkedin.com/in/klaus-mintel-b80a8b193/'],
|
||||||
'https://www.linkedin.com/in/klaus-mintel-b80a8b193/'
|
image: `${SITE_URL}/uploads/2024/12/DSC07963-Large.webp`,
|
||||||
],
|
|
||||||
image: `${SITE_URL}/uploads/2024/12/DSC07963-Large.webp`
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
@@ -101,9 +95,11 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
|
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container className="relative z-10 text-center text-white max-w-5xl">
|
<Container className="relative z-10 text-center text-white max-w-5xl">
|
||||||
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{t('hero.badge')}</Badge>
|
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
|
||||||
|
{t('hero.badge')}
|
||||||
|
</Badge>
|
||||||
<Heading level={1} className="text-white mb-4 md:mb-8">
|
<Heading level={1} className="text-white mb-4 md:mb-8">
|
||||||
{t('hero.subtitle')}
|
{t('hero.subtitle')}
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -120,7 +116,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-primary-dark text-white relative order-2 lg:order-1">
|
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-primary-dark text-white relative order-2 lg:order-1">
|
||||||
<div className="absolute top-0 right-0 w-32 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
|
<div className="absolute top-0 right-0 w-32 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<Badge variant="accent" className="mb-4 md:mb-8">{t('michael.role')}</Badge>
|
<Badge variant="accent" className="mb-4 md:mb-8">
|
||||||
|
{t('michael.role')}
|
||||||
|
</Badge>
|
||||||
<Heading level={2} className="text-white mb-6 md:mb-10 text-3xl md:text-5xl">
|
<Heading level={2} className="text-white mb-6 md:mb-10 text-3xl md:text-5xl">
|
||||||
<span className="text-white">{t('michael.name')}</span>
|
<span className="text-white">{t('michael.name')}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -133,9 +131,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
<p className="text-base md:text-xl leading-relaxed text-white/70 mb-6 md:mb-12 max-w-xl">
|
<p className="text-base md:text-xl leading-relaxed text-white/70 mb-6 md:mb-12 max-w-xl">
|
||||||
{t('michael.description')}
|
{t('michael.description')}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
href="https://www.linkedin.com/in/michael-bodemer-33b493122/"
|
href="https://www.linkedin.com/in/michael-bodemer-33b493122/"
|
||||||
variant="accent"
|
variant="accent"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
|
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
|
||||||
>
|
>
|
||||||
@@ -173,26 +171,36 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
<Container className="relative z-10">
|
<Container className="relative z-10">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
|
||||||
<div className="lg:col-span-6">
|
<div className="lg:col-span-6">
|
||||||
<Heading level={2} subtitle={t('legacy.subtitle')} className="text-white mb-6 md:mb-10">
|
<Heading
|
||||||
|
level={2}
|
||||||
|
subtitle={t('legacy.subtitle')}
|
||||||
|
className="text-white mb-6 md:mb-10"
|
||||||
|
>
|
||||||
<span className="text-white">{t('legacy.title')}</span>
|
<span className="text-white">{t('legacy.title')}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="space-y-4 md:space-y-8 text-base md:text-2xl text-white/80 leading-relaxed font-medium">
|
<div className="space-y-4 md:space-y-8 text-base md:text-2xl text-white/80 leading-relaxed font-medium">
|
||||||
<p className="border-l-4 border-accent pl-5 md:pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl md:rounded-r-2xl">
|
<p className="border-l-4 border-accent pl-5 md:pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl md:rounded-r-2xl">
|
||||||
{t('legacy.p1')}
|
{t('legacy.p1')}
|
||||||
</p>
|
</p>
|
||||||
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">
|
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">{t('legacy.p2')}</p>
|
||||||
{t('legacy.p2')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:col-span-6 grid grid-cols-2 md:grid-cols-2 gap-3 md:gap-6">
|
<div className="lg:col-span-6 grid grid-cols-2 md:grid-cols-2 gap-3 md:gap-6">
|
||||||
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
|
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
|
||||||
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.expertise')}</div>
|
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
|
||||||
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.expertiseDesc')}</div>
|
{t('legacy.expertise')}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
|
||||||
|
{t('legacy.expertiseDesc')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
|
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
|
||||||
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.network')}</div>
|
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
|
||||||
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.networkDesc')}</div>
|
{t('legacy.network')}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
|
||||||
|
{t('legacy.networkDesc')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +224,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-neutral-light text-saturated relative order-2">
|
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-neutral-light text-saturated relative order-2">
|
||||||
<div className="absolute top-0 left-0 w-32 h-full bg-saturated/5 skew-x-12 -translate-x-1/2" />
|
<div className="absolute top-0 left-0 w-32 h-full bg-saturated/5 skew-x-12 -translate-x-1/2" />
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<Badge variant="saturated" className="mb-4 md:mb-8">{t('klaus.role')}</Badge>
|
<Badge variant="saturated" className="mb-4 md:mb-8">
|
||||||
|
{t('klaus.role')}
|
||||||
|
</Badge>
|
||||||
<Heading level={2} className="text-saturated mb-6 md:mb-10 text-3xl md:text-6xl">
|
<Heading level={2} className="text-saturated mb-6 md:mb-10 text-3xl md:text-6xl">
|
||||||
{t('klaus.name')}
|
{t('klaus.name')}
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -229,9 +239,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
<p className="text-base md:text-xl leading-relaxed text-text-secondary mb-6 md:mb-12 max-w-xl">
|
<p className="text-base md:text-xl leading-relaxed text-text-secondary mb-6 md:mb-12 max-w-xl">
|
||||||
{t('klaus.description')}
|
{t('klaus.description')}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
href="https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
|
href="https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
|
||||||
variant="saturated"
|
variant="saturated"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
|
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
|
||||||
>
|
>
|
||||||
@@ -255,11 +265,14 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
<p className="text-base md:text-xl text-text-secondary leading-relaxed">
|
<p className="text-base md:text-xl text-text-secondary leading-relaxed">
|
||||||
{t('manifesto.tagline')}
|
{t('manifesto.tagline')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Mobile-only progress indicator */}
|
{/* Mobile-only progress indicator */}
|
||||||
<div className="flex lg:hidden mt-8 gap-2">
|
<div className="flex lg:hidden mt-8 gap-2">
|
||||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||||
<div key={i} className="h-1.5 flex-1 bg-neutral-medium rounded-full overflow-hidden">
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-1.5 flex-1 bg-neutral-medium rounded-full overflow-hidden"
|
||||||
|
>
|
||||||
<div className="h-full bg-accent w-0 group-active:w-full transition-all duration-500" />
|
<div className="h-full bg-accent w-0 group-active:w-full transition-all duration-500" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -268,12 +281,21 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-10">
|
<div className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-10">
|
||||||
{[0, 1, 2, 3, 4, 5].map((idx) => (
|
{[0, 1, 2, 3, 4, 5].map((idx) => (
|
||||||
<div key={idx} className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98] touch-target-none">
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98] touch-target-none"
|
||||||
|
>
|
||||||
<div className="w-10 h-10 md:w-16 md:h-16 bg-white rounded-xl md:rounded-2xl flex items-center justify-center mb-4 md:mb-8 shadow-sm group-hover:bg-accent transition-colors duration-500">
|
<div className="w-10 h-10 md:w-16 md:h-16 bg-white rounded-xl md:rounded-2xl flex items-center justify-center mb-4 md:mb-8 shadow-sm group-hover:bg-accent transition-colors duration-500">
|
||||||
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">0{idx + 1}</span>
|
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">
|
||||||
|
0{idx + 1}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">{t(`manifesto.items.${idx}.title`)}</h3>
|
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">
|
||||||
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">{t(`manifesto.items.${idx}.description`)}</p>
|
{t(`manifesto.items.${idx}.title`)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">
|
||||||
|
{t(`manifesto.items.${idx}.description`)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { config } from '@/lib/config';
|
||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
const baseUrl = config.baseUrl || 'https://klz-cables.com';
|
||||||
return {
|
return {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
@@ -11,8 +13,8 @@ export default function robots(): MetadataRoute.Robots {
|
|||||||
{
|
{
|
||||||
userAgent: ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'Google-Extended', 'OAI-SearchBot'],
|
userAgent: ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'Google-Extended', 'OAI-SearchBot'],
|
||||||
allow: '/',
|
allow: '/',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
sitemap: 'https://klz-cables.com/sitemap.xml',
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { config } from '@/lib/config';
|
||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
import { getAllProducts } from '@/lib/mdx';
|
import { getAllProducts } from '@/lib/mdx';
|
||||||
import { getAllPosts } from '@/lib/blog';
|
import { getAllPosts } from '@/lib/blog';
|
||||||
import { getAllPages } from '@/lib/pages';
|
import { getAllPages } from '@/lib/pages';
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
const baseUrl = 'https://klz-cables.com';
|
const baseUrl = config.baseUrl || 'https://klz-cables.com';
|
||||||
const locales = ['de', 'en'];
|
const locales = ['de', 'en'];
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -38,7 +39,8 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
// We need to find the category for the product to build the URL
|
// We need to find the category for the product to build the URL
|
||||||
// In this project, products are under /products/[category]/[slug]
|
// In this project, products are under /products/[category]/[slug]
|
||||||
// The category is in product.frontmatter.categories
|
// The category is in product.frontmatter.categories
|
||||||
const category = product.frontmatter.categories[0]?.toLowerCase().replace(/\s+/g, '-') || 'other';
|
const category =
|
||||||
|
product.frontmatter.categories[0]?.toLowerCase().replace(/\s+/g, '-') || 'other';
|
||||||
sitemapEntries.push({
|
sitemapEntries.push({
|
||||||
url: `${baseUrl}/${locale}/products/${category}/${product.slug}`,
|
url: `${baseUrl}/${locale}/products/${category}/${product.slug}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { config } from './config';
|
||||||
|
|
||||||
export const SITE_URL = 'https://klz-cables.com';
|
export const SITE_URL = config.baseUrl || 'https://klz-cables.com';
|
||||||
export const LOGO_URL = `${SITE_URL}/logo.png`;
|
export const LOGO_URL = `${SITE_URL}/logo.png`;
|
||||||
|
|
||||||
export const getOrganizationSchema = () => ({
|
export const getOrganizationSchema = () => ({
|
||||||
@@ -8,16 +9,14 @@ export const getOrganizationSchema = () => ({
|
|||||||
name: 'KLZ Cables',
|
name: 'KLZ Cables',
|
||||||
url: SITE_URL,
|
url: SITE_URL,
|
||||||
logo: LOGO_URL,
|
logo: LOGO_URL,
|
||||||
sameAs: [
|
sameAs: ['https://www.linkedin.com/company/klz-cables'],
|
||||||
'https://www.linkedin.com/company/klz-cables',
|
|
||||||
],
|
|
||||||
contactPoint: {
|
contactPoint: {
|
||||||
'@type': 'ContactPoint' as const,
|
'@type': 'ContactPoint' as const,
|
||||||
telephone: '+49-881-92537298',
|
telephone: '+49-881-92537298',
|
||||||
contactType: 'customer service' as const,
|
contactType: 'customer service' as const,
|
||||||
email: 'info@klz-cables.com',
|
email: 'info@klz-cables.com',
|
||||||
availableLanguage: ['German', 'English']
|
availableLanguage: ['German', 'English'],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getBreadcrumbSchema = (items: { name: string; item: string }[]) => ({
|
export const getBreadcrumbSchema = (items: { name: string; item: string }[]) => ({
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ async function main() {
|
|||||||
.map((i, el) => $(el).text())
|
.map((i, el) => $(el).text())
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
// Cleanup and filter
|
// Cleanup, filter and normalize domains to targetUrl
|
||||||
urls = [...new Set(urls)].filter((u) => u.startsWith('http')).sort();
|
const urlPattern = /https?:\/\/[^\/]+/;
|
||||||
|
urls = [...new Set(urls)]
|
||||||
|
.filter((u) => u.startsWith('http'))
|
||||||
|
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||||
|
.sort();
|
||||||
|
|
||||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user