Compare commits
8 Commits
v1.1.0-rc.
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 474fa4f3df | |||
| f1d49416d1 | |||
| e3e0a7670c | |||
| 8a87318b12 | |||
| 93cb12d7d9 | |||
| 44f0c430a9 | |||
| 1478909a73 | |||
| 837abd4921 |
@@ -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",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"Navigation": {
|
||||
"menu": "Menu",
|
||||
"home": "KLZ Cables Home",
|
||||
"home": "Home",
|
||||
"team": "Team",
|
||||
"products": "Products",
|
||||
"blog": "Blog",
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function middleware(request: NextRequest) {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!api|_next/static|_next/image|_img|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