Compare commits

..

19 Commits

Author SHA1 Message Date
c4e5b44d5e 2.2.30-rc.8
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 46s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 46s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:47:00 +02:00
0378dde328 feat(og): Standardize OG images across all subpages with logo and background 2026-06-22 13:46:50 +02:00
3202139b1e 2.2.30-rc.7
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m12s
Build & Deploy / 🚀 Deploy (push) Successful in 51s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:05:39 +02:00
dc5489706d fix(og): Add missing title and description variables 2026-06-22 13:05:33 +02:00
e8b8493740 2.2.30-rc.6
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m40s
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 2s
2026-06-22 12:49:57 +02:00
ef44ebcf4f 2.2.30-rc.5
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 2m55s
Build & Deploy / 🧪 QA (push) Failing after 1m44s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 12:42:55 +02:00
816aef924e 2.2.30-rc.4
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20m43s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 12:05:04 +02:00
1bd9f26356 style(og): improve spacing, truncate text, and switch to flex layout 2026-06-22 12:04:53 +02:00
156ae66dec 2.2.30-rc.3
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m26s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 11:39:23 +02:00
b2d1865f40 fix(og): add missing logo type to OGImageTemplateProps 2026-06-22 11:39:20 +02:00
dd363d0a55 2.2.30-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 11:31:20 +02:00
84f10694bb feat(og): improve design with background image, logo and dynamic geometry 2026-06-22 11:31:19 +02:00
3e2b20ce0e 2.2.30-rc.1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
2026-06-22 10:04:17 +02:00
4cd919545e fix(og): add missing Inter fonts for Satori rendering 2026-06-22 10:04:17 +02:00
e5891e8214 2.2.30-rc.0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m20s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-22 09:12:50 +02:00
4e4e44a5fd feat(og): adapt OG images to E-TIB branding and fix ArrayBuffer font parsing 2026-06-22 09:12:50 +02:00
3e37c37863 fix: useSuspense bailout for TransitionProvider
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m18s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 00:23:53 +02:00
40ec588d52 perf: lighthouse 100 optimization and UI cleanup
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
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 / 🏗️ Build (push) Has been cancelled
2026-06-22 00:20:35 +02:00
b2c38fca7d refactor: improve deployment cleanup logic using explicit project-based teardown and label-based ghost container removal 2026-06-22 00:12:09 +02:00
31 changed files with 43126 additions and 8416 deletions

View File

@@ -400,10 +400,19 @@ jobs:
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
ssh root@alpha.mintel.me "docker system prune -f" ssh root@alpha.mintel.me "docker system prune -f"
ssh root@alpha.mintel.me "docker rm -f \$(docker ps -a -q --filter name=${SLUG}-etib-) 2>/dev/null || true"
# ── Deterministic cleanup: stop the EXACT compose project we're about to redeploy ──
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' down --remove-orphans 2>/dev/null || true"
# ── Safety net: kill ANY container whose Traefik host rule matches our target domain ──
# This catches ghost containers from prior deployments that used a different project name
# (e.g. 'e-tibcom' vs 'etib-production' due to Docker Compose defaulting to dir name)
ssh root@alpha.mintel.me "docker ps -q --filter label=traefik.http.routers.${PROJECT_NAME}.rule 2>/dev/null | xargs -r docker rm -f 2>/dev/null || true"
ssh root@alpha.mintel.me "docker ps --format '{{.Names}}' | grep -E '^e-tibcom-' | xargs -r docker rm -f 2>/dev/null || true"
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin" ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull" ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' pull"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans" ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' up -d --remove-orphans"
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'" ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────

View File

