Merge remote-tracking branch 'origin/main' into feature/ai-search
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 2m8s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Failing after 4m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s

# Conflicts:
#	.env
#	components/Header.tsx
#	components/home/Hero.tsx
This commit is contained in:
2026-03-04 10:56:41 +01:00
37 changed files with 1143 additions and 382 deletions

View File

@@ -8,6 +8,20 @@ export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
export const runtime = 'nodejs';
async function fetchImageAsBase64(url: string) {
try {
const res = await fetch(url);
if (!res.ok) return undefined;
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const contentType = res.headers.get('content-type') || 'image/jpeg';
return `data:${contentType};base64,${buffer.toString('base64')}`;
} catch (error) {
console.error('Failed to fetch OG image:', url, error);
return undefined;
}
}
export default async function Image({
params,
}: {
@@ -32,12 +46,19 @@ export default async function Image({
: `${SITE_URL}${post.frontmatter.featuredImage}`
: undefined;
// Fetch image explicitly and convert to base64 because Satori sometimes struggles
// fetching remote URLs directly inside ImageResponse correctly in various environments.
let base64Image: string | undefined = undefined;
if (featuredImage) {
base64Image = await fetchImageAsBase64(featuredImage);
}
return new ImageResponse(
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
image={base64Image || featuredImage}
/>,
{
...OG_IMAGE_SIZE,

View File

@@ -1,12 +1,18 @@
import { notFound, redirect } from 'next/navigation';
import JsonLd from '@/components/JsonLd';
import { SITE_URL } from '@/lib/schema';
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
import {
getPostBySlug,
getAdjacentPosts,
getReadingTime,
extractLexicalHeadings,
} from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
import { Heading } from '@/components/ui';
import { setRequestLocale } from 'next-intl/server';
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
@@ -67,6 +73,10 @@ export default async function BlogPost({ params }: BlogPostProps) {
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(post.slug, locale);
// Convert Lexical content into a plain string to estimate reading time roughly
// Extract headings for TOC
const headings = extractLexicalHeadings(post.content?.root || post.content);
// Convert Lexical content into a plain string to estimate reading time roughly
const rawTextContent = JSON.stringify(post.content);
@@ -88,6 +98,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
alt={post.frontmatter.title}
fill
priority
quality={100}
className="object-cover"
sizes="100vw"
style={{
@@ -113,7 +124,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
</Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium">
<time dateTime={post.frontmatter.date} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', {
year: 'numeric',
month: 'long',
day: 'numeric',
@@ -150,7 +161,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
</Heading>
<div className="flex items-center gap-6 text-text-primary/80 font-medium">
<time dateTime={post.frontmatter.date} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', {
year: 'numeric',
month: 'long',
day: 'numeric',
@@ -231,10 +242,10 @@ export default async function BlogPost({ params }: BlogPostProps) {
</div>
</div>
{/* Right Column: Sticky Sidebar - Temporarily Hidden without ToC */}
{/* Right Column: Sticky Sidebar - TOC */}
<aside className="sticky-narrative-sidebar hidden lg:block">
<div className="space-y-12">
{/* Future Payload Table of Contents Implementation */}
<div className="space-y-12 lg:sticky lg:top-32">
<TableOfContents headings={headings} locale={locale} />
</div>
</aside>
</div>

View File

@@ -198,7 +198,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
<div className="flex items-center gap-3 text-xs md:text-sm font-bold text-white/80 mb-3 tracking-widest uppercase">
<time dateTime={post.frontmatter.date} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', {
year: 'numeric',
month: 'long',
day: 'numeric',

View File

@@ -8,6 +8,7 @@ import { SITE_URL } from '@/lib/schema';
import { getOGImageMetadata } from '@/lib/metadata';
import { Suspense } from 'react';
import ContactMap from '@/components/ContactMap';
import ObfuscatedEmail from '@/components/ObfuscatedEmail';
interface ContactPageProps {
params: Promise<{
@@ -204,12 +205,10 @@ export default async function ContactPage({ params }: ContactPageProps) {
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">
{t('info.email')}
</h4>
<a
href="mailto:info@klz-cables.com"
<ObfuscatedEmail
email="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>
</address>

View File

@@ -322,6 +322,8 @@ export default async function ProductPage({ params }: ProductPageProps) {
}
}
console.log(`[DEBUG PAGE] Slug: ${productSlug}, children count: ${descriptionChildren.length}`);
const descriptionContent = {
root: {
...product.content.root,
@@ -353,29 +355,31 @@ export default async function ProductPage({ params }: ProductPageProps) {
categories={product.frontmatter.categories}
sku={product.frontmatter.sku}
/>
<section className="relative pt-40 pb-24 overflow-hidden bg-primary-dark">
<section className="relative pt-28 md:pt-40 pb-12 md:pb-24 overflow-hidden bg-primary-dark">
{/* Background Decorative Elements */}
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/5 to-transparent pointer-events-none" />
<div className="absolute -top-24 -right-24 w-96 h-96 bg-accent/10 rounded-full blur-3xl pointer-events-none" />
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
<nav className="flex flex-wrap items-center gap-y-1 mb-6 md:mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
<Link
href={`/${locale}/${productsSlug}`}
className="hover:text-accent transition-colors"
className="hover:text-accent transition-colors shrink-0"
>
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
</Link>
<span className="mx-4 opacity-20">/</span>
<span className="mx-2 md:mx-4 opacity-20">/</span>
<Link
href={`/${locale}/${productsSlug}/${categorySlug}`}
className="hover:text-accent transition-colors"
className="hover:text-accent transition-colors shrink-0 max-w-[140px] truncate"
>
{categoryTitle}
</Link>
<span className="mx-4 opacity-20">/</span>
<span className="text-white/90">{product.frontmatter.title}</span>
<span className="mx-2 md:mx-4 opacity-20">/</span>
<span className="text-white/90 truncate max-w-[140px] md:max-w-none">
{product.frontmatter.title}
</span>
</nav>
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
@@ -386,7 +390,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
{t('englishVersion')}
</div>
)}
<div className="flex flex-wrap gap-3 mb-8">
<div className="flex flex-wrap gap-2 mb-4 md:mb-8">
{product.frontmatter.categories.map((cat, idx) => (
<Badge
key={idx}
@@ -397,10 +401,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
</Badge>
))}
</div>
<Heading level={1} className="text-white mb-8 uppercase">
<Heading level={1} className="text-white mb-4 md:mb-8 uppercase">
{product.frontmatter.title}
</Heading>
<p className="text-xl md:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
<p className="text-base md:text-xl lg:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
{product.frontmatter.description}
</p>
</div>
@@ -414,11 +418,11 @@ export default async function ProductPage({ params }: ProductPageProps) {
{/* Large Product Image Section */}
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
<div
className="relative -mt-32 mb-32 animate-slide-up"
className="relative md:-mt-32 mb-8 md:mb-32 animate-slide-up"
style={{ animationDelay: '200ms' }}
>
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[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="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[24px] md:rounded-[48px] border border-neutral-dark/5 overflow-hidden p-6 md:p-20 lg:p-24">
<div className="relative w-full aspect-[4/3] md:aspect-[21/9]">
<Image
src={product.frontmatter.images[0]}
alt={product.frontmatter.title}
@@ -453,10 +457,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-20">
{/* Description Area Next to Sidebar */}
<div className="lg:col-span-8">
<div className="max-w-none prose prose-primary prose-lg md:prose-xl mb-16 pb-16 border-b border-neutral-dark/5">
<div className="max-w-none prose prose-primary prose-base md:prose-lg xl:prose-xl mb-8 md:mb-16 pb-8 md:pb-16 border-b border-neutral-dark/5">
{descriptionChildren.length > 0 ? (
<PayloadRichText data={descriptionContent} />
) : product.frontmatter.description ? (
@@ -464,6 +468,12 @@ export default async function ProductPage({ params }: ProductPageProps) {
{product.frontmatter.description}
</p>
) : null}
{product.application?.root?.children?.length > 0 && (
<div className="mt-12">
<PayloadRichText data={product.application} />
</div>
)}
</div>
</div>
@@ -472,7 +482,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
</div>
{/* Full-width Technical Data Below */}
<div className="mt-16 pt-16 border-t-0">
<div className="mt-8 md:mt-16 pt-8 md:pt-16 border-t-0">
<div className="max-w-none prose prose-primary prose-lg md:prose-xl">
<PayloadRichText data={technicalContent} />
</div>
@@ -530,7 +540,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
</div>
{/* Related Products Section */}
<div className="mt-16 pt-16 border-t border-neutral-dark/5">
<div className="mt-10 md:mt-16 pt-10 md:pt-16 border-t border-neutral-dark/5">
<RelatedProducts
currentSlug={productSlug}
categories={product.frontmatter.categories}

View File

@@ -1,5 +1,4 @@
import Reveal from '@/components/Reveal';
import Scribble from '@/components/Scribble';
import { Badge, Button, Card, Container, Heading, Section } from '@/components/ui';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
@@ -95,7 +94,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
return (
<div className="flex flex-col min-h-screen bg-neutral-light">
{/* Hero Section */}
<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 flex items-center pt-32 pb-16 md:pt-40 md:pb-24 overflow-hidden bg-primary-dark">
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge
@@ -106,15 +105,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
</Badge>
<Heading level={1} className="text-white mb-4 md:mb-8">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
<span className="relative z-10 text-accent italic">{chunks}</span>
<Scribble
variant="circle"
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block"
/>
</span>
),
green: (chunks) => <span className="text-accent italic">{chunks}</span>,
})}
</Heading>
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
@@ -223,7 +214,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<div className="absolute top-0 right-0 w-1/2 h-full bg-accent/5 -skew-x-12 translate-x-1/4" />
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-6 md:gap-12">
<div className="max-w-2xl text-center lg:text-left">
<h2 className="text-2xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-8 tracking-tight">
<h2 className="text-2xl md:text-4xl 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">

View File

@@ -122,12 +122,12 @@ export default async function TeamPage({ params }: TeamPageProps) {
<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-2xl md:text-4xl">
<span className="text-white">{t('michael.name')}</span>
</Heading>
<div className="relative mb-6 md:mb-12">
<div className="absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 bg-accent rounded-full" />
<p className="text-lg md:text-2xl font-bold italic leading-relaxed pl-5 md:pl-8 text-white/90">
<p className="text-base md:text-xl font-bold italic leading-relaxed pl-5 md:pl-8 text-white/90">
{t('michael.quote')}
</p>
</div>
@@ -156,6 +156,7 @@ export default async function TeamPage({ params }: TeamPageProps) {
alt={t('michael.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
quality={100}
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-primary-dark/60 lg:bg-gradient-to-r lg:from-primary-dark/20 to-transparent" />
@@ -225,6 +226,7 @@ export default async function TeamPage({ params }: TeamPageProps) {
alt={t('klaus.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
quality={100}
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-white/60 lg:bg-gradient-to-l lg:from-primary-dark/20 to-transparent" />
@@ -235,12 +237,12 @@ export default async function TeamPage({ params }: TeamPageProps) {
<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-2xl md:text-4xl">
{t('klaus.name')}
</Heading>
<div className="relative mb-6 md:mb-12">
<div className="absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 bg-saturated rounded-full" />
<p className="text-lg md:text-3xl font-bold italic leading-relaxed pl-5 md:pl-8 text-text-secondary">
<p className="text-base md:text-xl font-bold italic leading-relaxed pl-5 md:pl-8 text-text-secondary">
{t('klaus.quote')}
</p>
</div>

View File

@@ -72,6 +72,7 @@ export async function sendContactFormAction(formData: FormData) {
? `Product Inquiry: ${productName}`
: 'New Contact Form Submission';
const confirmationSubject = 'Thank you for your inquiry';
const isTestSubmission = email === 'testing@mintel.me';
try {
// 2a. Send notification to Mintel/Client
@@ -84,26 +85,30 @@ export async function sendContactFormAction(formData: FormData) {
}),
);
const notificationResult = await sendEmail({
replyTo: email,
subject: notificationSubject,
html: notificationHtml,
});
if (notificationResult.success) {
logger.info('Notification email sent successfully', {
messageId: notificationResult.messageId,
});
} else {
logger.error('Notification email FAILED', {
error: notificationResult.error,
if (!isTestSubmission) {
const notificationResult = await sendEmail({
replyTo: email,
subject: notificationSubject,
email,
html: notificationHtml,
});
services.errors.captureException(
new Error(`Notification email failed: ${notificationResult.error}`),
{ action: 'sendContactFormAction_notification', email },
);
if (notificationResult.success) {
logger.info('Notification email sent successfully', {
messageId: notificationResult.messageId,
});
} else {
logger.error('Notification email FAILED', {
error: notificationResult.error,
subject: notificationSubject,
email,
});
services.errors.captureException(
new Error(`Notification email failed: ${notificationResult.error}`),
{ action: 'sendContactFormAction_notification', email },
);
}
} else {
logger.info('Skipping notification email for test submission', { email });
}
// 2b. Send confirmation to Customer (branded as KLZ Cables)
@@ -115,26 +120,30 @@ export async function sendContactFormAction(formData: FormData) {
}),
);
const confirmationResult = await sendEmail({
to: email,
subject: confirmationSubject,
html: confirmationHtml,
});
if (confirmationResult.success) {
logger.info('Confirmation email sent successfully', {
messageId: confirmationResult.messageId,
});
} else {
logger.error('Confirmation email FAILED', {
error: confirmationResult.error,
subject: confirmationSubject,
if (!isTestSubmission) {
const confirmationResult = await sendEmail({
to: email,
subject: confirmationSubject,
html: confirmationHtml,
});
services.errors.captureException(
new Error(`Confirmation email failed: ${confirmationResult.error}`),
{ action: 'sendContactFormAction_confirmation', email },
);
if (confirmationResult.success) {
logger.info('Confirmation email sent successfully', {
messageId: confirmationResult.messageId,
});
} else {
logger.error('Confirmation email FAILED', {
error: confirmationResult.error,
subject: confirmationSubject,
to: email,
});
services.errors.captureException(
new Error(`Confirmation email failed: ${confirmationResult.error}`),
{ action: 'sendContactFormAction_confirmation', email },
);
}
} else {
logger.info('Skipping confirmation email for test submission', { email });
}
// Notify via Gotify (Internal)