Compare commits
19 Commits
chore/rest
...
perf/light
| 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
|
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'"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"chromeFlags":" --headless=new"}
|
|
||||||
2895
.lighthouseci/lhr-1782063683063.html
Normal file
2895
.lighthouseci/lhr-1782063683063.html
Normal file
File diff suppressed because one or more lines are too long
13346
.lighthouseci/lhr-1782063683063.json
Normal file
13346
.lighthouseci/lhr-1782063683063.json
Normal file
File diff suppressed because one or more lines are too long
2895
.lighthouseci/lhr-1782063711742.html
Normal file
2895
.lighthouseci/lhr-1782063711742.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2895
.lighthouseci/lhr-1782063741217.html
Normal file
2895
.lighthouseci/lhr-1782063741217.html
Normal file
File diff suppressed because one or more lines are too long
13289
.lighthouseci/lhr-1782063741217.json
Normal file
13289
.lighthouseci/lhr-1782063741217.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
Dockerfile
12
Dockerfile
@@ -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
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|||||||
@@ -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,27 +76,40 @@ 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 */}
|
||||||
|
<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 / Category */}
|
||||||
{label && (
|
{label && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: accentGreen,
|
color: primaryGreen,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.3em',
|
letterSpacing: '0.25em',
|
||||||
marginBottom: '32px',
|
marginBottom: '24px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -106,12 +120,12 @@ export function OGImageTemplate({
|
|||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: title.length > 40 ? '64px' : '82px',
|
fontSize: '72px',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: 'white',
|
color: 'white',
|
||||||
lineHeight: '1.05',
|
lineHeight: '1.1',
|
||||||
maxWidth: '950px',
|
maxWidth: '900px',
|
||||||
marginBottom: '40px',
|
marginBottom: '24px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
letterSpacing: '-0.02em',
|
letterSpacing: '-0.02em',
|
||||||
}}
|
}}
|
||||||
@@ -123,38 +137,49 @@ export function OGImageTemplate({
|
|||||||
{description && (
|
{description && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '32px',
|
fontSize: '36px',
|
||||||
color: 'rgba(255,255,255,0.7)',
|
color: 'rgba(255,255,255,0.85)',
|
||||||
maxWidth: '850px',
|
maxWidth: '850px',
|
||||||
lineHeight: '1.4',
|
lineHeight: '1.4',
|
||||||
display: 'flex',
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 3,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{description.length > 160 ? description.substring(0, 157) + '...' : description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Brand Footer */}
|
{/* Bottom: Brand Footer */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
bottom: '80px',
|
|
||||||
left: '80px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
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
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '80px',
|
width: '80px',
|
||||||
height: '6px',
|
height: '6px',
|
||||||
backgroundColor: accentGreen,
|
backgroundColor: primaryGreen,
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
marginRight: '24px',
|
marginRight: '24px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{!logo && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
@@ -165,11 +190,13 @@ export function OGImageTemplate({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
KLZ Cables
|
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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
5303
lh-report.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
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
@@ -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"
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Bad Gateway
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
Reference in New Issue
Block a user