@@ -1 +0,0 @@
{"chromeFlags":" --headless=new"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -60,19 +60,9 @@ RUN pnpm build
# Excel generation moved to post-deploy # Excel generation moved to post-deploy
# Stage 2: Runner # Stage 2: Runner
FROM node:20-alpine AS runner FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
WORKDIR /app WORKDIR /app
# Install dependencies for health checks and system compatibility
RUN apk add --no-cache libc6-compat curl
# Create nextjs user and group
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs && \
chown -R nextjs:nodejs /app
USER nextjs
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
ENV PORT=3000 ENV PORT=3000
ENV NODE_ENV=production ENV NODE_ENV=production

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getPageBySlug } from '@/lib/pages'; import { getPageBySlug } from '@/lib/pages';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -20,12 +20,16 @@ export default async function Image({
} }
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate <OGImageTemplate
title={pageData.frontmatter.title} title="Die Experten für Kabelnetzbau"
description={pageData.frontmatter.excerpt} description={`${pageData.frontmatter.title}`}
label="Information" label="Information"
image={bg}
logo={logo}
/>, />,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/blog'; import { getPostBySlug } from '@/lib/blog';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { SITE_URL } from '@/lib/schema'; import { SITE_URL } from '@/lib/schema';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
@@ -52,13 +52,16 @@ export default async function Image({
if (featuredImage) { if (featuredImage) {
base64Image = await fetchImageAsBase64(featuredImage); base64Image = await fetchImageAsBase64(featuredImage);
} }
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate <OGImageTemplate
title={post.frontmatter.title} title="Die Experten für Kabelnetzbau"
description={post.frontmatter.excerpt} description={`${post.frontmatter.title}`}
label={post.frontmatter.category || 'Blog'} label={post.frontmatter.category || 'Blog'}
image={base64Image || featuredImage} image={base64Image || featuredImage || bg}
logo={logo}
/>, />,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,9 +11,17 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Blog.meta' }); const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />, <OGImageTemplate
title="Die Experten für Kabelnetzbau"
description={t('title')}
label="Blog"
image={bg}
logo={logo}
/>,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,
fonts, fonts,

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,12 +11,22 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Contact' }); const t = await getTranslations({ locale, namespace: 'Contact' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
const title = t('meta.title') || t('title'); let title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle'); if (title.startsWith('E-TIB GmbH | ')) {
title = title.substring('E-TIB GmbH | '.length);
}
return new ImageResponse( return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Contact" />, <OGImageTemplate
title="Die Experten für Kabelnetzbau"
description={title}
label="Contact"
image={bg}
logo={logo}
/>,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,
fonts, fonts,

View File

@@ -57,7 +57,7 @@ export async function generateStaticParams() {
} }
export default async function ContactPage({ params }: ContactPageProps) { export default async function ContactPage({ params }: ContactPageProps) {
console.log('--- RENDERING CONTACT PAGE FOR LOCALE ---');
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'Contact' }); const t = await getTranslations({ locale, namespace: 'Contact' });

View File

@@ -10,7 +10,7 @@ import { getMessages } from 'next-intl/server';
import '../../styles/globals.css'; import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema'; import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper'; import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
import { setRequestLocale } from 'next-intl/server'; import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import { mapFileSlugToTranslated } from '@/lib/slugs'; import { mapFileSlugToTranslated } from '@/lib/slugs';
@@ -175,14 +175,7 @@ export default async function Layout(props: {
// Read directly from process.env — bypasses all abstraction to guarantee correctness // Read directly from process.env — bypasses all abstraction to guarantee correctness
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true'; const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
const hasSeenLoader = true; // Disabled to allow static generation and instant LCP (Lighthouse 100)
const cookieStore = await cookies();
const requestHeadersForBot = await import('next/headers').then(m => m.headers());
const userAgent = requestHeadersForBot.get('user-agent') || '';
const isBot = /Lighthouse|PageSpeed|Googlebot|Chrome-Lighthouse|GTmetrix/i.test(userAgent);
// Skip loader if the user has the cookie OR if it is a performance testing bot
const hasSeenLoader = cookieStore.has('etib_initial_loader_v5') || isBot;
return ( return (
<html <html
@@ -217,8 +210,7 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} /> <Footer companyInfo={companyInfo} />
<JsonLd /> <JsonLd />
<AnalyticsShell /> <AnalyticsShell />
{/* Annotator vorübergehend deaktiviert auf Staging laut Anforderung */}
{/* process.env.TARGET !== 'production' && <AnnotatorClientWrapper /> */}
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />} {feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</TransitionProvider> </TransitionProvider>
</NextIntlClientProvider> </NextIntlClientProvider>

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -10,13 +10,23 @@ export const runtime = 'nodejs';
export default async function Image({ params }: { params: Promise<{ locale: string }> }) { export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Index.meta' }); const t = await getTranslations({ locale, namespace: 'Index.meta' });
let pageTitle = t('title');
if (pageTitle.startsWith('E-TIB GmbH | ')) {
pageTitle = pageTitle.substring('E-TIB GmbH | '.length);
}
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate <OGImageTemplate
title={t('title')} title="Die Experten für Kabelnetzbau"
description={t('description')} description={pageTitle}
label="Reliable Energy Infrastructure" label="Kabelnetzbau & Infrastruktur"
image={bg}
logo={logo}
/>, />,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,

View File

@@ -4,19 +4,16 @@ import { notFound } from 'next/navigation';
import { getMdxContent } from '@/lib/mdx'; import { getMdxContent } from '@/lib/mdx';
import { MDXRemote } from 'next-mdx-remote/rsc'; import { MDXRemote } from 'next-mdx-remote/rsc';
export function generateStaticParams() {
return [{ locale: 'de' }, { locale: 'en' }];
}
import nextDynamic from 'next/dynamic'; import nextDynamic from 'next/dynamic';
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles)); const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid)); const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider)); const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock)); const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock)); const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock));
const HeroSection = nextDynamic(() => import('@/components/blocks/HeroSection').then(mod => mod.HeroSection), { ssr: true }); import { HeroSection } from '@/components/blocks/HeroSection';
import JsonLd from '@/components/JsonLd'; import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';

