Compare commits
11 Commits
v1.1.0-rc.
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 474fa4f3df | |||
| f1d49416d1 | |||
| e3e0a7670c | |||
| 8a87318b12 | |||
| 93cb12d7d9 | |||
| 44f0c430a9 | |||
| 1478909a73 | |||
| 837abd4921 | |||
| 75c6d363c0 | |||
| a2b7f28b9f | |||
| 52ecd1b052 |
@@ -112,7 +112,11 @@ export default async function Layout(props: {
|
||||
|
||||
return (
|
||||
<html lang={safeLocale} className={`scroll-smooth overflow-x-hidden ${inter.variable}`}>
|
||||
<head></head>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link rel="preconnect" href="https://img.infra.mintel.me" />
|
||||
</head>
|
||||
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
||||
<NextIntlClientProvider messages={messages} locale={safeLocale}>
|
||||
<RecordModeProvider isEnabled={recordModeEnabled}>
|
||||
|
||||
@@ -137,12 +137,12 @@ const containerVariants = {
|
||||
} as const;
|
||||
|
||||
const headingVariants = {
|
||||
hidden: { opacity: 1, y: 30, scale: 0.95 },
|
||||
hidden: { opacity: 1, y: 10, scale: 0.98 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: { duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
transition: { duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -167,12 +167,12 @@ const scribbleVariants = {
|
||||
} as const;
|
||||
|
||||
const subtitleVariants = {
|
||||
hidden: { opacity: 0, y: 40, scale: 0.95 },
|
||||
hidden: { opacity: 0, y: 20, scale: 0.98 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: { duration: 1, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
transition: { duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94], delay: 0.1 },
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -154,15 +154,15 @@ export default function HeroIllustration() {
|
||||
<stop offset="70%" stopColor="white" stopOpacity="0.4" />
|
||||
<stop offset="100%" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3" result="blur" />
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="1.5" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="soft-glow" x="-100%" y="-100%" width="300%" height="300%">
|
||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||
<filter id="soft-glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="1" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
@@ -215,10 +215,10 @@ export default function HeroIllustration() {
|
||||
</g>
|
||||
|
||||
{/* ANIMATED ENERGY FLOW */}
|
||||
<g filter="url(#glow)">
|
||||
<g>
|
||||
{POWER_LINES.map((line, i) => {
|
||||
// Only animate a subset of lines to reduce main-thread work
|
||||
if (i % 2 !== 0) return null;
|
||||
// Only animate a small subset of lines to reduce main-thread work significantly
|
||||
if (i % 5 !== 0) return null;
|
||||
const from = gridToScreen(line.from.col, line.from.row);
|
||||
const to = gridToScreen(line.to.col, line.to.row);
|
||||
const length = Math.sqrt(Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2));
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function ProductCategories() {
|
||||
alt={category.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 24vw"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-primary-dark/40 group-hover:bg-primary-dark/60 transition-all duration-500" />
|
||||
<div className="absolute inset-0 p-8 md:p-10 flex flex-col justify-end text-white">
|
||||
|
||||
@@ -6,14 +6,18 @@ export default function VideoSection() {
|
||||
|
||||
return (
|
||||
<section className="relative h-[70vh] overflow-hidden bg-primary">
|
||||
<video
|
||||
<video
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
>
|
||||
<source src="/uploads/2024/12/making-of-metal-cable-on-factory-2023-11-27-04-55-16-utc-2.webm" type="video/mp4" />
|
||||
<source
|
||||
src="/uploads/2024/12/making-of-metal-cable-on-factory-2023-11-27-04-55-16-utc-2.webm"
|
||||
type="video/webm"
|
||||
/>
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary/60 via-transparent to-primary/60 flex items-center justify-center">
|
||||
<div className="max-w-5xl px-6 text-center animate-slide-up">
|
||||
@@ -22,9 +26,12 @@ export default function VideoSection() {
|
||||
future: (chunks) => (
|
||||
<span className="relative inline-block mx-2">
|
||||
<span className="relative z-10 italic text-accent">{chunks}</span>
|
||||
<Scribble variant="underline" className="w-full h-4 -bottom-2 left-0 text-accent/40" />
|
||||
<Scribble
|
||||
variant="underline"
|
||||
className="w-full h-4 -bottom-2 left-0 text-accent/40"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
),
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
|
||||
DIRECTUS_URL: ${DIRECTUS_URL}
|
||||
DIRECTUS_URL: "${DIRECTUS_URL}"
|
||||
image: registry.infra.mintel.me/mintel/klz-cables.com:${IMAGE_TAG:-latest}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}"
|
||||
|
||||
# Public Router (Whitelist for OG Images, Sitemaps, Health)
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && (PathPrefix(`/health`) || PathPrefix(`/sitemap.xml`) || PathPrefix(`/robots.txt`) || PathPrefix(`/manifest.webmanifest`) || PathPrefix(`/_img`) || PathPrefix(`/api/og`) || PathPrefix(`/de/api/og`) || PathPrefix(`/en/api/og`) || PathPrefix(`/opengraph-image`) || PathPrefix(`/de/opengraph-image`) || PathPrefix(`/en/opengraph-image`) || PathPrefix(`/blog/opengraph-image`) || PathPrefix(`/de/blog/opengraph-image`) || PathPrefix(`/en/blog/opengraph-image`) || PathRegexp(`^/sitemap(-[0-9]+)?\\.xml$`))"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && (PathPrefix(`/health`) || PathPrefix(`/sitemap.xml`) || PathPrefix(`/robots.txt`) || PathPrefix(`/manifest.webmanifest`) || PathPrefix(`/api/og`) || PathPrefix(`/de/api/og`) || PathPrefix(`/en/api/og`) || PathPrefix(`/logo-white.svg`) || PathPrefix(`/icon-white.svg`) || PathPrefix(`/opengraph-image`) || PathPrefix(`/de/opengraph-image`) || PathPrefix(`/en/opengraph-image`) || PathPrefix(`/blog/opengraph-image`) || PathPrefix(`/de/blog/opengraph-image`) || PathPrefix(`/en/blog/opengraph-image`) || PathRegexp(`^/sitemap(-[0-9]+)?\\.xml$`) || PathRegexp(`.*\\.(svg|png|jpg|jpeg|gif|webp|ico|webm|mp4|map)$`))"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
||||
@@ -165,18 +165,31 @@ services:
|
||||
- "cms.klz.localhost:host-gateway"
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
IMGPROXY_URL_MAPPING: "${IMGPROXY_URL_MAPPING:-http://klz.localhost/:http://klz-app:3000/,http://cms.klz.localhost/:http://klz-cms:8055/}"
|
||||
IMGPROXY_URL_MAPPING: "${NEXT_PUBLIC_BASE_URL}:http://klz-app:3000,${DIRECTUS_URL}: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"
|
||||
IMGPROXY_LOG_LEVEL: debug
|
||||
IMGPROXY_ALLOW_LOCAL_NETWORKS: "true"
|
||||
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# HTTP router (local dev)
|
||||
# Existing Local HTTP Router
|
||||
- "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"
|
||||
|
||||
# NEW: Direct Public Staging Router for /_img (Bypasses Next.js rewrites)
|
||||
# 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.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"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-img-strip.stripprefix.prefixes=/_img"
|
||||
# HTTPS router (staging/prod)
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.rule=Host(`img.${TRAEFIK_HOST:-klz.localhost}`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
|
||||
@@ -28,6 +28,6 @@ export default function imgproxyLoader({
|
||||
return getImgproxyUrl(src, {
|
||||
width,
|
||||
resizing_type: 'fit',
|
||||
gravity: 'fv', // Use face-aware focusing (face detection)
|
||||
gravity: 'sm', // Use smart gravity (content-aware) instead of face detection (requires ML)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,22 +13,6 @@ interface ImgproxyOptions {
|
||||
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 {
|
||||
// Use local proxy path which is rewritten in next.config.mjs
|
||||
const baseUrl = '/_img';
|
||||
@@ -71,10 +55,18 @@ export function getImgproxyUrl(src: string, options: ImgproxyOptions = {}): stri
|
||||
`g:${gravity}`,
|
||||
].join('/');
|
||||
|
||||
// Using /unsafe/ for now as we don't handle signatures yet
|
||||
// Format: <base_url>/unsafe/<options>/<base64_url>
|
||||
const suffix = extension ? `@${extension}` : '';
|
||||
const encodedSrc = encodeBase64(absoluteSrc + suffix);
|
||||
// Using Base64 encoding for the source URL.
|
||||
// This completely eliminates any risk of intermediate proxies (Traefik/Next.js)
|
||||
// URL-decoding the path, which corrupts the double-slash (// to /) and causes 403 errors.
|
||||
// Imgproxy expects URL-safe Base64 (RFC 4648) without padding.
|
||||
const b64 =
|
||||
typeof window === 'undefined'
|
||||
? Buffer.from(absoluteSrc).toString('base64')
|
||||
: btoa(unescape(encodeURIComponent(absoluteSrc)));
|
||||
|
||||
return `${baseUrl}/unsafe/${processingOptions}/${encodedSrc}`;
|
||||
const urlSafeB64 = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
|
||||
const suffix = extension ? `.${extension}` : '';
|
||||
|
||||
return `${baseUrl}/unsafe/${processingOptions}/${urlSafeB64}${suffix}`;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"Navigation": {
|
||||
"menu": "Menü",
|
||||
"home": "KLZ Cables Startseite",
|
||||
"home": "Startseite",
|
||||
"team": "Team",
|
||||
"products": "Produkte",
|
||||
"blog": "Blog",
|
||||
@@ -122,7 +122,7 @@
|
||||
"quote": "Manchmal braucht es nur einen klaren Kopf und das richtige Kabel, um die Welt ein Stück besser zu machen.",
|
||||
"description": "Klaus ist der Fels in der Brandung – selbst wenn das Kabelchaos überhandnimmt. Mit jahrzehntelanger Erfahrung und einem stabilen Netzwerk sorgt er dafür, dass alles glatt läuft. Er denkt nicht nur in Lösungen, sondern bringt auch Humor und den nötigen Weitblick mit, um selbst komplexe Themen locker auf den Punkt zu bringen.",
|
||||
"linkedin": "Klaus' LinkedIn",
|
||||
"role": "Gründer & Visionär"
|
||||
"role": "Geschäftsführer"
|
||||
},
|
||||
"manifesto": {
|
||||
"title": "Unser Manifest",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"Navigation": {
|
||||
"menu": "Menu",
|
||||
"home": "KLZ Cables Home",
|
||||
"home": "Home",
|
||||
"team": "Team",
|
||||
"products": "Products",
|
||||
"blog": "Blog",
|
||||
@@ -122,7 +122,7 @@
|
||||
"quote": "Sometimes all it takes is a clear head and a good cable to make the world a little better.",
|
||||
"description": "Klaus is the man with the experience, bringing perspective and calm to the table—even when cable chaos threatens to take over. With impressive industry knowledge and a network as solid as our cables, he ensures everything runs smoothly. Klaus isn’t just a problem solver; he’s a strategic thinker who knows how to get to the point with a touch of humor.",
|
||||
"linkedin": "Check Klaus' LinkedIn",
|
||||
"role": "Founder & Visionary"
|
||||
"role": "Managing Director"
|
||||
},
|
||||
"manifesto": {
|
||||
"title": "Our manifesto",
|
||||
|
||||
@@ -95,7 +95,8 @@ export default function middleware(request: NextRequest) {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!api|_next/static|_next/image|favicon.ico|manifest.webmanifest|.*\\.(?:svg|png|jpg|jpeg|gif|webp|pdf|txt|vcf|xml)$).*)',
|
||||
'/((?!api|_next/static|_next/image|_img|favicon.ico|manifest.webmanifest|.*\\.(?:svg|png|jpg|jpeg|gif|webp|pdf|txt|vcf|xml|webm|mp4|map)$).*)',
|
||||
'/(de|en)/:path*',
|
||||
'/(de|en)/:path*',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const nextConfig = {
|
||||
// Make sure entries are not disposed too quickly
|
||||
maxInactiveAge: 60 * 1000,
|
||||
},
|
||||
productionBrowserSourceMaps: false,
|
||||
logging: {
|
||||
fetches: {
|
||||
fullUrl: true,
|
||||
@@ -353,4 +354,6 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withMintelConfig(nextConfig);
|
||||
export default withMintelConfig(nextConfig, {
|
||||
hideSourceMaps: true,
|
||||
});
|
||||
|
||||
@@ -129,6 +129,11 @@
|
||||
"next": "16.1.6"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@remotion/cli": "^4.0.421",
|
||||
"@remotion/google-fonts": "^4.0.421",
|
||||
|
||||
Reference in New Issue
Block a user