Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4e5b44d5e | ||
|
|
0378dde328 | ||
|
|
3202139b1e | ||
|
|
dc5489706d | ||
|
|
e8b8493740 | ||
|
|
ef44ebcf4f | ||
|
|
816aef924e | ||
|
|
1bd9f26356 | ||
|
|
156ae66dec | ||
|
|
b2d1865f40 | ||
|
|
dd363d0a55 | ||
|
|
84f10694bb | ||
|
|
3e2b20ce0e | ||
|
|
4cd919545e | ||
|
|
e5891e8214 | ||
|
|
4e4e44a5fd | ||
|
|
3e37c37863 | ||
|
|
40ec588d52 | ||
|
|
b2c38fca7d | ||
|
|
1b6f1d9405 | ||
|
|
6cc9ccdea8 | ||
|
|
aafc705f0e | ||
|
|
691f02a5b8 | ||
|
|
4b04178f01 | ||
|
|
f76940dc34 | ||
|
|
2653bd2569 |
+1
-1
@@ -12,7 +12,7 @@ NODE_ENV=production
|
|||||||
NEXT_PUBLIC_BASE_URL=https://e-tib.com
|
NEXT_PUBLIC_BASE_URL=https://e-tib.com
|
||||||
|
|
||||||
# Analytics (Umami)
|
# Analytics (Umami)
|
||||||
UMAMI_WEBSITE_ID=
|
UMAMI_WEBSITE_ID=d773ea10-a3b3-4ccf-9024-987e14c4d669
|
||||||
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
||||||
|
|
||||||
# Error Tracking (GlitchTip/Sentry)
|
# Error Tracking (GlitchTip/Sentry)
|
||||||
|
|||||||
@@ -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'"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
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
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"https://e-tib.com/en": "https://storage.googleapis.com/lighthouse-infrastructure.appspot.com/reports/1782063743188-8000.report.html"
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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,9 +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 hasSeenLoader = cookieStore.has('etib_initial_loader_v5');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
@@ -212,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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -4,17 +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 const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
import nextDynamic from 'next/dynamic';
|
import nextDynamic from 'next/dynamic';
|
||||||
|
|
||||||
const HomeHero = nextDynamic(() => import('@/components/blocks/HeroVideo').then(mod => mod.HeroVideo), { ssr: true });
|
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';
|
||||||
|
|||||||
+111
-84
@@ -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>
|
||||||
|
|||||||
@@ -43,11 +43,29 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
|
|
||||||
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
|
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Delay loading the video until after the page has visually painted the LCP image
|
let interactionTriggered = false;
|
||||||
const timer = setTimeout(() => {
|
|
||||||
|
const handleInteraction = () => {
|
||||||
|
if (interactionTriggered) return;
|
||||||
|
interactionTriggered = true;
|
||||||
setVideoSrcLoaded(true);
|
setVideoSrcLoaded(true);
|
||||||
}, 500);
|
|
||||||
return () => clearTimeout(timer);
|
// Cleanup listeners
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||||
|
window.removeEventListener(event, handleInteraction)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for any user interaction
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||||
|
window.addEventListener(event, handleInteraction, { passive: true, once: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||||
|
window.removeEventListener(event, handleInteraction)
|
||||||
|
);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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
File diff suppressed because one or more lines are too long
+36
-3
@@ -11,10 +11,21 @@ export async function getOgFonts() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
||||||
const boldFont = readFileSync(boldFontPath);
|
const boldFontBuffer = readFileSync(boldFontPath);
|
||||||
const regularFont = readFileSync(regularFontPath);
|
const regularFontBuffer = readFileSync(regularFontPath);
|
||||||
|
|
||||||
|
// Satori strictly requires an ArrayBuffer.
|
||||||
|
const boldFont = boldFontBuffer.buffer.slice(
|
||||||
|
boldFontBuffer.byteOffset,
|
||||||
|
boldFontBuffer.byteOffset + boldFontBuffer.byteLength,
|
||||||
|
);
|
||||||
|
const regularFont = regularFontBuffer.buffer.slice(
|
||||||
|
regularFontBuffer.byteOffset,
|
||||||
|
regularFontBuffer.byteOffset + regularFontBuffer.byteLength,
|
||||||
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`,
|
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -37,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
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -138,7 +138,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.23",
|
"version": "2.2.30-rc.8",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user