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...`);