Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d69ac2de2 | |||
| a9f16b49cc | |||
| 21cf0725ab | |||
| 2c162f5314 | |||
| be3b2e6a7b | |||
| f2e59fa52f | |||
| 16d494689f | |||
| d24b8a8694 | |||
| 8d42946711 | |||
| b15217b9be | |||
| b138025380 | |||
| 18d2ace03b | |||
| 89ed1faa6b | |||
| 7307e812a4 | |||
| 20d6194a7e | |||
| 86e2fd7097 | |||
| bf8073f5bf | |||
| e8adb87b02 | |||
| 067e34c19b | |||
| e6017e7436 | |||
| 1955bfcd1b | |||
| 73886406a4 | |||
| b2701d6bcd | |||
| 4f46825fb4 | |||
| c63766711d | |||
| c738001f9a | |||
| 69dd79dfad | |||
| a6c4cc53f9 |
2895
.lighthouseci/lhr-1782911226698.html
Normal file
2895
.lighthouseci/lhr-1782911226698.html
Normal file
File diff suppressed because one or more lines are too long
11796
.lighthouseci/lhr-1782911226698.json
Normal file
11796
.lighthouseci/lhr-1782911226698.json
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { getAppServices } from '@/lib/services/create-services';
|
||||
import { Button, Heading } from '@/components/ui';
|
||||
import { Terminal, Activity, AlertTriangle, RefreshCw, Home, ShieldAlert } from 'lucide-react';
|
||||
import { m } from 'framer-motion';
|
||||
@@ -26,9 +25,7 @@ export default function Error({
|
||||
return;
|
||||
}
|
||||
|
||||
const services = getAppServices();
|
||||
services.errors.captureException(error);
|
||||
services.logger.error('Application error caught by boundary', {
|
||||
console.error('Application error caught by boundary', {
|
||||
message: error?.message || 'Unknown error',
|
||||
stack: error?.stack,
|
||||
digest: error?.digest,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { DynamicInitialLoader as InitialLoader } from '@/components/providers/Dy
|
||||
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
|
||||
|
||||
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({
|
||||
subsets: ['latin'],
|
||||
@@ -191,8 +191,8 @@ export default async function Layout(props: {
|
||||
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
||||
<TransitionProvider>
|
||||
<CorporateBackground />
|
||||
<InitialLoader shouldShowLoader={!hasSeenLoader} />
|
||||
<PageTransitionShutter />
|
||||
{!hasSeenLoader && <InitialLoader shouldShowLoader={!hasSeenLoader} />}
|
||||
{/* PageTransitionShutter loaded dynamically in TransitionProvider */}
|
||||
<SkipLink />
|
||||
<Header navLinks={navLinks} />
|
||||
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
|
||||
@@ -209,7 +209,7 @@ export default async function Layout(props: {
|
||||
<JsonLd />
|
||||
<AnalyticsShell />
|
||||
|
||||
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
|
||||
<FeedbackClientWrapper />
|
||||
</TransitionProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
|
||||
@@ -132,7 +132,12 @@ export default async function Home(props: { params: Promise<{ locale: string }>
|
||||
}
|
||||
|
||||
setRequestLocale(locale);
|
||||
preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
|
||||
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 allReferences = await getAllReferences(locale);
|
||||
|
||||
@@ -7,8 +7,7 @@ import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { standorteData, standorteLocations } from '@/lib/standorte-data';
|
||||
import nextDynamic from 'next/dynamic';
|
||||
const InteractiveGermanyMap = nextDynamic(() => import('@/components/blocks/InteractiveGermanyMap').then(mod => mod.InteractiveGermanyMap));
|
||||
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const FeedbackOverlay = dynamic(
|
||||
() => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
|
||||
{ ssr: false },
|
||||
);
|
||||
const IS_ENABLED = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
||||
|
||||
interface FeedbackClientWrapperProps {
|
||||
feedbackEnabled: boolean;
|
||||
}
|
||||
const FeedbackOverlay = IS_ENABLED
|
||||
? dynamic(
|
||||
() => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
|
||||
{ ssr: false },
|
||||
)
|
||||
: () => null;
|
||||
|
||||
export default function FeedbackClientWrapper({ feedbackEnabled }: FeedbackClientWrapperProps) {
|
||||
if (!feedbackEnabled) return null;
|
||||
export default function FeedbackClientWrapper() {
|
||||
if (!IS_ENABLED) return null;
|
||||
|
||||
return <FeedbackOverlay />;
|
||||
}
|
||||
|
||||
@@ -2,34 +2,28 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { getAppServices } from '@/lib/services/create-services';
|
||||
import { useAnalytics } from './useAnalytics';
|
||||
|
||||
/**
|
||||
* AnalyticsProvider Component
|
||||
*
|
||||
* Automatically tracks pageviews on client-side route changes.
|
||||
* 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() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { trackPageview } = useAnalytics();
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
const services = getAppServices();
|
||||
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
|
||||
|
||||
// Track pageview with the full URL
|
||||
// The service will relay this to our internal proxy which injects the Website ID
|
||||
services.analytics.trackPageview(url);
|
||||
|
||||
// Services like logger are already sub-initialized in getAppServices()
|
||||
// so we don't need to log here manually.
|
||||
}, [pathname, searchParams]);
|
||||
trackPageview(url);
|
||||
}, [pathname, searchParams, trackPageview]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,73 +1,69 @@
|
||||
'use client';
|
||||
|
||||
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.
|
||||
*
|
||||
* 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() {
|
||||
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(
|
||||
(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') {
|
||||
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(
|
||||
(url?: string) => {
|
||||
services.analytics.trackPageview(url);
|
||||
sendAnalyticsPayload('event', {
|
||||
url: url || window.location.pathname + window.location.search,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[Umami] Tracked pageview:', url ?? 'current location');
|
||||
}
|
||||
},
|
||||
[services]
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,6 +41,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
||||
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
|
||||
}}
|
||||
sizes="100vw"
|
||||
quality={50}
|
||||
priority
|
||||
/>
|
||||
{/* Cinematic Color Grading Overlay */}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
|
||||
let defaultPoster = "/assets/photos/DJI_0048.JPG";
|
||||
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;
|
||||
@@ -80,18 +80,18 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
{/* Background color while video loads */}
|
||||
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
||||
|
||||
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
|
||||
<Image
|
||||
<img
|
||||
key={`img-${pathname}-${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}
|
||||
fill
|
||||
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
||||
sizes="100vw"
|
||||
quality={25}
|
||||
priority
|
||||
className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none"
|
||||
decoding="sync"
|
||||
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 */}
|
||||
{videoUrl && (
|
||||
|
||||
@@ -47,19 +47,6 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
const isDraggingRef = React.useRef(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 title = props.title || data?.title || t('title');
|
||||
|
||||
@@ -23,7 +23,7 @@ export function EUFundingBadge() {
|
||||
alt="European Union Background"
|
||||
fill
|
||||
className="object-cover object-top"
|
||||
priority
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
|
||||
{/* Sweeping intense white light */}
|
||||
<m.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
||||
initial={{ left: '-150%' }}
|
||||
animate={{ left: '250%' }}
|
||||
initial={{ x: '-150%' }}
|
||||
animate={{ x: '250%' }}
|
||||
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
|
||||
/>
|
||||
</div>
|
||||
@@ -91,9 +91,9 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
|
||||
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
|
||||
>
|
||||
<m.div
|
||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-dark via-primary to-primary-light"
|
||||
initial={{ width: "0%" }}
|
||||
animate={{ width: "100%" }}
|
||||
className="absolute inset-y-0 left-0 w-full bg-gradient-to-r from-primary-dark via-primary to-primary-light origin-left"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
|
||||
/>
|
||||
</m.div>
|
||||
|
||||
@@ -12,12 +12,12 @@ export function PageTransitionShutter() {
|
||||
<AnimatePresence>
|
||||
{isTransitioning && (
|
||||
<m.div
|
||||
key="shutter"
|
||||
key="page-transition-shutter"
|
||||
initial={{ y: '-100%' }}
|
||||
animate={{ y: '0%' }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: '100%' }}
|
||||
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible"
|
||||
transition={{ duration: 0.65, ease: [0.76, 0, 0.24, 1] }}
|
||||
className="fixed left-0 right-0 z-[9998] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden"
|
||||
style={{
|
||||
height: '100vh',
|
||||
top: '0',
|
||||
@@ -56,8 +56,8 @@ export function PageTransitionShutter() {
|
||||
{/* Sweeping intense white light */}
|
||||
<m.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
||||
initial={{ left: '-150%' }}
|
||||
animate={{ left: '250%' }}
|
||||
initial={{ x: '-150%' }}
|
||||
animate={{ x: '250%' }}
|
||||
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,16 @@
|
||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||
import { LazyMotion } from 'framer-motion';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
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 {
|
||||
isTransitioning: boolean;
|
||||
transitionMessage: string | null;
|
||||
@@ -17,8 +24,12 @@ const TransitionContext = createContext<TransitionContextType | undefined>(undef
|
||||
export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
||||
const [hasMountedShutter, setHasMountedShutter] = useState(false);
|
||||
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
|
||||
const isTransitioningRef = React.useRef(isTransitioning);
|
||||
useEffect(() => {
|
||||
@@ -44,6 +55,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
setTransitionMessage(message);
|
||||
setIsTransitioning(true);
|
||||
setHasMountedShutter(true);
|
||||
|
||||
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
||||
setTimeout(() => {
|
||||
@@ -72,6 +84,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
/>
|
||||
</React.Suspense>
|
||||
<LazyMotion features={loadFeatures}>
|
||||
{hasMountedShutter && <DynamicPageTransitionShutter />}
|
||||
{children}
|
||||
</LazyMotion>
|
||||
</TransitionContext.Provider>
|
||||
|
||||
@@ -118,7 +118,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
items={[
|
||||
{
|
||||
title: "Kabelleitungsnetzbau",
|
||||
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
||||
description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)",
|
||||
tag: "Energie",
|
||||
size: "large",
|
||||
href: "/de/kabeltiefbau",
|
||||
|
||||
@@ -8,7 +8,7 @@ layout: "fullBleed"
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
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' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
|
||||
@@ -5,7 +5,9 @@ import { join } from 'path';
|
||||
* 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.
|
||||
*/
|
||||
let cachedFonts: any[] | null = null;
|
||||
export async function getOgFonts() {
|
||||
if (cachedFonts) return cachedFonts;
|
||||
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.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)`,
|
||||
);
|
||||
|
||||
return [
|
||||
cachedFonts = [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: boldFont,
|
||||
@@ -42,28 +44,35 @@ export async function getOgFonts() {
|
||||
style: 'normal' as const,
|
||||
},
|
||||
];
|
||||
return cachedFonts;
|
||||
} catch (error) {
|
||||
console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
let cachedBackground: string | null = null;
|
||||
export function getOgBackground() {
|
||||
if (cachedBackground) return cachedBackground;
|
||||
try {
|
||||
const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg');
|
||||
const bgBase64 = readFileSync(bgPath, 'base64');
|
||||
return `data:image/jpeg;base64,${bgBase64}`;
|
||||
cachedBackground = `data:image/jpeg;base64,${bgBase64}`;
|
||||
return cachedBackground;
|
||||
} catch (err) {
|
||||
console.error('[OG] Failed to load background', err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let cachedLogo: string | null = null;
|
||||
export function getOgLogo() {
|
||||
if (cachedLogo) return cachedLogo;
|
||||
try {
|
||||
const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
|
||||
const logoBase64 = readFileSync(logoPath, 'base64');
|
||||
return `data:image/png;base64,${logoBase64}`;
|
||||
cachedLogo = `data:image/png;base64,${logoBase64}`;
|
||||
return cachedLogo;
|
||||
} catch (err) {
|
||||
console.error('[OG] Failed to load logo', err);
|
||||
return undefined;
|
||||
|
||||
@@ -15,7 +15,6 @@ const nextConfig = {
|
||||
pagesBufferLength: 2,
|
||||
},
|
||||
experimental: {
|
||||
inlineCss: true,
|
||||
staleTimes: {
|
||||
dynamic: 0,
|
||||
static: 30,
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.2.89",
|
||||
"version": "2.4.6",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 4.7 KiB |
BIN
public/assets/videos/web/hero-kabelpflug-poster-mobile.webp
Normal file
BIN
public/assets/videos/web/hero-kabelpflug-poster-mobile.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
public/assets/videos/web/hero-kabelpflug-poster.webp
Normal file
BIN
public/assets/videos/web/hero-kabelpflug-poster.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -39,7 +39,7 @@ describe('Task 6 Content Updates', () => {
|
||||
|
||||
// DE Home
|
||||
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('Mitarbeitende');
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('Task 6 Content Updates', () => {
|
||||
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
||||
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"');
|
||||
|
||||
// EN Kabelnetzbau
|
||||
|
||||
Reference in New Issue
Block a user