feat: Add professional portfolio images and Hero redesign

- Add 15 optimized WebP images for portfolio (21-53KB each)
- Redesign Hero section with full-width background image
- Add SSR support for Hero component
- Update About section with new portrait image
- Update Contact page with professional headshot
- Add images to Services/Leistungen page
- Add image optimization script (sharp)
- Update translations for gallery section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 23:33:28 +01:00
co-authored by Claude Opus 4.5
parent b1ec7b4d61
commit 87c7ebc5e3
75 changed files with 10271 additions and 1314 deletions
+56 -20
View File
@@ -2,8 +2,10 @@ import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google';
import { locales, type Locale } from '@/i18n/config';
import { locales, localeMetadata, seoKeywords, type Locale } from '@/i18n/config';
import type { Metadata } from 'next';
import { Header, Footer, GlobalBackground } from '@/components/layout';
import { PersonJsonLd, ProfessionalServiceJsonLd, OrganizationJsonLd, WebSiteJsonLd } from '@/components/seo';
import '../globals.css';
const inter = Inter({
@@ -23,43 +25,57 @@ export function generateStaticParams() {
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const currentLocale = locale as Locale;
const meta = localeMetadata[currentLocale] || localeMetadata.de;
const keywords = seoKeywords[currentLocale] || seoKeywords.de;
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const titles: Record<Locale, string> = {
de: 'Damjan Savic | AI & Automation Specialist aus Koeln',
en: 'Damjan Savic | AI & Automation Specialist from Cologne',
sr: 'Damjan Savic | AI & Automation Specialist iz Kelna',
de: 'Damjan Savić | AI & Automation Specialist aus Köln',
en: 'Damjan Savić | AI & Automation Specialist from Cologne',
sr: 'Damjan Savić | AI & Automation Specialist iz Kelna',
};
const descriptions: Record<Locale, string> = {
de: 'Entwicklung von KI-Agenten und Automatisierungsloesungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.',
de: 'Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.',
en: 'Building AI agents and automation solutions. From voice AI platforms to autonomous web agents.',
sr: 'Razvoj AI agenata i resenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.',
};
// Generate alternates using localeMetadata
const languageAlternates: Record<string, string> = {
'x-default': `${BASE_URL}/de`,
};
locales.forEach((loc) => {
languageAlternates[loc] = `${BASE_URL}/${loc}`;
});
// Generate OG alternate locales
const allOgLocales = locales.map((loc) => localeMetadata[loc].ogLocale);
const alternateOgLocales = allOgLocales.filter((l) => l !== meta.ogLocale);
return {
title: {
template: '%s | Damjan Savic',
default: titles[locale as Locale] || titles.de,
template: '%s | Damjan Savić',
default: titles[currentLocale] || titles.de,
},
description: descriptions[locale as Locale] || descriptions.de,
description: descriptions[currentLocale] || descriptions.de,
keywords: keywords,
alternates: {
canonical: `/${locale}`,
languages: {
de: '/de',
en: '/en',
sr: '/sr',
},
canonical: `${BASE_URL}/${locale}`,
languages: languageAlternates,
},
openGraph: {
type: 'website',
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
siteName: 'Damjan Savic',
locale: meta.ogLocale,
alternateLocale: alternateOgLocales,
siteName: 'Damjan Savić',
images: [
{
url: '/images/og-image.jpg',
width: 1200,
height: 630,
alt: 'Damjan Savic - AI & Automation Specialist',
alt: locale === 'de' ? 'Damjan Savić - KI & Automatisierung Spezialist' : locale === 'sr' ? 'Damjan Savić - AI & Automatizacija Specijalista' : 'Damjan Savić - AI & Automation Specialist',
},
],
},
@@ -82,10 +98,30 @@ export default async function LocaleLayout({ children, params }: Props) {
const messages = await getMessages();
return (
<html lang={locale} className={inter.variable}>
<body className="bg-zinc-900 text-zinc-50 antialiased">
<html lang={locale} className={`${inter.variable} dark`}>
<head>
{/* Preconnect für bessere Performance */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://mxadgucxhmstlzsbgmoz.supabase.co" />
{/* Theme Color für mobile Browser */}
<meta name="theme-color" content="#18181b" />
{/* JSON-LD Structured Data */}
<PersonJsonLd locale={locale as Locale} />
<ProfessionalServiceJsonLd locale={locale as Locale} />
<OrganizationJsonLd locale={locale as Locale} />
<WebSiteJsonLd locale={locale as Locale} />
</head>
<body className="min-h-screen bg-zinc-900 text-zinc-50 antialiased">
<NextIntlClientProvider messages={messages}>
{children}
<div className="min-h-screen flex flex-col relative">
<GlobalBackground />
<Header />
<main className="flex-1 pt-16 relative z-10">{children}</main>
<Footer />
</div>
</NextIntlClientProvider>
</body>
</html>