Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4e5b44d5e | ||
|
|
0378dde328 | ||
|
|
3202139b1e | ||
|
|
dc5489706d | ||
|
|
e8b8493740 | ||
|
|
ef44ebcf4f | ||
|
|
816aef924e | ||
|
|
1bd9f26356 | ||
|
|
156ae66dec | ||
|
|
b2d1865f40 | ||
|
|
dd363d0a55 | ||
|
|
84f10694bb | ||
|
|
3e2b20ce0e | ||
|
|
4cd919545e | ||
|
|
e5891e8214 | ||
|
|
4e4e44a5fd | ||
|
|
3e37c37863 | ||
|
|
40ec588d52 | ||
|
|
b2c38fca7d |
@@ -400,10 +400,19 @@ jobs:
|
||||
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 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 && docker compose -p '${{ needs.prepare.outputs.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' pull"
|
||||
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'"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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
+1
-11
@@ -60,19 +60,9 @@ RUN pnpm build
|
||||
# Excel generation moved to post-deploy
|
||||
|
||||
# Stage 2: Runner
|
||||
FROM node:20-alpine AS runner
|
||||
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
|
||||
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 PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPageBySlug } from '@/lib/pages';
|
||||
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 contentType = 'image/png';
|
||||
@@ -20,12 +20,16 @@ export default async function Image({
|
||||
}
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
const bg = getOgBackground();
|
||||
const logo = getOgLogo();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title={pageData.frontmatter.title}
|
||||
description={pageData.frontmatter.excerpt}
|
||||
title="Die Experten für Kabelnetzbau"
|
||||
description={`${pageData.frontmatter.title}`}
|
||||
label="Information"
|
||||
image={bg}
|
||||
logo={logo}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPostBySlug } from '@/lib/blog';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
@@ -52,13 +52,16 @@ export default async function Image({
|
||||
if (featuredImage) {
|
||||
base64Image = await fetchImageAsBase64(featuredImage);
|
||||
}
|
||||
const bg = getOgBackground();
|
||||
const logo = getOgLogo();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title={post.frontmatter.title}
|
||||
description={post.frontmatter.excerpt}
|
||||
title="Die Experten für Kabelnetzbau"
|
||||
description={`${post.frontmatter.title}`}
|
||||
label={post.frontmatter.category || 'Blog'}
|
||||
image={base64Image || featuredImage}
|
||||
image={base64Image || featuredImage || bg}
|
||||
logo={logo}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -11,9 +11,17 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
||||
const fonts = await getOgFonts();
|
||||
const bg = getOgBackground();
|
||||
const logo = getOgLogo();
|
||||
|
||||
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,
|
||||
fonts,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -11,12 +11,22 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||
const fonts = await getOgFonts();
|
||||
const bg = getOgBackground();
|
||||
const logo = getOgLogo();
|
||||
|
||||
const title = t('meta.title') || t('title');
|
||||
const description = t('meta.description') || t('subtitle');
|
||||
let title = t('meta.title') || t('title');
|
||||
if (title.startsWith('E-TIB GmbH | ')) {
|
||||
title = title.substring('E-TIB GmbH | '.length);
|
||||
}
|
||||
|
||||
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,
|
||||
fonts,
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function generateStaticParams() {
|
||||
}
|
||||
|
||||
export default async function ContactPage({ params }: ContactPageProps) {
|
||||
console.log('--- RENDERING CONTACT PAGE FOR LOCALE ---');
|
||||
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||
|
||||
+3
-11
@@ -10,7 +10,7 @@ import { getMessages } from 'next-intl/server';
|
||||
import '../../styles/globals.css';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
|
||||
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
|
||||
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import { Inter } from 'next/font/google';
|
||||
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
|
||||
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
||||
|
||||
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;
|
||||
const hasSeenLoader = true; // Disabled to allow static generation and instant LCP (Lighthouse 100)
|
||||
|
||||
return (
|
||||
<html
|
||||
@@ -217,8 +210,7 @@ export default async function Layout(props: {
|
||||
<Footer companyInfo={companyInfo} />
|
||||
<JsonLd />
|
||||
<AnalyticsShell />
|
||||
{/* Annotator vorübergehend deaktiviert auf Staging laut Anforderung */}
|
||||
{/* process.env.TARGET !== 'production' && <AnnotatorClientWrapper /> */}
|
||||
|
||||
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
|
||||
</TransitionProvider>
|
||||
</NextIntlClientProvider>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -10,13 +10,23 @@ export const runtime = 'nodejs';
|
||||
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
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 bg = getOgBackground();
|
||||
const logo = getOgLogo();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title={t('title')}
|
||||
description={t('description')}
|
||||
label="Reliable Energy Infrastructure"
|
||||
title="Die Experten für Kabelnetzbau"
|
||||
description={pageTitle}
|
||||
label="Kabelnetzbau & Infrastruktur"
|
||||
image={bg}
|
||||
logo={logo}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
|
||||
@@ -4,19 +4,16 @@ import { notFound } from 'next/navigation';
|
||||
import { getMdxContent } from '@/lib/mdx';
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [{ locale: 'de' }, { locale: 'en' }];
|
||||
}
|
||||
|
||||
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 HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
|
||||
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
|
||||
const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
|
||||
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 { Button } from '@/components/ui/Button';
|
||||
|
||||
@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
|
||||
description?: string;
|
||||
label?: string;
|
||||
image?: string;
|
||||
logo?: string;
|
||||
mode?: 'dark' | 'light' | 'image';
|
||||
}
|
||||
|
||||
@@ -13,11 +14,11 @@ export function OGImageTemplate({
|
||||
description,
|
||||
label,
|
||||
image,
|
||||
logo,
|
||||
mode = 'dark',
|
||||
}: OGImageTemplateProps) {
|
||||
const primaryBlue = '#001a4d';
|
||||
const accentGreen = '#10a379';
|
||||
const saturatedBlue = '#011dff';
|
||||
const backgroundDark = '#1a1a1a';
|
||||
const primaryGreen = '#0e7a5c';
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
height: '100%',
|
||||
@@ -26,7 +27,7 @@ export function OGImageTemplate({
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
|
||||
backgroundColor: mode === 'light' ? '#ffffff' : backgroundDark,
|
||||
padding: '80px',
|
||||
position: 'relative',
|
||||
fontFamily: 'Inter',
|
||||
@@ -65,7 +66,7 @@ export function OGImageTemplate({
|
||||
left: 0,
|
||||
right: 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>
|
||||
@@ -75,27 +76,40 @@ export function OGImageTemplate({
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-150px',
|
||||
right: '-150px',
|
||||
width: '600px',
|
||||
height: '600px',
|
||||
borderRadius: '300px',
|
||||
backgroundColor: `${accentGreen}15`,
|
||||
top: '-300px',
|
||||
right: '-200px',
|
||||
width: '800px',
|
||||
height: '1200px',
|
||||
backgroundColor: `${primaryGreen}1A`,
|
||||
transform: 'rotate(25deg)',
|
||||
display: 'flex',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 10 }}>
|
||||
{/* Main Content Wrapper */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
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: accentGreen,
|
||||
color: primaryGreen,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.3em',
|
||||
marginBottom: '32px',
|
||||
letterSpacing: '0.25em',
|
||||
marginBottom: '24px',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
@@ -106,12 +120,12 @@ export function OGImageTemplate({
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: title.length > 40 ? '64px' : '82px',
|
||||
fontSize: '72px',
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
lineHeight: '1.05',
|
||||
maxWidth: '950px',
|
||||
marginBottom: '40px',
|
||||
lineHeight: '1.1',
|
||||
maxWidth: '900px',
|
||||
marginBottom: '24px',
|
||||
display: 'flex',
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
@@ -123,38 +137,49 @@ export function OGImageTemplate({
|
||||
{description && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '32px',
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
fontSize: '36px',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
maxWidth: '850px',
|
||||
lineHeight: '1.4',
|
||||
display: 'flex',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{description.length > 160 ? description.substring(0, 157) + '...' : description}
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Brand Footer */}
|
||||
{/* Bottom: Brand Footer */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '80px',
|
||||
left: '80px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{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: accentGreen,
|
||||
backgroundColor: primaryGreen,
|
||||
borderRadius: '3px',
|
||||
marginRight: '24px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!logo && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
@@ -165,11 +190,13 @@ export function OGImageTemplate({
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
KLZ Cables
|
||||
E-TIB GmbH
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Saturated Blue Brand Strip */}
|
||||
{/* Primary Green Brand Strip */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -177,7 +204,7 @@ export function OGImageTemplate({
|
||||
right: 0,
|
||||
width: '12px',
|
||||
height: '100%',
|
||||
backgroundColor: saturatedBlue,
|
||||
backgroundColor: primaryGreen,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/Button';
|
||||
import Image from 'next/image';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { preload } from 'react-dom';
|
||||
|
||||
interface HeroVideoProps {
|
||||
data?: any;
|
||||
@@ -42,10 +41,6 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
|
||||
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
|
||||
|
||||
if (posterSrc) {
|
||||
preload(posterSrc, { as: 'image', fetchPriority: 'high' });
|
||||
}
|
||||
|
||||
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
let interactionTriggered = false;
|
||||
@@ -55,17 +50,19 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
interactionTriggered = true;
|
||||
setVideoSrcLoaded(true);
|
||||
|
||||
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) =>
|
||||
// Cleanup listeners
|
||||
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||
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 })
|
||||
);
|
||||
|
||||
return () => {
|
||||
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) =>
|
||||
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||
window.removeEventListener(event, handleInteraction)
|
||||
);
|
||||
};
|
||||
@@ -76,30 +73,28 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
{/* Background color while video loads */}
|
||||
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
||||
|
||||
{/* Dramatic Vignette & Fade to content */}
|
||||
<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" />
|
||||
|
||||
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
|
||||
<Image
|
||||
key={`img-${pathname}-${posterSrc}`}
|
||||
src={posterSrc}
|
||||
alt={posterAlt}
|
||||
fill
|
||||
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
||||
sizes="100vw"
|
||||
priority
|
||||
fetchPriority="high"
|
||||
className="object-cover"
|
||||
/>
|
||||
{videoUrl && videoSrcLoaded && (
|
||||
|
||||
{/* Render video on top if available, but defer src to avoid blocking LCP */}
|
||||
{videoUrl && (
|
||||
<video
|
||||
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"
|
||||
src={videoUrl}
|
||||
src={videoSrcLoaded ? videoUrl : undefined}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -107,15 +107,15 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full 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="relative z-10">
|
||||
<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
|
||||
ref={containerRef}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseUp={onMouseUp}
|
||||
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) => {
|
||||
const imgSrc = ref.image
|
||||
|
||||
@@ -15,8 +15,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
||||
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
|
||||
const isTransitioningRef = React.useRef(isTransitioning);
|
||||
@@ -24,18 +22,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
isTransitioningRef.current = 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) => {
|
||||
// Wenn wir bereits navigieren, nichts tun
|
||||
if (isTransitioning) return;
|
||||
@@ -75,11 +61,35 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
|
||||
<React.Suspense fallback={null}>
|
||||
<TransitionRouteWatcher
|
||||
isTransitioningRef={isTransitioningRef}
|
||||
setIsTransitioning={setIsTransitioning}
|
||||
setTransitionMessage={setTransitionMessage}
|
||||
/>
|
||||
</React.Suspense>
|
||||
{children}
|
||||
</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() {
|
||||
const context = useContext(TransitionContext);
|
||||
if (!context) {
|
||||
|
||||
+5303
File diff suppressed because one or more lines are too long
+25
-3
@@ -6,15 +6,15 @@ import { join } from 'path';
|
||||
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
|
||||
*/
|
||||
export async function getOgFonts() {
|
||||
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.ttf');
|
||||
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.ttf');
|
||||
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
|
||||
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
|
||||
|
||||
try {
|
||||
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
||||
const boldFontBuffer = readFileSync(boldFontPath);
|
||||
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(
|
||||
boldFontBuffer.byteOffset,
|
||||
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
|
||||
*/
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.2.30",
|
||||
"version": "2.2.30-rc.8",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
Regular → Executable
BIN
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
@@ -9,8 +9,8 @@ echo "🚀 Starting High-Fidelity Local Audit..."
|
||||
|
||||
# 1. Environment and Infrastructure
|
||||
export DOCKER_HOST="unix:///Users/marcmintel/.docker/run/docker.sock"
|
||||
export IMGPROXY_URL="http://klz-imgproxy:8080"
|
||||
export NEXT_URL="http://klz.localhost"
|
||||
export IMGPROXY_URL="http://etib-imgproxy:8080"
|
||||
export NEXT_URL="http://etib.localhost"
|
||||
export NEXT_PUBLIC_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)
|
||||
echo "📦 Starting infrastructure services..."
|
||||
# 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
|
||||
echo "🏗️ Building and starting klz-app (Production)..."
|
||||
# 3. Build and Start etib-app in Production Mode
|
||||
echo "🏗️ Building and starting etib-app (Production)..."
|
||||
# We bypass the dev override by explicitly using the base compose file
|
||||
NEXT_PUBLIC_BASE_URL=$NEXT_URL \
|
||||
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
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Bad Gateway
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
Reference in New Issue
Block a user