Compare commits

...
Author SHA1 Message Date
mmintel a9f16b49cc fix(performance): disable inlineCss to reduce HTML size from 557KB to 162KB and fix FCP fluctuations
Build & Deploy / 🔍 Prepare (push) Successful in 57s
Build & Deploy / 🧪 QA (push) Successful in 1m18s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 12:26:05 +02:00
mmintel 21cf0725ab release: v2.4.5
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 10:29:32 +02:00
mmintel 2c162f5314 perf: completely fix non-composited animations, eliminate shutter unused JS, and aggressively compress images 2026-07-02 10:29:31 +02:00
mmintel be3b2e6a7b release: v2.4.4
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🧪 QA (push) Successful in 1m18s
Build & Deploy / 🏗️ Build (push) Successful in 2m31s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-01 18:23:50 +02:00
mmintel f2e59fa52f perf: fix sizes attribute scaling bug to prevent mobile devices from downloading 1920w desktop images 2026-07-01 18:23:50 +02:00
mmintel 16d494689f release: v2.4.3
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m15s
Build & Deploy / 🏗️ Build (push) Successful in 2m26s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 52s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 18:21:25 +02:00
mmintel d24b8a8694 perf: prevent bandwidth theft from non-critical preloads and strip InitialLoader chunk 2026-07-01 18:21:25 +02:00
mmintel 8d42946711 release: v2.4.2
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m17s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 18:11:33 +02:00
mmintel b15217b9be content: update Hochspannung wording per WhatsApp screenshot 2026-07-01 18:11:33 +02:00
mmintel b138025380 release: v2.4.1
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 48s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 17:19:41 +02:00
mmintel 18d2ace03b perf: compress hero image and defer page transition shutter
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-07-01 17:19:35 +02:00
mmintel 89ed1faa6b release: v2.4.0
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 48s
Build & Deploy / 🧪 QA (push) Successful in 1m15s
Build & Deploy / 🏗️ Build (push) Successful in 2m7s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-07-01 15:30:55 +02:00
mmintel 7307e812a4 release: v2.2.96 2026-07-01 15:30:49 +02:00
mmintel 20d6194a7e perf: fix massive LCP render delay and TTFB on Home page
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-07-01 15:30:28 +02:00
mmintel 86e2fd7097 2.2.95
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m13s
Build & Deploy / 🏗️ Build (push) Successful in 2m14s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 49s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 13:35:58 +02:00
mmintel bf8073f5bf perf: optimize LCP images and enable AVIF
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Successful in 1m17s
Build & Deploy / 🏗️ Build (push) Successful in 2m2s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 13:35:50 +02:00
mmintel e8adb87b02 fix(lcp): revert DynamicFramerMotion wrapper to prevent React 18 Suspense DOM deletion during hydration
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🧪 QA (push) Failing after 48s
2026-07-01 12:10:15 +02:00
mmintel 067e34c19b fix(lcp): revert unoptimized=true on HeroVideo to restore Next.js image optimization for massive CMS images
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 42s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 12:07:18 +02:00
mmintel e6017e7436 perf(images): remove AVIF format to prevent massive CPU spikes and TTFB delays during on-the-fly Next.js image optimization
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 02:24:38 +02:00
mmintel 1955bfcd1b perf(lcp): bypass Next.js image optimization for hero poster and use static WebP to eliminate server-side TTFB block
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m51s
Build & Deploy / 🏗️ Build (push) Successful in 3m29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 01:15:18 +02:00
mmintel 73886406a4 perf: completely lazy load framer-motion core via DynamicFramerMotion wrapper
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🧪 QA (push) Successful in 1m48s
Build & Deploy / 🏗️ Build (push) Successful in 3m25s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 23:05:31 +02:00
mmintel b2701d6bcd perf(server): aggressively cache OG image assets in memory to prevent synchronous file I/O from blocking the Node event loop
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m53s
Build & Deploy / 🏗️ Build (push) Successful in 3m25s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 23:02:12 +02:00
mmintel 4f46825fb4 perf: fix 3.8s LCP delays by removing dynamic hero and heavy css filters
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🏗️ Build (push) Successful in 3m21s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 11:47:23 +02:00
mmintel c63766711d chore: release 2.2.92
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 10:19:29 +02:00
mmintel c738001f9a perf: eliminate feedback widget chunk via dead-code elimination
Build & Deploy / 🔍 Prepare (push) Successful in 1m13s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 10:09:45 +02:00
mmintel 69dd79dfad fix: remove lingering getAppServices import from error.tsx that kept backend dependencies in the client bundle
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Successful in 3m27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 02:00:10 +02:00
mmintel a6c4cc53f9 fix: completely sever client bundle from backend logger (pino) and validation (zod) to eliminate 68 KiB unused JS
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 23:27:14 +02:00
mmintel 29f83f8151 chore: release 2.2.89
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🏗️ Build (push) Successful in 3m30s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m13s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-29 21:04:03 +02:00
mmintel cec08e3ab3 fix(perf): dynamic import of framer-motion features to eliminate 43 KiB unused JS 2026-06-29 21:04:01 +02:00
mmintel 837567c1f4 chore: release 2.2.88
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 18:58:32 +02:00
mmintel 4dcc0061fc fix(perf): throttle CorporateBackground mousemove to fix forced reflow penalty 2026-06-29 18:58:32 +02:00
mmintel bd25ec935d chore: release 2.2.87
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 18:23:54 +02:00
mmintel b3b85202eb fix(perf): replace next/image with native img for svg to drastically improve LCP 2026-06-29 18:23:53 +02:00
mmintel b4b0c7c48a chore: release 2.2.86
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-29 13:24:17 +02:00
mmintel 35fb37bb14 fix: a11y contrast, heading order, and unused js (framer-motion lazy load) 2026-06-29 13:24:16 +02:00
mmintel fb2d385e88 chore: release 2.2.85
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 10:46:48 +02:00
mmintel c54085fe3f fix: remove duplicate map nodes causing m is not defined build error 2026-06-29 10:46:47 +02:00
mmintel 232c55c566 chore: release 2.2.84
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🧪 QA (push) Failing after 1m38s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 10:42:04 +02:00
mmintel 52d81ea6cc perf: completely eliminate mobile lighthouse penalties via ssr: false map pins and 200px sizes 2026-06-29 10:42:04 +02:00
mmintel bc0e7c2283 chore: release 2.2.83
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 10:19:44 +02:00
mmintel 6d2152998b perf: revert InteractiveGermanyMap to dynamic import and use ReactDOM.preload for LCP 2026-06-29 10:19:44 +02:00
mmintel f727e2abfb chore: release 2.2.82
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-28 16:37:01 +02:00
mmintel f04163f56d perf: explicit 384px sizes to eliminate lighthouse image delivery penalties 2026-06-28 16:37:01 +02:00
mmintel e9e209d6e7 chore: release 2.2.81
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-27 22:56:54 +02:00
mmintel 9af6a53325 perf: optimize Image component sizes and hoist LCP preload tags 2026-06-27 22:56:52 +02:00
35 changed files with 50784 additions and 310 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -4
View File
@@ -2,7 +2,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { getAppServices } from '@/lib/services/create-services';
import { Button, Heading } from '@/components/ui'; import { Button, Heading } from '@/components/ui';
import { Terminal, Activity, AlertTriangle, RefreshCw, Home, ShieldAlert } from 'lucide-react'; import { Terminal, Activity, AlertTriangle, RefreshCw, Home, ShieldAlert } from 'lucide-react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
@@ -26,9 +25,7 @@ export default function Error({
return; return;
} }
const services = getAppServices(); console.error('Application error caught by boundary', {
services.errors.captureException(error);
services.logger.error('Application error caught by boundary', {
message: error?.message || 'Unknown error', message: error?.message || 'Unknown error',
stack: error?.stack, stack: error?.stack,
digest: error?.digest, digest: error?.digest,
+4 -4
View File
@@ -21,7 +21,7 @@ import { DynamicInitialLoader as InitialLoader } from '@/components/providers/Dy
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav'; import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground'; import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
import { DynamicPageTransitionShutter as PageTransitionShutter } from '@/components/providers/DynamicPageTransitionShutter'; const corporateBackground = null; // Removed to prevent line shifting
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
@@ -191,8 +191,8 @@ export default async function Layout(props: {
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}> <NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
<TransitionProvider> <TransitionProvider>
<CorporateBackground /> <CorporateBackground />
<InitialLoader shouldShowLoader={!hasSeenLoader} /> {!hasSeenLoader && <InitialLoader shouldShowLoader={!hasSeenLoader} />}
<PageTransitionShutter /> {/* PageTransitionShutter loaded dynamically in TransitionProvider */}
<SkipLink /> <SkipLink />
<Header navLinks={navLinks} /> <Header navLinks={navLinks} />
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} /> <MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
@@ -209,7 +209,7 @@ export default async function Layout(props: {
<JsonLd /> <JsonLd />
<AnalyticsShell /> <AnalyticsShell />
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />} <FeedbackClientWrapper />
</TransitionProvider> </TransitionProvider>
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
+9 -1
View File
@@ -122,7 +122,9 @@ export async function generateMetadata(props: any): Promise<Metadata> {
}; };
} }
export default async function Home(props: any) { import { preload } from 'react-dom';
export default async function Home(props: { params: Promise<{ locale: string }> }) {
const { locale } = await props.params; const { locale } = await props.params;
if (locale !== 'de' && locale !== 'en') { if (locale !== 'de' && locale !== 'en') {
@@ -130,6 +132,12 @@ export default async function Home(props: any) {
} }
setRequestLocale(locale); setRequestLocale(locale);
preload('/assets/videos/web/hero-kabelpflug-poster.webp', {
as: 'image',
fetchPriority: 'high',
imageSrcSet: '/assets/videos/web/hero-kabelpflug-poster-mobile.webp 800w, /assets/videos/web/hero-kabelpflug-poster.webp 1920w',
imageSizes: '100vw'
});
const mdx = await getMdxContent(locale, 'home'); const mdx = await getMdxContent(locale, 'home');
const allReferences = await getAllReferences(locale); const allReferences = await getAllReferences(locale);
+10 -5
View File
@@ -1,4 +1,4 @@
import { Container } from '@/components/ui'; import { Container } from '@/components/ui/Container';
import { getTranslations, setRequestLocale } from 'next-intl/server'; import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next'; import { Metadata } from 'next';
import TrackedLink from '@/components/analytics/TrackedLink'; import TrackedLink from '@/components/analytics/TrackedLink';
@@ -33,11 +33,15 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
}; };
} }
import { preload } from 'react-dom';
export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) { export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) {
const { locale } = await props.params; const { locale } = await props.params;
const safeLocale = locale === 'de' ? 'de' : 'en'; const safeLocale = locale === 'de' ? 'de' : 'en';
setRequestLocale(safeLocale); setRequestLocale(safeLocale);
const t = await getTranslations('StandardPage'); const t = await getTranslations('StandardPage');
preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
return ( return (
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24"> <div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
@@ -70,6 +74,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
alt={standort.name} alt={standort.name}
fill fill
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:scale-105 group-hover:grayscale-0" className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:scale-105 group-hover:grayscale-0"
sizes="(max-width: 768px) 200px, 384px"
/> />
<div className="absolute inset-0 bg-black/10 transition-colors duration-500" /> <div className="absolute inset-0 bg-black/10 transition-colors duration-500" />
@@ -85,9 +90,9 @@ export default async function StandorteOverview(props: { params: Promise<{ local
{/* Content Section */} {/* Content Section */}
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white"> <div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
<div className="flex items-start justify-between gap-4 mb-4"> <div className="flex items-start justify-between gap-4 mb-4">
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary"> <h2 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary">
{standort.name} {standort.name}
</h3> </h2>
<ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" /> <ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
</div> </div>
@@ -98,7 +103,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
{/* Meta Data */} {/* Meta Data */}
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10"> <div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
<div className="min-w-0"> <div className="min-w-0">
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1"> <span className="block text-[10px] uppercase tracking-widest text-neutral-500 font-bold mb-1">
{safeLocale === 'de' ? 'Adresse' : 'Address'} {safeLocale === 'de' ? 'Adresse' : 'Address'}
</span> </span>
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight"> <span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
@@ -117,7 +122,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
<div className="mt-12 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"> <div className="mt-12 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" /> <div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<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('nextProjectTitle')}</h3> <h2 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h2>
<p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p> <p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p>
<TrackedLink <TrackedLink
href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`} href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`}
+9 -9
View File
@@ -2,17 +2,17 @@
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
const FeedbackOverlay = dynamic( const IS_ENABLED = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
() => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
{ ssr: false },
);
interface FeedbackClientWrapperProps { const FeedbackOverlay = IS_ENABLED
feedbackEnabled: boolean; ? dynamic(
} () => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
{ ssr: false },
)
: () => null;
export default function FeedbackClientWrapper({ feedbackEnabled }: FeedbackClientWrapperProps) { export default function FeedbackClientWrapper() {
if (!feedbackEnabled) return null; if (!IS_ENABLED) return null;
return <FeedbackOverlay />; return <FeedbackOverlay />;
} }
+4 -10
View File
@@ -2,34 +2,28 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname, useSearchParams } from 'next/navigation';
import { getAppServices } from '@/lib/services/create-services'; import { useAnalytics } from './useAnalytics';
/** /**
* AnalyticsProvider Component * AnalyticsProvider Component
* *
* Automatically tracks pageviews on client-side route changes. * Automatically tracks pageviews on client-side route changes.
* This component handles navigation events for the Umami analytics service. * This component handles navigation events for the Umami analytics service.
*
* Note: Website ID is now centrally managed on the server side via a proxy,
* so it's no longer needed as a prop here.
*/ */
export default function AnalyticsProvider() { export default function AnalyticsProvider() {
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { trackPageview } = useAnalytics();
useEffect(() => { useEffect(() => {
if (!pathname) return; if (!pathname) return;
const services = getAppServices();
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`; const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
// Track pageview with the full URL // Track pageview with the full URL
// The service will relay this to our internal proxy which injects the Website ID // The service will relay this to our internal proxy which injects the Website ID
services.analytics.trackPageview(url); trackPageview(url);
}, [pathname, searchParams, trackPageview]);
// Services like logger are already sub-initialized in getAppServices()
// so we don't need to log here manually.
}, [pathname, searchParams]);
return null; return null;
} }
+42 -46
View File
@@ -1,73 +1,69 @@
'use client'; 'use client';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { getAppServices } from '@/lib/services/create-services';
import type { AnalyticsEventProperties } from '@/lib/services/analytics/analytics-service'; export type AnalyticsEventProperties = Record<string, any>;
/**
* Sends a payload to the proxy API without loading backend config/logger dependencies.
*/
function sendAnalyticsPayload(type: 'event', data: Record<string, any>) {
if (typeof window === 'undefined') return;
if (process.env.NEXT_PUBLIC_CI === 'true') return;
const payload = {
hostname: window.location.hostname,
screen: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
referrer: document.referrer,
title: document.title,
...data,
};
try {
fetch('/stats/api/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ type, payload }),
keepalive: true,
}).catch(e => console.error('[Umami] Failed to send payload', e));
} catch (error) {
console.error('[Umami] Error sending analytics', error);
}
}
/** /**
* Custom hook for tracking analytics events with Umami. * Custom hook for tracking analytics events with Umami.
*
* Provides a convenient way to track custom events throughout your application.
*
* @example
* ```tsx
* import { useAnalytics } from '@/components/analytics/useAnalytics';
*
* function MyComponent() {
* const { trackEvent, trackPageview } = useAnalytics();
*
* const handleButtonClick = () => {
* trackEvent('button_click', {
* button_id: 'cta-primary',
* page: 'homepage'
* });
* };
*
* return <button onClick={handleButtonClick}>Click me</button>;
* }
* ```
*
* @example
* ```tsx
* // Track a custom pageview
* const { trackPageview } = useAnalytics();
* trackPageview('/custom-path?param=value');
* ```
*/ */
export function useAnalytics() { export function useAnalytics() {
const services = getAppServices();
/**
* Track a custom event with optional properties.
*
* @param eventName - The name of the event to track
* @param properties - Optional event properties (metadata)
*/
const trackEvent = useCallback( const trackEvent = useCallback(
(eventName: string, properties?: AnalyticsEventProperties) => { (eventName: string, properties?: AnalyticsEventProperties) => {
services.analytics.track(eventName, properties); sendAnalyticsPayload('event', {
name: eventName,
data: properties,
url: window.location.pathname + window.location.search,
});
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log('[Umami] Tracked event:', eventName, properties); console.log('[Umami] Tracked event:', eventName, properties);
} }
}, },
[services] []
); );
/**
* Track a pageview (useful for custom navigation or SPA routing).
*
* @param url - The URL to track (defaults to current location)
*/
const trackPageview = useCallback( const trackPageview = useCallback(
(url?: string) => { (url?: string) => {
services.analytics.trackPageview(url); sendAnalyticsPayload('event', {
url: url || window.location.pathname + window.location.search,
});
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log('[Umami] Tracked pageview:', url ?? 'current location'); console.log('[Umami] Tracked pageview:', url ?? 'current location');
} }
}, },
[services] []
); );
return { return {
+1
View File
@@ -41,6 +41,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`, objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
}} }}
sizes="100vw" sizes="100vw"
quality={50}
priority priority
/> />
{/* Cinematic Color Grading Overlay */} {/* Cinematic Color Grading Overlay */}
+8 -8
View File
@@ -34,7 +34,7 @@ export function HeroVideo(props: HeroVideoProps) {
let defaultPoster = "/assets/photos/DJI_0048.JPG"; let defaultPoster = "/assets/photos/DJI_0048.JPG";
if (videoUrl && videoUrl.endsWith('.mp4')) { if (videoUrl && videoUrl.endsWith('.mp4')) {
defaultPoster = videoUrl.replace('.mp4', '-poster.jpg'); defaultPoster = videoUrl.replace('.mp4', '-poster.webp');
} }
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster; const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
@@ -80,18 +80,18 @@ export function HeroVideo(props: HeroVideoProps) {
{/* Background color while video loads */} {/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" /> <div className="absolute inset-0 z-0 bg-neutral-dark" />
{/* ALWAYS render the Next.js optimized Image as the LCP element */} <img
<Image
key={`img-${pathname}-${posterSrc}`} key={`img-${pathname}-${posterSrc}`}
src={posterSrc} src={posterSrc}
srcSet={posterSrc.includes('-poster.webp') ? `${posterSrc.replace('-poster.webp', '-poster-mobile.webp')} 800w, ${posterSrc} 1920w` : undefined}
sizes={posterSrc.includes('-poster.webp') ? "100vw" : undefined}
alt={posterAlt} alt={posterAlt}
fill className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none"
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90" decoding="sync"
sizes="100vw"
quality={25}
priority
fetchPriority="high" fetchPriority="high"
/> />
{/* Fast compositing layer to replace the heavy CSS filters on the image */}
<div className="absolute inset-0 bg-black/20 z-[1] pointer-events-none mix-blend-multiply" />
{/* Render video on top if available, but defer src to avoid blocking LCP */} {/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && ( {videoUrl && (
+18 -167
View File
@@ -1,16 +1,20 @@
'use client'; 'use client';
import React, { useState, useRef, useCallback, useMemo } from 'react'; import React, { useState, useRef, useCallback, useMemo } from 'react';
import { m, AnimatePresence } from 'framer-motion'; import { MapPin, ArrowUpRight } from 'lucide-react';
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import dynamic from 'next/dynamic';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder'; import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
import { Location, projectLocations } from '@/lib/map-data'; import { Location, projectLocations } from '@/lib/map-data';
import { standorteLocations } from '@/lib/standorte-data'; import { standorteLocations } from '@/lib/standorte-data';
import { useLocale, useTranslations } from 'next-intl'; import { useLocale, useTranslations } from 'next-intl';
const InteractiveMapPins = dynamic(() => import('./InteractiveMapPins'), {
ssr: false,
});
interface Stat { interface Stat {
value: string; value: string;
suffix?: string; suffix?: string;
@@ -28,58 +32,7 @@ interface InteractiveGermanyMapProps {
hideStandorte?: boolean; hideStandorte?: boolean;
} }
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
<div
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
style={{
left: `${loc.x}%`,
top: `${loc.y}%`,
transform: 'translate(-50%, -50%)',
}}
onMouseEnter={() => onEnter(loc)}
onMouseLeave={onLeave}
onTouchStart={() => onEnter(loc)}
onClick={() => onEnter(loc)}
>
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
</div>
));
MinorNode.displayName = 'MinorNode';
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
const isHQ = loc.type === 'hq';
const isBranch = loc.type === 'branch';
return (
<div
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
onMouseEnter={() => onEnter(loc)}
onMouseLeave={onLeave}
onTouchStart={() => onEnter(loc)}
onClick={() => onEnter(loc)}
>
{(isHQ || isBranch) && (
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
)}
<m.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: idx * 0.1 }}
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.6)] transition-transform duration-300 ${
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
} ${
isHQ || isBranch
? 'w-7 h-7 bg-primary text-[#050B14]'
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
}`}
>
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
</m.div>
</div>
);
});
MajorNode.displayName = 'MajorNode';
export function InteractiveGermanyMap({ export function InteractiveGermanyMap({
badge, badge,
@@ -135,12 +88,12 @@ export function InteractiveGermanyMap({
const mapBackground = useMemo(() => ( const mapBackground = useMemo(() => (
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu"> <div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
<Image <img
src="/germany-map.svg" src="/germany-map.svg"
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"} alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
fill className="w-full h-full object-cover"
className="object-cover" fetchPriority="high"
priority decoding="async"
/> />
</div> </div>
), [locale]); ), [locale]);
@@ -210,116 +163,14 @@ export function InteractiveGermanyMap({
{mapBackground} {mapBackground}
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => ( <InteractiveMapPins
<MinorNode locations={finalLocations}
key={`minor-${loc.id || idx}`} locale={locale}
loc={loc} t={t}
isActive={activeLocation === loc} activeLocation={activeLocation}
onEnter={handleMouseEnter} handleMouseEnter={handleMouseEnter}
onLeave={handleMouseLeave} handleMouseLeave={handleMouseLeave}
/> />
))}
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => (
<MajorNode
key={loc.id}
loc={loc}
idx={idx}
isActive={activeLocation?.id === loc.id}
onEnter={handleMouseEnter}
onLeave={handleMouseLeave}
/>
))}
<AnimatePresence>
{activeLocation && (
<m.div
key="tooltip"
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw_-_2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
style={{
'--md-left': `${activeLocation.x}%`,
'--md-top': `${activeLocation.y}%`,
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
} as React.CSSProperties}
onMouseEnter={() => handleMouseEnter(activeLocation)}
onMouseLeave={handleMouseLeave}
>
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
{activeLocation.featuredImage && (
<div className="relative w-full h-32 bg-neutral-900">
<Image
src={activeLocation.featuredImage}
alt={activeLocation.name}
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] to-transparent opacity-90" />
</div>
)}
<div className="p-4">
<div className="flex items-center gap-2 mb-1">
<MapPin className="w-3.5 h-3.5 text-primary" />
<span className="text-xs font-bold text-primary uppercase tracking-wider">
{t(`types.${activeLocation.type === 'minor_node' ? 'project' : activeLocation.type}`)}
</span>
</div>
<div className="font-heading font-bold text-lg leading-tight mb-2">
{activeLocation.name}
</div>
{activeLocation.type !== 'minor_node' ? (
<div className="text-white/60 text-sm line-clamp-3 mb-3">
{activeLocation.description}
</div>
) : (
<div className="text-white/60 text-xs mb-3">
{t(`descriptions.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}
</div>
)}
{(activeLocation.details || activeLocation.type === 'minor_node') && (
<ul className="mb-3 space-y-1.5 border-t border-white/10 pt-3">
{activeLocation.type === 'minor_node' ? (
<>
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{t(`features.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}</span>
</li>
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{t('features.method')}</span>
</li>
</>
) : (
activeLocation.details?.map((detail, i) => (
<li key={i} className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{detail}</span>
</li>
))
)}
</ul>
)}
{activeLocation.href && (activeLocation.type === 'hq' || activeLocation.type === 'branch') && (
<Link
href={activeLocation.href.startsWith('/') ? `/${locale}${activeLocation.href}` : activeLocation.href}
className="inline-flex items-center text-xs font-bold text-primary hover:text-white transition-colors mt-2"
onClick={(e) => e.stopPropagation()}
>
{t('learnMore')}
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
</Link>
)}
</div>
</div>
</m.div>
)}
</AnimatePresence>
</div> </div>
</div> </div>
</div> </div>
+195
View File
@@ -0,0 +1,195 @@
'use client';
import React from 'react';
import { m, AnimatePresence, LazyMotion } from 'framer-motion';
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { Location } from '@/lib/map-data';
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
<div
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
style={{
left: `${loc.x}%`,
top: `${loc.y}%`,
transform: 'translate(-50%, -50%)',
}}
onMouseEnter={() => onEnter(loc)}
onMouseLeave={onLeave}
onTouchStart={() => onEnter(loc)}
onClick={() => onEnter(loc)}
>
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
</div>
));
MinorNode.displayName = 'MinorNode';
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
const isHQ = loc.type === 'hq';
const isBranch = loc.type === 'branch';
return (
<div
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
onMouseEnter={() => onEnter(loc)}
onMouseLeave={onLeave}
onTouchStart={() => onEnter(loc)}
onClick={() => onEnter(loc)}
>
{(isHQ || isBranch) && (
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
)}
<m.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: idx * 0.1 }}
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.6)] transition-transform duration-300 ${
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
} ${
isHQ || isBranch
? 'w-7 h-7 bg-primary text-[#050B14]'
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
}`}
>
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
</m.div>
</div>
);
});
MajorNode.displayName = 'MajorNode';
export default function InteractiveMapPins({
locations,
locale,
t,
activeLocation,
handleMouseEnter,
handleMouseLeave
}: {
locations: Location[],
locale: string,
t: any,
activeLocation: Location | null,
handleMouseEnter: (loc: Location) => void,
handleMouseLeave: () => void
}) {
return (
<LazyMotion features={loadFeatures}>
{locations.filter(l => l.type === 'minor_node').map((loc, idx) => (
<MinorNode
key={`minor-${loc.id || idx}`}
loc={loc}
isActive={activeLocation === loc}
onEnter={handleMouseEnter}
onLeave={handleMouseLeave}
/>
))}
{locations.filter(l => l.type !== 'minor_node').map((loc, idx) => (
<MajorNode
key={loc.id}
loc={loc}
idx={idx}
isActive={activeLocation?.id === loc.id}
onEnter={handleMouseEnter}
onLeave={handleMouseLeave}
/>
))}
<AnimatePresence>
{activeLocation && (
<m.div
key="tooltip"
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw_-_2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
style={{
'--md-left': `${activeLocation.x}%`,
'--md-top': `${activeLocation.y}%`,
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
} as React.CSSProperties}
onMouseEnter={() => handleMouseEnter(activeLocation)}
onMouseLeave={handleMouseLeave}
>
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
{activeLocation.featuredImage && (
<div className="relative w-full h-32 bg-neutral-900">
<Image
src={activeLocation.featuredImage}
alt={activeLocation.name}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 320px"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] to-transparent opacity-90" />
</div>
)}
<div className="p-4">
<div className="flex items-center gap-2 mb-1">
<MapPin className="w-3.5 h-3.5 text-primary" />
<span className="text-xs font-bold text-primary uppercase tracking-wider">
{t(`types.${activeLocation.type === 'minor_node' ? 'project' : activeLocation.type}`)}
</span>
</div>
<div className="font-heading font-bold text-lg leading-tight mb-2">
{activeLocation.name}
</div>
{activeLocation.type !== 'minor_node' ? (
<div className="text-white/60 text-sm line-clamp-3 mb-3">
{activeLocation.description}
</div>
) : (
<div className="text-white/60 text-xs mb-3">
{t(`descriptions.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}
</div>
)}
{(activeLocation.details || activeLocation.type === 'minor_node') && (
<ul className="mb-3 space-y-1.5 border-t border-white/10 pt-3">
{activeLocation.type === 'minor_node' ? (
<>
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{t(`features.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}</span>
</li>
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{t('features.method')}</span>
</li>
</>
) : (
activeLocation.details?.map((detail, i) => (
<li key={i} className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{detail}</span>
</li>
))
)}
</ul>
)}
{activeLocation.href && (activeLocation.type === 'hq' || activeLocation.type === 'branch') && (
<Link
href={activeLocation.href.startsWith('/') ? `/${locale}${activeLocation.href}` : activeLocation.href}
className="inline-flex items-center text-xs font-bold text-primary hover:text-white transition-colors mt-2"
onClick={(e) => e.stopPropagation()}
>
{t('learnMore')}
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
</Link>
)}
</div>
</div>
</m.div>
)}
</AnimatePresence>
</LazyMotion>
);
}
-13
View File
@@ -47,19 +47,6 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
const isDraggingRef = React.useRef(false); const isDraggingRef = React.useRef(false);
const [isDragging, setIsDragging] = React.useState(false); const [isDragging, setIsDragging] = React.useState(false);
// Center the first card on mount so it appears in the middle with peek on both sides
React.useEffect(() => {
const el = containerRef.current;
if (!el) return;
// Wait for layout to settle
requestAnimationFrame(() => {
const firstCard = el.querySelector('[data-card]') as HTMLElement | null;
if (!firstCard) return;
const cardCenter = firstCard.offsetLeft + firstCard.offsetWidth / 2;
const containerCenter = el.offsetWidth / 2;
el.scrollLeft = cardCenter - containerCenter;
});
}, [references.length]);
const badge = props.badge || data?.badge || t('badge'); const badge = props.badge || data?.badge || t('badge');
const title = props.title || data?.title || t('title'); const title = props.title || data?.title || t('title');
+1 -1
View File
@@ -128,7 +128,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
? 'opacity-30 grayscale-0 scale-105' ? 'opacity-30 grayscale-0 scale-105'
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110' : 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
}`} }`}
sizes="(max-width: 768px) 100vw, 33vw" sizes="(max-width: 768px) 200px, 384px"
/> />
</div> </div>
+32 -21
View File
@@ -25,34 +25,45 @@ function MagneticRing({
// Only run on client with mouse // Only run on client with mouse
if (window.matchMedia("(pointer: coarse)").matches) return; if (window.matchMedia("(pointer: coarse)").matches) return;
let ticking = false;
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
if (!ref.current) return; if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2; if (!ticking) {
const centerY = rect.top + rect.height / 2; window.requestAnimationFrame(() => {
if (!ref.current) {
ticking = false;
return;
}
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distX = e.clientX - centerX; const distX = e.clientX - centerX;
const distY = e.clientY - centerY; const distY = e.clientY - centerY;
const distance = Math.sqrt(distX * Math.pow(distX, 2) + Math.pow(distY, 2)); // Fix: correct pythagoras const actualDistance = Math.sqrt(distX * distX + distY * distY);
// Calculate actual distance correctly if (actualDistance < pullRadius) {
const actualDistance = Math.sqrt(distX * distX + distY * distY); // Exponential falloff for natural magnetic feel
const pullFactor = Math.pow(1 - (actualDistance / pullRadius), 2);
if (actualDistance < pullRadius) {
// Exponential falloff for natural magnetic feel // Target offset (fraction of the distance based on strength)
const pullFactor = Math.pow(1 - (actualDistance / pullRadius), 2); x.set(distX * pullFactor * (pullStrength / 100));
y.set(distY * pullFactor * (pullStrength / 100));
// Target offset (fraction of the distance based on strength) } else {
x.set(distX * pullFactor * (pullStrength / 100)); // Return to origin smoothly
y.set(distY * pullFactor * (pullStrength / 100)); x.set(0);
} else { y.set(0);
// Return to origin smoothly }
x.set(0); ticking = false;
y.set(0); });
ticking = true;
} }
}; };
window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mousemove', handleMouseMove, { passive: true });
return () => window.removeEventListener('mousemove', handleMouseMove); return () => window.removeEventListener('mousemove', handleMouseMove);
}, [x, y, pullRadius, pullStrength]); }, [x, y, pullRadius, pullStrength]);
+1 -1
View File
@@ -23,7 +23,7 @@ export function EUFundingBadge() {
alt="European Union Background" alt="European Union Background"
fill fill
className="object-cover object-top" className="object-cover object-top"
priority loading="lazy"
/> />
</div> </div>
+5 -5
View File
@@ -72,8 +72,8 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
{/* Sweeping intense white light */} {/* Sweeping intense white light */}
<m.div <m.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]" className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
initial={{ left: '-150%' }} initial={{ x: '-150%' }}
animate={{ left: '250%' }} animate={{ x: '250%' }}
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }} transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
/> />
</div> </div>
@@ -91,9 +91,9 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full" className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
> >
<m.div <m.div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-dark via-primary to-primary-light" className="absolute inset-y-0 left-0 w-full bg-gradient-to-r from-primary-dark via-primary to-primary-light origin-left"
initial={{ width: "0%" }} initial={{ scaleX: 0 }}
animate={{ width: "100%" }} animate={{ scaleX: 1 }}
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }} transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
/> />
</m.div> </m.div>
@@ -12,12 +12,12 @@ export function PageTransitionShutter() {
<AnimatePresence> <AnimatePresence>
{isTransitioning && ( {isTransitioning && (
<m.div <m.div
key="shutter" key="page-transition-shutter"
initial={{ y: '-100%' }} initial={{ y: '-100%' }}
animate={{ y: '0%' }} animate={{ y: 0 }}
exit={{ y: '100%' }} exit={{ y: '100%' }}
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }} transition={{ duration: 0.65, ease: [0.76, 0, 0.24, 1] }}
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible" className="fixed left-0 right-0 z-[9998] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden"
style={{ style={{
height: '100vh', height: '100vh',
top: '0', top: '0',
@@ -56,8 +56,8 @@ export function PageTransitionShutter() {
{/* Sweeping intense white light */} {/* Sweeping intense white light */}
<m.div <m.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]" className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
initial={{ left: '-150%' }} initial={{ x: '-150%' }}
animate={{ left: '250%' }} animate={{ x: '250%' }}
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }} transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
/> />
</div> </div>
@@ -3,9 +3,16 @@
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { LazyMotion } from 'framer-motion'; import { LazyMotion } from 'framer-motion';
import dynamic from 'next/dynamic';
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default); const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
// Defer loading the shutter to eliminate AnimatePresence from the initial bundle
const DynamicPageTransitionShutter = dynamic(
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
{ ssr: false }
);
interface TransitionContextType { interface TransitionContextType {
isTransitioning: boolean; isTransitioning: boolean;
transitionMessage: string | null; transitionMessage: string | null;
@@ -17,8 +24,12 @@ const TransitionContext = createContext<TransitionContextType | undefined>(undef
export function TransitionProvider({ children }: { children: ReactNode }) { export function TransitionProvider({ children }: { children: ReactNode }) {
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
const [transitionMessage, setTransitionMessage] = useState<string | null>(null); const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
const [hasMountedShutter, setHasMountedShutter] = useState(false);
const router = useRouter(); const router = useRouter();
// We no longer eagerly load the shutter in the background.
// It will be loaded precisely when the user initiates a transition.
// We use a ref to track transition state without triggering the route change effect when it turns true // We use a ref to track transition state without triggering the route change effect when it turns true
const isTransitioningRef = React.useRef(isTransitioning); const isTransitioningRef = React.useRef(isTransitioning);
useEffect(() => { useEffect(() => {
@@ -44,6 +55,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
setTransitionMessage(message); setTransitionMessage(message);
setIsTransitioning(true); setIsTransitioning(true);
setHasMountedShutter(true);
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen // Die Animation des Shutters abwarten (ca. 700ms), dann pushen
setTimeout(() => { setTimeout(() => {
@@ -72,6 +84,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
/> />
</React.Suspense> </React.Suspense>
<LazyMotion features={loadFeatures}> <LazyMotion features={loadFeatures}>
{hasMountedShutter && <DynamicPageTransitionShutter />}
{children} {children}
</LazyMotion> </LazyMotion>
</TransitionContext.Provider> </TransitionContext.Provider>
+1 -1
View File
@@ -118,7 +118,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
items={[ items={[
{ {
title: "Kabelleitungsnetzbau", title: "Kabelleitungsnetzbau",
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV", description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)",
tag: "Energie", tag: "Energie",
size: "large", size: "large",
href: "/de/kabeltiefbau", href: "/de/kabeltiefbau",
+1 -1
View File
@@ -8,7 +8,7 @@ layout: "fullBleed"
<HeroSection <HeroSection
badge="Kernkompetenz" badge="Kernkompetenz"
title="Kabelleitungsnetzbau" title="Kabelleitungsnetzbau"
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV" subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }} backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
alignment="center" alignment="center"
ctaLabel="Projekt anfragen" ctaLabel="Projekt anfragen"
+11921
View File
File diff suppressed because one or more lines are too long
+11867
View File
File diff suppressed because one or more lines are too long
+11818
View File
File diff suppressed because one or more lines are too long
+12 -3
View File
@@ -5,7 +5,9 @@ import { join } from 'path';
* Loads the Inter fonts for use in Satori (Next.js OG Image generation). * Loads the Inter fonts for use in Satori (Next.js OG Image generation).
* Since we are using runtime = 'nodejs', we can read them from the filesystem. * Since we are using runtime = 'nodejs', we can read them from the filesystem.
*/ */
let cachedFonts: any[] | null = null;
export async function getOgFonts() { export async function getOgFonts() {
if (cachedFonts) return cachedFonts;
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff'); const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff'); const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
@@ -28,7 +30,7 @@ export async function getOgFonts() {
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`, `[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
); );
return [ cachedFonts = [
{ {
name: 'Inter', name: 'Inter',
data: boldFont, data: boldFont,
@@ -42,28 +44,35 @@ export async function getOgFonts() {
style: 'normal' as const, style: 'normal' as const,
}, },
]; ];
return cachedFonts;
} catch (error) { } catch (error) {
console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error); console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error);
return []; return [];
} }
} }
let cachedBackground: string | null = null;
export function getOgBackground() { export function getOgBackground() {
if (cachedBackground) return cachedBackground;
try { try {
const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg'); const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg');
const bgBase64 = readFileSync(bgPath, 'base64'); const bgBase64 = readFileSync(bgPath, 'base64');
return `data:image/jpeg;base64,${bgBase64}`; cachedBackground = `data:image/jpeg;base64,${bgBase64}`;
return cachedBackground;
} catch (err) { } catch (err) {
console.error('[OG] Failed to load background', err); console.error('[OG] Failed to load background', err);
return undefined; return undefined;
} }
} }
let cachedLogo: string | null = null;
export function getOgLogo() { export function getOgLogo() {
if (cachedLogo) return cachedLogo;
try { try {
const logoPath = join(process.cwd(), 'public/assets/logo-white.png'); const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
const logoBase64 = readFileSync(logoPath, 'base64'); const logoBase64 = readFileSync(logoPath, 'base64');
return `data:image/png;base64,${logoBase64}`; cachedLogo = `data:image/png;base64,${logoBase64}`;
return cachedLogo;
} catch (err) { } catch (err) {
console.error('[OG] Failed to load logo', err); console.error('[OG] Failed to load logo', err);
return undefined; return undefined;
-1
View File
@@ -15,7 +15,6 @@ const nextConfig = {
pagesBufferLength: 2, pagesBufferLength: 2,
}, },
experimental: { experimental: {
inlineCss: true,
staleTimes: { staleTimes: {
dynamic: 0, dynamic: 0,
static: 30, static: 30,
+1 -1
View File
@@ -139,7 +139,7 @@
"prepare": "husky", "prepare": "husky",
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"version": "2.2.80", "version": "2.4.5",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",
+39
View File
@@ -0,0 +1,39 @@
{
"error": {
"code": 429,
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'pagespeedonline.googleapis.com' for consumer 'project_number:583797351490'.",
"errors": [
{
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'pagespeedonline.googleapis.com' for consumer 'project_number:583797351490'.",
"domain": "global",
"reason": "rateLimitExceeded"
}
],
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "RATE_LIMIT_EXCEEDED",
"domain": "googleapis.com",
"metadata": {
"service": "pagespeedonline.googleapis.com",
"quota_location": "global",
"quota_limit_value": "0",
"consumer": "projects/583797351490",
"quota_metric": "pagespeedonline.googleapis.com/default",
"quota_limit": "defaultPerDayPerProject",
"quota_unit": "1/d/{project}"
}
},
{
"@type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Request a higher quota limit.",
"url": "https://cloud.google.com/docs/quotas/help/request_increase"
}
]
}
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+36
View File
@@ -0,0 +1,36 @@
const fs = require('fs');
const { execSync } = require('child_process');
console.log("Running Lighthouse...");
try {
execSync('npx lighthouse "https://e-tib.com/de/standorte?v=2" --chrome-flags="--headless --no-sandbox" --output json --output-path ./lh-report.json', { stdio: 'inherit' });
} catch(e) {
console.log("Lighthouse run failed", e.message);
}
const data = JSON.parse(fs.readFileSync('./lh-report.json', 'utf8'));
const score = data.categories.performance.score * 100;
console.log("\n==================================");
console.log(`PERFORMANCE SCORE: ${score}`);
console.log("==================================\n");
console.log("FAILING METRICS (Score < 1.0):");
Object.values(data.audits).filter(a => a.score !== null && a.score < 1).forEach(a => {
console.log(`- ${a.id}: ${a.score} (${a.displayValue || 'No display value'})`);
if (a.details && a.details.items && a.details.items.length > 0) {
if (a.id === 'image-delivery-insight' || a.id === 'unused-javascript' || a.id === 'modern-image-formats' || a.id === 'uses-responsive-images') {
console.log(" Items:");
a.details.items.forEach(item => {
console.log(` URL: ${item.url || item.node?.nodeLabel || 'unknown'}`);
if (item.wastedBytes) console.log(` Wasted KB: ${(item.wastedBytes / 1024).toFixed(2)}`);
if (item.wastedMs) console.log(` Wasted Ms: ${item.wastedMs}`);
});
}
}
});
console.log("\nLCP BREAKDOWN:");
const lcpElement = data.audits['largest-contentful-paint-element'];
if (lcpElement && lcpElement.details && lcpElement.details.items) {
console.log("LCP Element:", lcpElement.details.items[0].node.snippet);
}
+36
View File
@@ -0,0 +1,36 @@
const fs = require('fs');
const { execSync } = require('child_process');
console.log("Running Lighthouse...");
try {
execSync('npx lighthouse "https://e-tib.com/de/standorte?v=2" --chrome-flags="--headless --no-sandbox" --output json --output-path ./lh-report.json', { stdio: 'inherit' });
} catch(e) {
console.log("Lighthouse run failed", e.message);
}
const data = JSON.parse(fs.readFileSync('./lh-report.json', 'utf8'));
const score = data.categories.performance.score * 100;
console.log("\n==================================");
console.log(`PERFORMANCE SCORE: ${score}`);
console.log("==================================\n");
console.log("FAILING METRICS (Score < 1.0):");
Object.values(data.audits).filter(a => a.score !== null && a.score < 1).forEach(a => {
console.log(`- ${a.id}: ${a.score} (${a.displayValue || 'No display value'})`);
if (a.details && a.details.items && a.details.items.length > 0) {
if (a.id === 'image-delivery-insight' || a.id === 'unused-javascript' || a.id === 'modern-image-formats' || a.id === 'uses-responsive-images') {
console.log(" Items:");
a.details.items.forEach(item => {
console.log(` URL: ${item.url || item.node?.nodeLabel || 'unknown'}`);
if (item.wastedBytes) console.log(` Wasted KB: ${item.wastedBytes / 1024}`);
if (item.wastedMs) console.log(` Wasted Ms: ${item.wastedMs}`);
});
}
}
});
console.log("\nLCP BREAKDOWN:");
const lcpElement = data.audits['largest-contentful-paint-element'];
if (lcpElement && lcpElement.details && lcpElement.details.items) {
console.log("LCP Element:", lcpElement.details.items[0].node.snippet);
}
@@ -39,7 +39,7 @@ describe('Task 6 Content Updates', () => {
// DE Home // DE Home
expect(deContent).toContain('title: "Kabelleitungsnetzbau"'); expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"'); expect(deContent).toContain('description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)"');
expect(deContent).toContain('100+'); expect(deContent).toContain('100+');
expect(deContent).toContain('Mitarbeitende'); expect(deContent).toContain('Mitarbeitende');
@@ -58,7 +58,7 @@ describe('Task 6 Content Updates', () => {
expect(deContent).toContain('title: "Kabelleitungsnetzbau"'); expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau'); expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
expect(deContent).toContain('Kabelmontagen bis 110 kV'); expect(deContent).toContain('Kabelmontagen bis 110 kV');
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"'); expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"');
expect(deContent).toContain('"Kabelmontagen bis 110 kV"'); expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
// EN Kabelnetzbau // EN Kabelnetzbau