diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index ceefde90..c4e8cff1 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -406,11 +406,79 @@ jobs:
run: pnpm run check:og
# ──────────────────────────────────────────────────────────────────────────────
- # JOB 6: Notifications
+ # JOB 6: Lighthouse (Performance & Accessibility)
+ # ──────────────────────────────────────────────────────────────────────────────
+ lighthouse:
+ name: ⚡ Lighthouse
+ needs: [prepare, deploy]
+ if: success() && needs.prepare.outputs.target != 'skip'
+ runs-on: docker
+ container:
+ image: catthehacker/ubuntu:act-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v3
+ with:
+ version: 10
+ - name: 🔐 Registry Auth
+ run: |
+ echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
+ echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+ - name: 🔍 Install Chromium (Native & ARM64)
+ run: |
+ apt-get update
+ apt-get install -y gnupg wget ca-certificates
+
+ # Detect OS
+ OS_ID=$(. /etc/os-release && echo $ID)
+ CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
+
+ if [ "$OS_ID" = "debian" ]; then
+ echo "🎯 Debian detected - installing native chromium"
+ apt-get install -y chromium
+ else
+ echo "🎯 Ubuntu detected - adding xtradeb PPA"
+ mkdir -p /etc/apt/keyrings
+ KEY_ID="82BB6851C64F6880"
+
+ # Fetch PPA key
+ wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
+
+ # Add PPA repository
+ echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
+
+ # PRIORITY PINNING: Force PPA over Snap-dummy
+ printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
+
+ apt-get update
+ apt-get install -y --allow-downgrades chromium
+ fi
+
+ # Standardize binary paths
+ [ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome
+ [ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
+ - name: ⚡ Run Lighthouse CI
+ env:
+ NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
+ GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
+ CHROME_PATH: /usr/bin/chromium
+ PAGESPEED_LIMIT: 8
+ run: pnpm run pagespeed:test
+
+ # ──────────────────────────────────────────────────────────────────────────────
+ # JOB 7: Notifications
# ──────────────────────────────────────────────────────────────────────────────
notifications:
name: 🔔 Notify
- needs: [prepare, deploy, smoke_test]
+ needs: [prepare, deploy, smoke_test, lighthouse]
if: always()
runs-on: docker
container:
diff --git a/.gitignore b/.gitignore
index 571b25dd..6e743d84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,11 @@ node_modules
.next
.DS_Store
+# Lighthouse CI
+.lighthouseci/
+lighthouserc.cjs
+.lighthouserc.json
+
# Directus
directus/uploads
!directus/extensions/
diff --git a/Dockerfile b/Dockerfile
index b0e62dea..aa2e3a1c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,6 +5,7 @@ WORKDIR /app
# Arguments for build-time configuration
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_TARGET
+ARG NEXT_PUBLIC_IMGPROXY_URL
ARG DIRECTUS_URL
ARG UMAMI_WEBSITE_ID
ARG UMAMI_API_ENDPOINT
@@ -13,6 +14,7 @@ ARG NPM_TOKEN
# Environment variables for Next.js build
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
+ENV NEXT_PUBLIC_IMGPROXY_URL=$NEXT_PUBLIC_IMGPROXY_URL
ENV DIRECTUS_URL=$DIRECTUS_URL
ENV UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx
index 5cc8739f..6c885dfb 100644
--- a/app/[locale]/blog/[slug]/page.tsx
+++ b/app/[locale]/blog/[slug]/page.tsx
@@ -32,11 +32,6 @@ export async function generateMetadata({ params }: BlogPostProps): Promise
-
-
-
-
+
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index a388987d..67b6864c 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -79,7 +79,9 @@ export async function generateMetadata({
}
const title = t('title') || 'KLZ Cables';
- const description = t('description') || '';
+ const description =
+ t('description') ||
+ 'Ihr Experte für hochwertige Stromkabel, Mittelspannungslösungen und Solarkabel. Zuverlässige Infrastruktur für eine grüne Energiezukunft.';
return {
title,
diff --git a/components/analytics/AnalyticsShell.tsx b/components/analytics/AnalyticsShell.tsx
new file mode 100644
index 00000000..bda24abe
--- /dev/null
+++ b/components/analytics/AnalyticsShell.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import dynamic from 'next/dynamic';
+import { Suspense } from 'react';
+
+const DynamicAnalyticsProvider = dynamic(() => import('./AnalyticsProvider'), {
+ ssr: false,
+});
+const DynamicScrollDepthTracker = dynamic(() => import('./ScrollDepthTracker'), {
+ ssr: false,
+});
+
+export default function AnalyticsShell() {
+ return (
+
+
+
+
+ );
+}
diff --git a/components/home/GallerySection.tsx b/components/home/GallerySection.tsx
index 9e6495d8..f13b3602 100644
--- a/components/home/GallerySection.tsx
+++ b/components/home/GallerySection.tsx
@@ -49,6 +49,7 @@ export default function GallerySection() {
fill
className="object-cover transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
+ loading="lazy"
/>
diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx
index 44fa1f14..c11e3758 100644
--- a/components/home/Hero.tsx
+++ b/components/home/Hero.tsx
@@ -130,19 +130,19 @@ const containerVariants = {
visible: {
opacity: 1,
transition: {
- staggerChildren: 0.12,
- delayChildren: 0.4,
+ staggerChildren: 0.1,
+ delayChildren: 0.1,
},
},
} as const;
const headingVariants = {
- hidden: { opacity: 0, y: 60, scale: 0.85 },
+ hidden: { opacity: 1, y: 30, scale: 0.95 },
visible: {
opacity: 1,
y: 0,
scale: 1,
- transition: { duration: 1.2, ease: [0.25, 0.46, 0.45, 0.94] },
+ transition: { duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] },
},
} as const;
diff --git a/components/home/RecentPosts.tsx b/components/home/RecentPosts.tsx
index ec8834be..ddcad274 100644
--- a/components/home/RecentPosts.tsx
+++ b/components/home/RecentPosts.tsx
@@ -44,6 +44,7 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
fill
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, 33vw"
+ loading="lazy"
/>
{post.frontmatter.category && (
diff --git a/config/lighthouserc.json b/config/lighthouserc.json
new file mode 100644
index 00000000..4b94d952
--- /dev/null
+++ b/config/lighthouserc.json
@@ -0,0 +1,52 @@
+{
+ "ci": {
+ "collect": {
+ "numberOfRuns": 1,
+ "settings": {
+ "preset": "desktop",
+ "onlyCategories": ["performance", "accessibility", "best-practices", "seo"],
+ "chromeFlags": "--no-sandbox --disable-setuid-sandbox"
+ }
+ },
+ "assert": {
+ "assertions": {
+ "categories:performance": [
+ "error",
+ {
+ "minScore": 0.8
+ }
+ ],
+ "categories:accessibility": [
+ "error",
+ {
+ "minScore": 0.9
+ }
+ ],
+ "categories:best-practices": [
+ "error",
+ {
+ "minScore": 0.9
+ }
+ ],
+ "categories:seo": [
+ "error",
+ {
+ "minScore": 0.9
+ }
+ ],
+ "first-contentful-paint": [
+ "warn",
+ {
+ "maxNumericValue": 2000
+ }
+ ],
+ "interactive": [
+ "warn",
+ {
+ "maxNumericValue": 3500
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/data/blog/de/100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur.mdx b/data/blog/de/100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur.mdx
index a413613f..c20cd784 100644
--- a/data/blog/de/100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur.mdx
+++ b/data/blog/de/100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur.mdx
@@ -4,6 +4,7 @@ date: '2025-03-31T12:00:34'
featuredImage: /uploads/2025/02/image_fx_-6.webp
locale: de
category: Kabel Technologie
+excerpt: Die Energiewende braucht leistungsfähige Netze. Erfahren Sie, warum Investitionen in die Kabelinfrastruktur der Schlüssel zu 100 % erneuerbarer Energie sind.
---
# 100 % erneuerbare Energie? Nur mit der richtigen Kabelinfrastruktur!
Die Vision ist klar: Ein Europa, das seinen Strom zu 100 % aus erneuerbaren Energien gewinnt. Doch während Solar- und Windparks boomen, hinkt der Ausbau der Stromnetze hinterher. Die Ursache? Eine Infrastruktur, die für fossile Kraftwerke gebaut wurde und mit den neuen Anforderungen nicht Schritt hält.
diff --git a/docker-compose.yml b/docker-compose.yml
index 479da9a7..a074f861 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,6 +5,7 @@ services:
dockerfile: Dockerfile
args:
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
+ NEXT_PUBLIC_IMGPROXY_URL: ${NEXT_PUBLIC_IMGPROXY_URL}
DIRECTUS_URL: ${DIRECTUS_URL}
image: registry.infra.mintel.me/mintel/klz-cables.com:${IMAGE_TAG:-latest}
restart: unless-stopped
@@ -152,6 +153,33 @@ services:
networks:
- default
+ klz-imgproxy:
+ image: darthsim/imgproxy:latest
+ restart: unless-stopped
+ networks:
+ - default
+ - infra
+ extra_hosts:
+ - "klz.localhost:host-gateway"
+ - "cms.klz.localhost:host-gateway"
+ - "host.docker.internal:host-gateway"
+ environment:
+ IMGPROXY_URL_MAPPING: "http://klz.localhost/:http://klz-app:3000/,http://cms.klz.localhost/:http://klz-cms:8055/"
+ IMGPROXY_USE_ETAG: "true"
+ IMGPROXY_MAX_SRC_RESOLUTION: 20
+ IMGPROXY_ALLOWED_NETWORKS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
+ IMGPROXY_IGNORE_SSL_ERRORS: "true"
+ IMGPROXY_DEBUG: "true"
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.rule=Host(`img.${TRAEFIK_HOST:-klz.localhost}`)"
+ - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.entrypoints=web"
+ - "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.service=${PROJECT_NAME:-klz}-imgproxy-svc"
+ - "traefik.http.services.${PROJECT_NAME:-klz}-imgproxy-svc.loadbalancer.server.port=8080"
+ - "traefik.docker.network=infra"
+ - "caddy=http://img.${TRAEFIK_HOST:-klz.localhost}"
+ - "caddy.reverse_proxy={{upstreams 8080}}"
+
networks:
default:
name: ${PROJECT_NAME:-klz-cables}-internal
diff --git a/lib/imgproxy-loader.ts b/lib/imgproxy-loader.ts
new file mode 100644
index 00000000..ab510b1d
--- /dev/null
+++ b/lib/imgproxy-loader.ts
@@ -0,0 +1,27 @@
+import { getImgproxyUrl } from './imgproxy';
+
+/**
+ * Next.js Image Loader for imgproxy
+ *
+ * @param {Object} props - properties from Next.js Image component
+ * @param {string} props.src - The source image URL
+ * @param {number} props.width - The desired image width
+ * @param {number} props.quality - The desired image quality (ignored for now as imgproxy handles it)
+ */
+export default function imgproxyLoader({
+ src,
+ width,
+ _quality,
+}: {
+ src: string;
+ width: number;
+ _quality?: number;
+}) {
+ // We use the width provided by Next.js for responsive images
+ // Height is set to 0 to maintain aspect ratio
+ return getImgproxyUrl(src, {
+ width,
+ resizing_type: 'fit',
+ gravity: 'fv', // Use face-aware focusing (face detection)
+ });
+}
diff --git a/lib/imgproxy.ts b/lib/imgproxy.ts
new file mode 100644
index 00000000..b23da396
--- /dev/null
+++ b/lib/imgproxy.ts
@@ -0,0 +1,82 @@
+/**
+ * Generates an imgproxy URL for a given source image and options.
+ *
+ * Documentation: https://docs.imgproxy.net/usage/processing
+ */
+
+interface ImgproxyOptions {
+ width?: number;
+ height?: number;
+ resizing_type?: 'fit' | 'fill' | 'fill-down' | 'force' | 'auto';
+ gravity?: string;
+ enlarge?: boolean;
+ extension?: string;
+}
+
+/**
+ * Encodes a string to Base64 (URL-safe)
+ */
+function encodeBase64(str: string): string {
+ if (typeof Buffer !== 'undefined') {
+ return Buffer.from(str)
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '');
+ } else {
+ // Fallback for browser environment if Buffer is not available
+ return window.btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+ }
+}
+
+export function getImgproxyUrl(src: string, options: ImgproxyOptions = {}): string {
+ const baseUrl = process.env.NEXT_PUBLIC_IMGPROXY_URL || 'https://img.infra.mintel.me';
+
+ // If no imgproxy URL is configured, return the source as is
+ if (!baseUrl) return src;
+
+ // Handle local paths or relative URLs
+ let absoluteSrc = src;
+ if (src.startsWith('/')) {
+ const baseUrlForSrc =
+ process.env.NEXT_PUBLIC_BASE_URL ||
+ (typeof window !== 'undefined' ? window.location.origin : 'https://klz-cables.com');
+ if (baseUrlForSrc) {
+ absoluteSrc = `${baseUrlForSrc.replace(/\/$/, '')}${src}`;
+ }
+ }
+
+ // Development mapping: Map local domains to internal Docker hostnames
+ // so imgproxy can fetch images without SSL issues or external routing
+ if (process.env.NODE_ENV === 'development') {
+ if (absoluteSrc.includes('klz.localhost')) {
+ absoluteSrc = absoluteSrc.replace(/^https?:\/\/klz\.localhost/, 'http://klz-app:3000');
+ } else if (absoluteSrc.includes('cms.klz.localhost')) {
+ absoluteSrc = absoluteSrc.replace(/^https?:\/\/cms\.klz\.localhost/, 'http://klz-cms:8055');
+ }
+ // Also handle direct container names if needed
+ }
+
+ const {
+ width = 0,
+ height = 0,
+ resizing_type = 'fit',
+ gravity = 'sm', // Default to smart gravity
+ enlarge = false,
+ extension = '',
+ } = options;
+
+ // Processing options
+ // Format: /rs::::/g:
+ const processingOptions = [
+ `rs:${resizing_type}:${width}:${height}:${enlarge ? 1 : 0}`,
+ `g:${gravity}`,
+ ].join('/');
+
+ // Using /unsafe/ for now as we don't handle signatures yet
+ // Format: /unsafe//
+ const suffix = extension ? `@${extension}` : '';
+ const encodedSrc = encodeBase64(absoluteSrc + suffix);
+
+ return `${baseUrl}/unsafe/${processingOptions}/${encodedSrc}`;
+}
diff --git a/lighthouserc.js b/lighthouserc.js
deleted file mode 100644
index 70238c83..00000000
--- a/lighthouserc.js
+++ /dev/null
@@ -1,19 +0,0 @@
-module.exports = {
- ci: {
- collect: {
- numberOfRuns: 1,
- settings: {
- preset: 'desktop',
- onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'],
- },
- },
- assert: {
- assertions: {
- 'categories:performance': ['warn', { minScore: 0.9 }],
- 'categories:accessibility': ['warn', { minScore: 0.9 }],
- 'categories:best-practices': ['warn', { minScore: 0.9 }],
- 'categories:seo': ['warn', { minScore: 0.9 }],
- },
- },
- },
-};
diff --git a/messages/de.json b/messages/de.json
index cfca5ce8..3df71c4f 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -58,7 +58,7 @@
}
},
"Navigation": {
- "home": "Start",
+ "home": "KLZ Cables Startseite",
"team": "Team",
"products": "Produkte",
"blog": "Blog",
@@ -394,4 +394,4 @@
"cta": "Zurück zur Sicherheit"
}
}
-}
\ No newline at end of file
+}
diff --git a/messages/en.json b/messages/en.json
index 09a87f1e..e184d090 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -59,7 +59,7 @@
},
"Navigation": {
"menu": "Menu",
- "home": "Home",
+ "home": "KLZ Cables Home",
"team": "Team",
"products": "Products",
"blog": "Blog",
diff --git a/middleware.ts b/middleware.ts
index ca97860d..6bc5bfd1 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,5 +1,5 @@
import createMiddleware from 'next-intl/middleware';
-import { NextRequest } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
// Create the internationalization middleware
const intlMiddleware = createMiddleware({
@@ -20,9 +20,10 @@ export default function middleware(request: NextRequest) {
pathname.startsWith('/errors') ||
pathname.startsWith('/health') ||
pathname.includes('/api/og') ||
- pathname.includes('opengraph-image')
+ pathname.includes('opengraph-image') ||
+ pathname.endsWith('sitemap.xml')
) {
- return;
+ return NextResponse.next();
}
// Build header object for logging
@@ -93,6 +94,6 @@ export default function middleware(request: NextRequest) {
export const config = {
matcher: [
- '/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|pdf|txt|vcf)$).*)',
+ '/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|pdf|txt|vcf|xml)$).*)',
],
};
diff --git a/next.config.mjs b/next.config.mjs
index 14234b95..6fd4954d 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -317,17 +317,8 @@ const nextConfig = {
];
},
images: {
- remotePatterns: [
- {
- protocol: 'https',
- hostname: 'klz-cables.com',
- port: '',
- pathname: '/wp-content/uploads/**',
- },
- ],
- dangerouslyAllowSVG: true,
- contentDispositionType: 'attachment',
- contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
+ loader: 'custom',
+ loaderFile: './lib/imgproxy-loader.ts',
},
async rewrites() {
const umamiUrl =
diff --git a/package.json b/package.json
index 9335a892..3ecdc1f4 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"@vitejs/plugin-react": "^5.1.4",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.23",
+ "cheerio": "^1.2.0",
"eslint": "^9.18.0",
"happy-dom": "^20.6.1",
"husky": "^9.1.7",
@@ -112,6 +113,7 @@
"cms:push:testing:DANGER": "./scripts/sync-directus.sh push testing",
"cms:push:prod:DANGER": "./scripts/sync-directus.sh push production",
"pagespeed:test": "tsx ./scripts/pagespeed-sitemap.ts",
+ "pagespeed:audit": "./scripts/audit-local.sh",
"pagespeed:urls": "tsx -e \"import sitemap from './app/sitemap'; sitemap().then(urls => console.log(urls.map(u => u.url).join('\\n')))\"",
"remotion:render": "remotion render WebsiteVideo remotion/index.ts out.mp4",
"remotion:preview": "remotion preview remotion/index.ts",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 40cb4420..bdc98520 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -186,6 +186,9 @@ importers:
autoprefixer:
specifier: ^10.4.23
version: 10.4.24(postcss@8.5.6)
+ cheerio:
+ specifier: ^1.2.0
+ version: 1.2.0
eslint:
specifier: ^9.18.0
version: 9.39.2(jiti@2.6.1)
@@ -3396,6 +3399,10 @@ packages:
resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==}
engines: {node: '>=18.17'}
+ cheerio@1.2.0:
+ resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
+ engines: {node: '>=20.18.1'}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -3893,6 +3900,10 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
+ entities@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -4554,6 +4565,9 @@ packages:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
+ htmlparser2@10.1.0:
+ resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
+
htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
@@ -6955,6 +6969,10 @@ packages:
resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==}
engines: {node: '>=18.17'}
+ undici@7.22.0:
+ resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
+ engines: {node: '>=20.18.1'}
+
unicode-properties@1.4.1:
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
@@ -10712,6 +10730,20 @@ snapshots:
undici: 6.23.0
whatwg-mimetype: 4.0.0
+ cheerio@1.2.0:
+ dependencies:
+ cheerio-select: 2.1.0
+ dom-serializer: 2.0.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ encoding-sniffer: 0.2.1
+ htmlparser2: 10.1.0
+ parse5: 7.3.0
+ parse5-htmlparser2-tree-adapter: 7.1.0
+ parse5-parser-stream: 7.1.2
+ undici: 7.22.0
+ whatwg-mimetype: 4.0.0
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -11211,6 +11243,8 @@ snapshots:
entities@6.0.1: {}
+ entities@7.0.1: {}
+
env-paths@2.2.1: {}
env-paths@3.0.0: {}
@@ -12200,6 +12234,13 @@ snapshots:
css-line-break: 2.1.0
text-segmentation: 1.0.3
+ htmlparser2@10.1.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 7.0.1
+
htmlparser2@8.0.2:
dependencies:
domelementtype: 2.3.0
@@ -15077,6 +15118,8 @@ snapshots:
undici@6.23.0: {}
+ undici@7.22.0: {}
+
unicode-properties@1.4.1:
dependencies:
base64-js: 1.5.1
diff --git a/scripts/audit-local.sh b/scripts/audit-local.sh
new file mode 100755
index 00000000..5eeca499
--- /dev/null
+++ b/scripts/audit-local.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+# audit-local.sh
+# Runs a high-fidelity Lighthouse audit locally using the Docker production stack.
+
+set -e
+
+echo "🚀 Starting High-Fidelity Local Audit..."
+
+# 1. Environment and Infrastructure
+export DOCKER_HOST="unix:///Users/marcmintel/.docker/run/docker.sock"
+export NEXT_PUBLIC_IMGPROXY_URL="http://img.klz.localhost"
+export NEXT_URL="http://klz.localhost"
+
+docker network create infra 2>/dev/null || true
+docker volume create klz-cablescom_directus-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-cms klz-gatekeeper
+
+# 3. Build and Start klz-app and klz-imgproxy in Production Mode
+echo "🏗️ Building and starting klz-app (Production)..."
+# We bypass the dev override by explicitly using the base compose file
+NEXT_PUBLIC_BASE_URL=$NEXT_URL \
+docker-compose -f docker-compose.yml up -d --build klz-app klz-imgproxy
+
+# 4. Wait for application to be ready
+echo "⏳ Waiting for application to be healthy..."
+MAX_RETRIES=30
+RETRY_COUNT=0
+
+until $(curl -s -f -o /dev/null "$NEXT_URL/health"); do
+ if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
+ echo "❌ Error: App did not become healthy in time."
+ exit 1
+ fi
+ echo " ...waiting for $NEXT_URL/health"
+ sleep 2
+ RETRY_COUNT=$((RETRY_COUNT+1))
+done
+
+echo "✅ App is healthy at $NEXT_URL"
+
+# 5. Run Lighthouse Audit
+echo "⚡ Executing Lighthouse CI..."
+NEXT_PUBLIC_BASE_URL=$NEXT_URL PAGESPEED_LIMIT=5 pnpm run pagespeed:test "$NEXT_URL"
+
+echo "✨ Audit completed! Summary above."
+echo "💡 You can stop the production app with: docker-compose stop klz-app"
diff --git a/scripts/pagespeed-sitemap.ts b/scripts/pagespeed-sitemap.ts
index 4000be43..369e73f8 100644
--- a/scripts/pagespeed-sitemap.ts
+++ b/scripts/pagespeed-sitemap.ts
@@ -86,7 +86,7 @@ async function main() {
// Using a more robust way to execute and capture output
// We remove 'npx lhci upload' to keep everything local and avoid Google-hosted reports
- const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --collect.settings.chromeFlags='--no-sandbox --disable-setuid-sandbox' --collect.settings.extraHeaders='${extraHeaders}' && npx lhci assert`;
+ const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --config=config/lighthouserc.json --collect.settings.extraHeaders='${extraHeaders}' && npx lhci assert --config=config/lighthouserc.json`;
console.log(`💻 Executing LHCI...`);