View File

@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
description?: string; description?: string;
label?: string; label?: string;
image?: string; image?: string;
logo?: string;
mode?: 'dark' | 'light' | 'image'; mode?: 'dark' | 'light' | 'image';
} }
@@ -13,11 +14,11 @@ export function OGImageTemplate({
description, description,
label, label,
image, image,
logo,
mode = 'dark', mode = 'dark',
}: OGImageTemplateProps) { }: OGImageTemplateProps) {
const primaryBlue = '#001a4d'; const backgroundDark = '#1a1a1a';
const accentGreen = '#10a379'; const primaryGreen = '#0e7a5c';
const saturatedBlue = '#011dff';
const containerStyle: React.CSSProperties = { const containerStyle: React.CSSProperties = {
height: '100%', height: '100%',
@@ -26,7 +27,7 @@ export function OGImageTemplate({
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue, backgroundColor: mode === 'light' ? '#ffffff' : backgroundDark,
padding: '80px', padding: '80px',
position: 'relative', position: 'relative',
fontFamily: 'Inter', fontFamily: 'Inter',
@@ -65,7 +66,7 @@ export function OGImageTemplate({
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.95), rgba(0,26,77,0.6))', background: 'linear-gradient(135deg, rgba(26,26,26,0.98) 0%, rgba(26,26,26,0.85) 40%, rgba(14,122,92,0.4) 100%)',
}} }}
/> />
</div> </div>
@@ -75,101 +76,127 @@ export function OGImageTemplate({
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: '-150px', top: '-300px',
right: '-150px', right: '-200px',
width: '600px', width: '800px',
height: '600px', height: '1200px',
borderRadius: '300px', backgroundColor: `${primaryGreen}1A`,
backgroundColor: `${accentGreen}15`, transform: 'rotate(25deg)',
display: 'flex', display: 'flex',
}} }}
/> />
<div style={{ display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 10 }}> {/* Main Content Wrapper */}
{/* Label / Category */}
{label && (
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: accentGreen,
textTransform: 'uppercase',
letterSpacing: '0.3em',
marginBottom: '32px',
display: 'flex',
}}
>
{label}
</div>
)}
{/* Title */}
<div
style={{
fontSize: title.length > 40 ? '64px' : '82px',
fontWeight: 700,
color: 'white',
lineHeight: '1.05',
maxWidth: '950px',
marginBottom: '40px',
display: 'flex',
letterSpacing: '-0.02em',
}}
>
{title}
</div>
{/* Description */}
{description && (
<div
style={{
fontSize: '32px',
color: 'rgba(255,255,255,0.7)',
maxWidth: '850px',
lineHeight: '1.4',
display: 'flex',
fontWeight: 400,
}}
>
{description.length > 160 ? description.substring(0, 157) + '...' : description}
</div>
)}
</div>
{/* Brand Footer */}
<div <div
style={{ style={{
position: 'absolute',
bottom: '80px',
left: '80px',
display: 'flex', display: 'flex',
alignItems: 'center', flexDirection: 'column',
justifyContent: 'space-between',
height: '100%',
width: '100%',
position: 'relative',
zIndex: 10,
}} }}
> >
{/* Top: Text Content */}
<div style={{ display: 'flex', flexDirection: 'column' }}>
{/* Label / Category */}
{label && (
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: primaryGreen,
textTransform: 'uppercase',
letterSpacing: '0.25em',
marginBottom: '24px',
display: 'flex',
}}
>
{label}
</div>
)}
{/* Title */}
<div
style={{
fontSize: '72px',
fontWeight: 700,
color: 'white',
lineHeight: '1.1',
maxWidth: '900px',
marginBottom: '24px',
display: 'flex',
letterSpacing: '-0.02em',
}}
>
{title}
</div>
{/* Description */}
{description && (
<div
style={{
fontSize: '36px',
color: 'rgba(255,255,255,0.85)',
maxWidth: '850px',
lineHeight: '1.4',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontWeight: 400,
}}
>
{description}
</div>
)}
</div>
{/* Bottom: Brand Footer */}
<div <div
style={{ style={{
width: '80px',
height: '6px',
backgroundColor: accentGreen,
borderRadius: '3px',
marginRight: '24px',
}}
/>
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.15em',
display: 'flex', display: 'flex',
alignItems: 'center',
}} }}
> >
KLZ Cables {logo ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={logo}
alt="E-TIB GmbH"
height="56"
style={{ marginRight: '24px', objectFit: 'contain' }}
/>
) : (
<div
style={{
width: '80px',
height: '6px',
backgroundColor: primaryGreen,
borderRadius: '3px',
marginRight: '24px',
}}
/>
)}
{!logo && (
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.15em',
display: 'flex',
}}
>
E-TIB GmbH
</div>
)}
</div> </div>
</div> </div>
{/* Saturated Blue Brand Strip */} {/* Primary Green Brand Strip */}
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -177,7 +204,7 @@ export function OGImageTemplate({
right: 0, right: 0,
width: '12px', width: '12px',
height: '100%', height: '100%',
backgroundColor: saturatedBlue, backgroundColor: primaryGreen,
}} }}
/> />
</div> </div>

