+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {t.rich('title', {
+ green: (chunks) => (
+
+
+ {chunks}
+
+
+
+
+
+ ),
+ })}
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
-
-const containerVariants = {
- hidden: { opacity: 1 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.1,
- delayChildren: 0.1,
- },
- },
-} as const;
-
-const headingVariants = {
- hidden: { opacity: 1, y: 10, scale: 0.98 },
- visible: {
- opacity: 1,
- y: 0,
- scale: 1,
- transition: { duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] },
- },
-} as const;
-
-const accentVariants = {
- hidden: { opacity: 0, scale: 0.9, rotate: -5 },
- visible: {
- opacity: 1,
- scale: 1,
- rotate: 0,
- transition: { duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] },
- },
-} as const;
-
-const scribbleVariants = {
- hidden: { opacity: 0, scale: 0, rotate: 180 },
- visible: {
- opacity: 1,
- scale: 1,
- rotate: 0,
- transition: { duration: 1, type: 'spring', stiffness: 300, damping: 20 },
- },
-} as const;
-
-const subtitleVariants = {
- hidden: { opacity: 1, y: 20, scale: 0.98 },
- visible: {
- opacity: 1,
- y: 0,
- scale: 1,
- transition: { duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94], delay: 0.1 },
- },
-} as const;
-
-const buttonContainerVariants = {
- hidden: { opacity: 1 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.15,
- delayChildren: 0.4,
- },
- },
-} as const;
-
-const buttonVariants = {
- hidden: { opacity: 1, y: 30, scale: 0.9 },
- visible: {
- opacity: 1,
- y: 0,
- scale: 1,
- transition: { type: 'spring', stiffness: 400, damping: 20 },
- },
-} as const;
diff --git a/components/record-mode/PlaybackCursor.tsx b/components/record-mode/PlaybackCursor.tsx
index b3b21d60..6b7f603f 100644
--- a/components/record-mode/PlaybackCursor.tsx
+++ b/components/record-mode/PlaybackCursor.tsx
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from 'react';
-import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
+import { m, LazyMotion, AnimatePresence } from 'framer-motion';
import { useRecordMode } from './RecordModeContext';
export function PlaybackCursor() {
@@ -24,7 +24,7 @@ export function PlaybackCursor() {
if (!isPlaying) return null;
return (
-
+ import('@/lib/framer-features').then(res => res.default)}>
+ import('@/lib/framer-features').then(res => res.default)}>
{/* 1. Global Toolbar - Slim Industrial Bar */}
diff --git a/components/record-mode/RecordModeVisuals.tsx b/components/record-mode/RecordModeVisuals.tsx
index a2174d5b..9378ed95 100644
--- a/components/record-mode/RecordModeVisuals.tsx
+++ b/components/record-mode/RecordModeVisuals.tsx
@@ -10,7 +10,6 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
const [iframeUrl, setIframeUrl] = React.useState(null);
React.useEffect(() => {
- setMounted(true);
// Explicit non-magical detection
const embedded =
window.location.search.includes('embedded=true') || window.name === 'record-mode-iframe';
@@ -21,13 +20,12 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
url.searchParams.set('embedded', 'true');
setIframeUrl(url.toString());
}
- }, [isEmbedded]);
-
- // Hydration Guard: Match server on first render
- if (!mounted) return <>{children}>;
+ }, []);
// Recursion Guard: If we are already in an embedded iframe,
// strictly return just the children to prevent Inception.
+ // Note: This causes a hydration mismatch remount ONLY when actually embedded (e.g. inside Directus).
+ // Standard users and Lighthouse bots will NOT suffer a remount.
if (isEmbedded) {
return (
<>
diff --git a/components/team/Gallery.tsx b/components/team/Gallery.tsx
index 83e9b9b1..7fec0a32 100644
--- a/components/team/Gallery.tsx
+++ b/components/team/Gallery.tsx
@@ -3,7 +3,8 @@
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import Image from 'next/image';
-import Lightbox from '@/components/Lightbox';
+import dynamic from 'next/dynamic';
+const Lightbox = dynamic(() => import('@/components/Lightbox'), { ssr: false });
import { Section, Container, Heading } from '@/components/ui';
export default function Gallery() {
diff --git a/config/lighthouserc.json b/config/lighthouserc.json
index e46f1edd..9716768d 100644
--- a/config/lighthouserc.json
+++ b/config/lighthouserc.json
@@ -1,10 +1,15 @@
{
"ci": {
"collect": {
- "numberOfRuns": 1,
+ "numberOfRuns": 3,
"settings": {
"preset": "desktop",
- "onlyCategories": ["performance", "accessibility", "best-practices", "seo"],
+ "onlyCategories": [
+ "performance",
+ "accessibility",
+ "best-practices",
+ "seo"
+ ],
"chromeFlags": "--no-sandbox --disable-setuid-sandbox"
}
},
@@ -49,4 +54,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index be795f4e..4fe8fe1c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -183,9 +183,9 @@ services:
# This fixes the Next.js URL-decoding bug on dynamic image proxy paths
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.rule=(Host(`${TRAEFIK_HOST:-klz.localhost}`) || Host(`staging.klz-cables.com`) || Host(`testing.klz-cables.com`)) && PathPrefix(`/_img`)"
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.priority=99999"
- - "traefik.http.routers.${PROJECT_NAME:-klz}-img.entrypoints=websecure"
- - "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls=true"
- - "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-le}"
+ - "traefik.http.routers.${PROJECT_NAME:-klz}-img.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
+ - "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls=${TRAEFIK_TLS:-false}"
+ - "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.service=${PROJECT_NAME:-klz}-imgproxy-svc"
- "traefik.http.services.${PROJECT_NAME:-klz}-imgproxy-svc.loadbalancer.server.port=8080"
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.middlewares=${PROJECT_NAME:-klz}-img-strip"
diff --git a/lib/framer-features.ts b/lib/framer-features.ts
new file mode 100644
index 00000000..b18409fc
--- /dev/null
+++ b/lib/framer-features.ts
@@ -0,0 +1,2 @@
+import { domAnimation } from 'framer-motion';
+export default domAnimation;
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818f..9edff1c7 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/scripts/pagespeed-sitemap.ts b/scripts/pagespeed-sitemap.ts
index 369e73f8..29c08922 100644
--- a/scripts/pagespeed-sitemap.ts
+++ b/scripts/pagespeed-sitemap.ts
@@ -58,9 +58,10 @@ async function main() {
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
);
// Try to pick a variety: home, some products, some blog posts
- const home = urls.filter((u) => u.endsWith('/de') || u.endsWith('/en') || u === targetUrl);
- const others = urls.filter((u) => !home.includes(u));
- urls = [...home, ...others.slice(0, limit - home.length)];
+ const homeEN = urls.filter((u) => u.endsWith('/en') || u === targetUrl);
+ const homeDE = urls.filter((u) => u.endsWith('/de'));
+ const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u));
+ urls = [...homeEN, ...homeDE, ...others.slice(0, limit - (homeEN.length + homeDE.length))];
}
console.log(`🧪 Pages to be tested:`);