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

This commit is contained in:
2026-02-02 12:10:09 +01:00
parent b25fdd877a
commit 42b06e1ef8
13 changed files with 616 additions and 316 deletions

View File

@@ -2,10 +2,15 @@ import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/blog';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { SITE_URL } from '@/lib/schema';
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);
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.
// For now, let's just make sure it's absolute.
const featuredImage = post.frontmatter.featuredImage
? (post.frontmatter.featuredImage.startsWith('http')
? post.frontmatter.featuredImage.startsWith('http')
? post.frontmatter.featuredImage
: `https://klz-cables.com${post.frontmatter.featuredImage}`)
: `${SITE_URL}${post.frontmatter.featuredImage}`
: undefined;
return new ImageResponse(
(
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
/>
),
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
/>,
{
...OG_IMAGE_SIZE,
fonts,
}
},
);
}

View File

@@ -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);
if (!post) return {};
const description = post.frontmatter.excerpt || '';
@@ -32,8 +34,8 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
alternates: {
canonical: `/${locale}/blog/${slug}`,
languages: {
'de': `/de/blog/${slug}`,
'en': `/en/blog/${slug}`,
de: `/de/blog/${slug}`,
en: `/en/blog/${slug}`,
'x-default': `/en/blog/${slug}`,
},
},
@@ -43,7 +45,7 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
type: 'article',
publishedTime: post.frontmatter.date,
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),
},
twitter: {
@@ -66,16 +68,15 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
return (
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
{/* Featured Image Header */}
{post.frontmatter.featuredImage ? (
<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"
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
/>
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
{/* Title overlay on image */}
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
<div className="container mx-auto px-4">
@@ -87,7 +88,10 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</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}
</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]">
@@ -95,7 +99,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})}
</time>
<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, {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})}
</time>
<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`}
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
<svg
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>
{locale === 'de' ? 'Zurück zur Übersicht' : 'Back to Overview'}
</Link>
@@ -188,57 +202,63 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
{/* Structured Data */}
<JsonLd
id={`jsonld-${slug}`}
data={{
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.frontmatter.title,
datePublished: post.frontmatter.date,
dateModified: post.frontmatter.date,
image: post.frontmatter.featuredImage ? `https://klz-cables.com${post.frontmatter.featuredImage}` : undefined,
author: {
'@type': 'Organization',
name: 'KLZ Cables',
url: 'https://klz-cables.com',
logo: 'https://klz-cables.com/logo-blue.svg'
},
publisher: {
'@type': 'Organization',
name: 'KLZ Cables',
logo: {
'@type': 'ImageObject',
url: 'https://klz-cables.com/logo-blue.svg',
data={
{
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.frontmatter.title,
datePublished: post.frontmatter.date,
dateModified: post.frontmatter.date,
image: post.frontmatter.featuredImage
? `${SITE_URL}${post.frontmatter.featuredImage}`
: undefined,
author: {
'@type': 'Organization',
name: 'KLZ Cables',
url: SITE_URL,
logo: `${SITE_URL}/logo-blue.svg`,
},
},
description: post.frontmatter.excerpt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://klz-cables.com/${locale}/blog/${slug}`,
},
articleSection: post.frontmatter.category,
wordCount: post.content.split(/\s+/).length,
timeRequired: `PT${getReadingTime(post.content)}M`
} as any}
publisher: {
'@type': 'Organization',
name: 'KLZ Cables',
logo: {
'@type': 'ImageObject',
url: `${SITE_URL}/logo-blue.svg`,
},
},
description: post.frontmatter.excerpt,
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
id={`breadcrumb-${slug}`}
data={{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Blog',
item: `https://klz-cables.com/${locale}/blog`,
},
{
'@type': 'ListItem',
position: 2,
name: post.frontmatter.title,
item: `https://klz-cables.com/${locale}/blog/${slug}`,
},
],
} as any}
data={
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Blog',
item: `${SITE_URL}/${locale}/blog`,
},
{
'@type': 'ListItem',
position: 2,
name: post.frontmatter.title,
item: `${SITE_URL}/${locale}/blog/${slug}`,
},
],
} as any
}
/>
</article>
);

View File

@@ -4,6 +4,7 @@ import { Section, Container, Heading, Card, Badge, Button } from '@/components/u
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { getOGImageMetadata } from '@/lib/metadata';
import { SITE_URL } from '@/lib/schema';
interface BlogIndexProps {
params: {
@@ -19,15 +20,15 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
alternates: {
canonical: `/${locale}/blog`,
languages: {
'de': '/de/blog',
'en': '/en/blog',
de: '/de/blog',
en: '/en/blog',
'x-default': '/en/blog',
},
},
openGraph: {
title: `${t('title')} | KLZ Cables`,
description: t('description'),
url: `https://klz-cables.com/${locale}/blog`,
url: `${SITE_URL}/${locale}/blog`,
images: getOGImageMetadata('blog', t('title'), locale),
},
twitter: {
@@ -41,10 +42,10 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
export default async function BlogIndex({ params: { locale } }: BlogIndexProps) {
const t = await getTranslations('Blog');
const posts = await getAllPosts(locale);
// Sort posts by date descending
const sortedPosts = [...posts].sort((a, b) =>
new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
const sortedPosts = [...posts].sort(
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
);
const featuredPost = sortedPosts[0];
@@ -65,10 +66,12 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
<div className="absolute inset-0 image-overlay-gradient" />
</>
)}
<Container className="relative z-10">
<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 && (
<>
<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">
{featuredPost.frontmatter.excerpt}
</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')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
<span className="ml-3 transition-transform group-hover:translate-x-2">
&rarr;
</span>
</Button>
</>
)}
@@ -97,10 +107,30 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
</Heading>
<div className="flex flex-wrap gap-2 md:gap-4">
{/* 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 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>
<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
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>
</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" />
{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}
</Badge>
)}
@@ -131,7 +164,7 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})}
</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">
@@ -145,8 +178,18 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
{t('readMore')}
</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">
<svg className="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
<svg
className="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</div>
</div>
@@ -156,13 +199,21 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
</Reveal>
))}
</div>
{/* Pagination Placeholder */}
<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="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>
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base" disabled>
{t('prev')}
</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>
</Container>
</Section>