View File

@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/Button';
import Image from 'next/image'; import Image from 'next/image';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { preload } from 'react-dom';
interface HeroVideoProps { interface HeroVideoProps {
data?: any; data?: any;
@@ -42,10 +41,6 @@ export function HeroVideo(props: HeroVideoProps) {
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project'); const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`; const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
if (posterSrc) {
preload(posterSrc, { as: 'image', fetchPriority: 'high' });
}
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false); const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
useEffect(() => { useEffect(() => {
let interactionTriggered = false; let interactionTriggered = false;
@@ -55,17 +50,19 @@ export function HeroVideo(props: HeroVideoProps) {
interactionTriggered = true; interactionTriggered = true;
setVideoSrcLoaded(true); setVideoSrcLoaded(true);
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) => // Cleanup listeners
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction) window.removeEventListener(event, handleInteraction)
); );
}; };
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) => // Listen for any user interaction
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.addEventListener(event, handleInteraction, { passive: true, once: true }) window.addEventListener(event, handleInteraction, { passive: true, once: true })
); );
return () => { return () => {
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) => ['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction) window.removeEventListener(event, handleInteraction)
); );
}; };
@@ -76,30 +73,28 @@ 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" />
{/* Dramatic Vignette & Fade to content */} {/* ALWAYS render the Next.js optimized Image as the LCP element */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-transparent via-black/20 to-black/80 z-[2]" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/60 to-transparent z-[3]" />
{/* Top Fade for Header Navigation Readability */}
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
<Image <Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc} src={posterSrc}
alt={posterAlt} alt={posterAlt}
fill fill
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
priority priority
fetchPriority="high"
className="object-cover"
/> />
{videoUrl && videoSrcLoaded && (
{/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && (
<video <video
key={`${pathname}-${videoUrl}`} key={`${pathname}-${videoUrl}`}
className="absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90" className="absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl} src={videoSrcLoaded ? videoUrl : undefined}
autoPlay autoPlay
muted muted
loop loop
playsInline playsInline
preload="none"
/> />
)} )}

View File

@@ -107,15 +107,15 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
</div> </div>
</div> </div>
<div className="w-full relative z-10"> <div className="relative z-10">
<div className="absolute right-0 top-0 bottom-0 w-24 bg-gradient-to-l from-neutral-dark to-transparent z-20 pointer-events-none" /> <div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
<div <div
ref={containerRef} ref={containerRef}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 pl-4 sm:pl-[1.5rem] md:pl-[2rem] lg:pl-[max(2.5rem,calc(50vw-512px+40px))] xl:pl-[max(3rem,calc(50vw-640px+48px))] 2xl:pl-[max(4rem,calc(50vw-700px+64px))] 3xl:pl-[max(4rem,calc(50vw-800px+64px))] pr-16 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`} className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 px-6 md:px-[max(2rem,calc((100vw-1280px)/2))] lg:px-[max(2.5rem,calc((100vw-1280px)/2))] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
> >
{references.map((ref, i) => { {references.map((ref, i) => {
const imgSrc = ref.image const imgSrc = ref.image

View File

@@ -15,8 +15,6 @@ 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 router = useRouter(); const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// 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);
@@ -24,18 +22,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
isTransitioningRef.current = isTransitioning; isTransitioningRef.current = isTransitioning;
}, [isTransitioning]); }, [isTransitioning]);
// Watch for route changes to complete the transition
useEffect(() => {
if (isTransitioningRef.current) {
// The route has actually changed in the browser, or at least the RSC payload arrived
const timer = setTimeout(() => {
setIsTransitioning(false);
setTimeout(() => setTransitionMessage(null), 300);
}, 100);
return () => clearTimeout(timer);
}
}, [pathname, searchParams]); // Run when pathname or search parameters change
const startTransition = useCallback((href: string, message: string | null = null) => { const startTransition = useCallback((href: string, message: string | null = null) => {
// Wenn wir bereits navigieren, nichts tun // Wenn wir bereits navigieren, nichts tun
if (isTransitioning) return; if (isTransitioning) return;
@@ -75,11 +61,35 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
return ( return (
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}> <TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
<React.Suspense fallback={null}>
<TransitionRouteWatcher
isTransitioningRef={isTransitioningRef}
setIsTransitioning={setIsTransitioning}
setTransitionMessage={setTransitionMessage}
/>
</React.Suspense>
{children} {children}
</TransitionContext.Provider> </TransitionContext.Provider>
); );
} }
function TransitionRouteWatcher({ isTransitioningRef, setIsTransitioning, setTransitionMessage }: any) {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (isTransitioningRef.current) {
const timer = setTimeout(() => {
setIsTransitioning(false);
setTimeout(() => setTransitionMessage(null), 300);
}, 100);
return () => clearTimeout(timer);
}
}, [pathname, searchParams, setIsTransitioning, setTransitionMessage, isTransitioningRef]);
return null;
}
export function useTransition() { export function useTransition() {
const context = useContext(TransitionContext); const context = useContext(TransitionContext);
if (!context) { if (!context) {

5303
lh-report.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -6,15 +6,15 @@ import { join } from 'path';
* 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.
*/ */
export async function getOgFonts() { export async function getOgFonts() {
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.ttf'); const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.ttf'); const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
try { try {
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`); console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
const boldFontBuffer = readFileSync(boldFontPath); const boldFontBuffer = readFileSync(boldFontPath);
const regularFontBuffer = readFileSync(regularFontPath); const regularFontBuffer = readFileSync(regularFontPath);
// Satori (Vercel OG) strictly requires an ArrayBuffer, not a Node Buffer view. // Satori strictly requires an ArrayBuffer.
const boldFont = boldFontBuffer.buffer.slice( const boldFont = boldFontBuffer.buffer.slice(
boldFontBuffer.byteOffset, boldFontBuffer.byteOffset,
boldFontBuffer.byteOffset + boldFontBuffer.byteLength, boldFontBuffer.byteOffset + boldFontBuffer.byteLength,
@@ -48,6 +48,28 @@ export async function getOgFonts() {
} }
} }
export function getOgBackground() {
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}`;
} catch (err) {
console.error('[OG] Failed to load background', err);
return undefined;
}
}
export function getOgLogo() {
try {
const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
const logoBase64 = readFileSync(logoPath, 'base64');
return `data:image/png;base64,${logoBase64}`;
} catch (err) {
console.error('[OG] Failed to load logo', err);
return undefined;
}
}
/** /**
* Common configuration for OG images * Common configuration for OG images
*/ */

View File

@@ -138,7 +138,7 @@
"prepare": "husky", "prepare": "husky",
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"version": "2.2.30", "version": "2.2.30-rc.8",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",

BIN
public/assets/photos/DJI_0048.JPG Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 7.8 MiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,8 +9,8 @@ echo "🚀 Starting High-Fidelity Local Audit..."
# 1. Environment and Infrastructure # 1. Environment and Infrastructure
export DOCKER_HOST="unix:///Users/marcmintel/.docker/run/docker.sock" export DOCKER_HOST="unix:///Users/marcmintel/.docker/run/docker.sock"
export IMGPROXY_URL="http://klz-imgproxy:8080" export IMGPROXY_URL="http://etib-imgproxy:8080"
export NEXT_URL="http://klz.localhost" export NEXT_URL="http://etib.localhost"
export NEXT_PUBLIC_CI=true export NEXT_PUBLIC_CI=true
export CI=true export CI=true
@@ -20,14 +20,14 @@ docker volume create klz_db_data 2>/dev/null || true
# 2. Start infra services (DB, CMS, Gatekeeper) # 2. Start infra services (DB, CMS, Gatekeeper)
echo "📦 Starting infrastructure services..." echo "📦 Starting infrastructure services..."
# Using --remove-orphans to ensure a clean state # Using --remove-orphans to ensure a clean state
docker-compose up -d --remove-orphans klz-db klz-gatekeeper docker-compose up -d --remove-orphans etib-db etib-gatekeeper
# 3. Build and Start klz-app in Production Mode # 3. Build and Start etib-app in Production Mode
echo "🏗️ Building and starting klz-app (Production)..." echo "🏗️ Building and starting etib-app (Production)..."
# We bypass the dev override by explicitly using the base compose file # We bypass the dev override by explicitly using the base compose file
NEXT_PUBLIC_BASE_URL=$NEXT_URL \ NEXT_PUBLIC_BASE_URL=$NEXT_URL \
NEXT_PUBLIC_CI=true \ NEXT_PUBLIC_CI=true \
docker-compose -f docker-compose.yml up -d --build klz-app docker-compose -f docker-compose.yml up -d --build etib-app
# 4. Wait for application to be ready # 4. Wait for application to be ready
echo "⏳ Waiting for application to be healthy..." echo "⏳ Waiting for application to be healthy..."
@@ -54,4 +54,4 @@ echo "♿ Executing WCAG Audit..."
NEXT_PUBLIC_BASE_URL=$NEXT_URL PAGESPEED_LIMIT=10 pnpm run check:wcag "$NEXT_URL" NEXT_PUBLIC_BASE_URL=$NEXT_URL PAGESPEED_LIMIT=10 pnpm run check:wcag "$NEXT_URL"
echo "✨ Audit completed! Summary above." echo "✨ Audit completed! Summary above."
echo "💡 You can stop the production app with: docker-compose stop klz-app" echo "💡 You can stop the production app with: docker-compose stop etib-app"

View File

@@ -1 +0,0 @@
Bad Gateway

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB