Compare commits

...

9 Commits

Author SHA1 Message Date
c4bc10ef76 fix: restore missing SVG animations in HeroIllustration
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Failing after 17s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-21 01:56:09 +01:00
e95f7c6dd2 fix: restore missing translations on 404 page 2026-02-21 00:38:49 +01:00
17a91e48e6 perf: pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 8m19s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Smoke Test (push) Successful in 54s
Build & Deploy / ⚡ Lighthouse (push) Successful in 8m38s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 23:42:33 +01:00
4d0a94d288 perf: pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 9m1s
Build & Deploy / 🏗️ Build (push) Successful in 12m31s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / ⚡ Lighthouse (push) Successful in 2m50s
Build & Deploy / 🧪 Smoke Test (push) Successful in 3m34s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 21:24:12 +01:00
3568c13941 perf: pipeline 2026-02-20 19:06:19 +01:00
d538d7b9ec fix(blog): ensure target environment vars are parsed for accurate strict filtering in prod, and integrate face detection gravity for blog thumbnails
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-02-20 18:54:09 +01:00
8c08b552cf fix: svg stroke width 2026-02-20 18:48:10 +01:00
1dd74a3861 ci: fix pipeline cache corruption and secrets warning
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-20 16:20:53 +01:00
8d77ca45f7 feat(blog): implement scheduled and draft posts filtering and preview UI
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m59s
Build & Deploy / 🏗️ Build (push) Failing after 34s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-20 15:41:07 +01:00
13 changed files with 212 additions and 76 deletions

View File

@@ -10,17 +10,31 @@ jobs:
- 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
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Configure Private Registry
run: |
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"

View File

@@ -154,14 +154,26 @@ jobs:
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: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -206,8 +218,8 @@ jobs:
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
tags: registry.infra.mintel.me/mintel/klz-cables.com:${{ needs.prepare.outputs.image_tag }}
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache,mode=max
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache-v2
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache-v2,mode=max
secrets: |
"NPM_TOKEN=${{ secrets.REGISTRY_PASS }}"
@@ -386,14 +398,26 @@ jobs:
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: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -418,14 +442,26 @@ jobs:
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: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc

View File

@@ -8,7 +8,6 @@ ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ARG UMAMI_WEBSITE_ID
ARG UMAMI_API_ENDPOINT
ARG NPM_TOKEN
# Environment variables for Next.js build
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
@@ -25,7 +24,7 @@ COPY pnpm-lock.yaml package.json .npmrc* ./
# Configure private registry and install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
--mount=type=secret,id=NPM_TOKEN \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
pnpm install --frozen-lockfile && \

View File

@@ -5,6 +5,7 @@ import { MDXRemote } from 'next-mdx-remote/rsc';
import { getPostBySlug, getAdjacentPosts, getReadingTime, getHeadings } from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
@@ -69,13 +70,24 @@ export default async function BlogPost({ params }: BlogPostProps) {
category={post.frontmatter.category}
readingTime={getReadingTime(post.content)}
/>
{(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && (
<div className="bg-orange-500 text-white text-center py-2 px-4 font-bold text-sm tracking-wider uppercase relative z-50">
Preview (Not visible in production)
</div>
)}
{/* Featured Image Header */}
{post.frontmatter.featuredImage ? (
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
<div
className="absolute inset-0 bg-cover bg-center transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100"
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
/>
<div className="absolute inset-0 transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100">
<Image
src={`${post.frontmatter.featuredImage}?gravity=obj:face`}
alt={post.frontmatter.title}
fill
priority
className="object-cover"
sizes="100vw"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
{/* Title overlay on image */}

View File

@@ -62,7 +62,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{featuredPost && featuredPost.frontmatter.featuredImage && (
<>
<Image
src={featuredPost.frontmatter.featuredImage}
src={`${featuredPost.frontmatter.featuredImage}?gravity=obj:face`}
alt={featuredPost.frontmatter.title}
fill
className="absolute inset-0 w-full h-full object-cover scale-105 animate-slow-zoom opacity-40 md:opacity-60"
@@ -75,9 +75,16 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="saturated" className="mb-4 md:mb-6">
{t('featuredPost')}
</Badge>
<div className="flex flex-wrap items-center gap-3 mb-4 md:mb-6">
<Badge variant="saturated">{t('featuredPost')}</Badge>
{featuredPost &&
(new Date(featuredPost.frontmatter.date) > new Date() ||
featuredPost.frontmatter.public === false) && (
<Badge variant="accent" className="bg-orange-500 text-white border-none">
Preview
</Badge>
)}
</div>
{featuredPost && (
<>
<Heading level={1} className="text-white mb-4 md:mb-8">
@@ -153,7 +160,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{post.frontmatter.featuredImage && (
<div className="relative h-48 md:h-72 overflow-hidden">
<Image
src={post.frontmatter.featuredImage}
src={`${post.frontmatter.featuredImage}?gravity=obj:face`}
alt={post.frontmatter.title}
fill
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
@@ -168,6 +175,15 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{post.frontmatter.category}
</Badge>
)}
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
<Badge
variant="accent"
className="absolute top-3 right-3 md:top-6 md:right-6 shadow-lg bg-orange-500 text-white border-none"
>
Preview
</Badge>
)}
</div>
)}
<div className="p-5 md:p-10 flex flex-col flex-1">

View File

@@ -81,7 +81,16 @@ export default async function Layout(props: {
}
// Pick only the namespaces required by client components to reduce the hydration payload size
const clientKeys = ['Footer', 'Navigation', 'Contact', 'Products', 'Team', 'Home'];
const clientKeys = [
'Footer',
'Navigation',
'Contact',
'Products',
'Team',
'Home',
'Error',
'StandardPage',
];
const clientMessages: Record<string, any> = {};
for (const key of clientKeys) {
if (messages[key]) {

View File

@@ -125,8 +125,9 @@ export default function HeroIllustration() {
}, []);
const viewBox = isMobile ? '400 0 1000 1100' : '-400 -200 1800 1100';
const scale = isMobile ? 1.44 : 1;
const opacity = isMobile ? 0.6 : 0.85;
// Increase scale slightly and opacity significantly on mobile to fix the "thin" appearance
const scale = isMobile ? 1.6 : 1;
const opacity = isMobile ? 0.9 : 0.85;
return (
<div className="absolute inset-0 z-0 overflow-visible bg-primary w-full h-full">
@@ -154,15 +155,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="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="1.5" result="blur" />
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="soft-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1" result="blur" />
<filter id="soft-glow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="2" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
@@ -215,10 +216,10 @@ export default function HeroIllustration() {
</g>
{/* ANIMATED ENERGY FLOW */}
<g>
<g filter="url(#glow)">
{POWER_LINES.map((line, i) => {
// Only animate a small subset of lines to reduce main-thread work significantly
if (i % 5 !== 0) return null;
// Only animate a subset of lines to reduce main-thread work
if (i % 2 !== 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));
@@ -232,12 +233,16 @@ export default function HeroIllustration() {
stroke="url(#energy-pulse)"
strokeWidth="3"
strokeLinecap="round"
style={{
strokeDasharray: `${length * 0.2} ${length * 0.8}`,
strokeDashoffset: length,
animation: `flow ${1.5 + (i % 3) * 0.5}s linear infinite`,
}}
/>
strokeDasharray={`${length * 0.2} ${length * 0.8}`}
>
<animate
attributeName="stroke-dashoffset"
from={length}
to={0}
dur={`${1.5 + (i % 3) * 0.5}s`}
repeatCount="indefinite"
/>
</line>
);
})}
</g>
@@ -263,13 +268,14 @@ export default function HeroIllustration() {
strokeWidth="1"
strokeOpacity="0.3"
/>
<circle
r="3"
fill="#82ed20"
fillOpacity="0.3"
filter="url(#soft-glow)"
style={{ animation: 'solar-pulse 2s ease-in-out infinite' }}
/>
<circle r="3" fill="#82ed20" fillOpacity="0.3" filter="url(#soft-glow)">
<animate
attributeName="fillOpacity"
values="0.2;0.5;0.2"
dur="2s"
repeatCount="indefinite"
/>
</circle>
</g>
);
})}
@@ -289,26 +295,28 @@ export default function HeroIllustration() {
strokeOpacity="0.3"
/>
<g transform="translate(0, -60)">
<g
style={{
transformOrigin: '0px 0px',
animation: `spin-slow ${3 + i}s linear infinite`,
}}
>
{[0, 120, 240].map((angle, j) => (
<line
key={`blade-${i}-${j}`}
x1="0"
y1="0"
x2="0"
y2="-30"
stroke="white"
strokeWidth="1.5"
strokeOpacity="0.4"
transform={`rotate(${angle})`}
{[0, 120, 240].map((angle, j) => (
<line
key={`blade-${i}-${j}`}
x1="0"
y1="0"
x2="0"
y2="-30"
stroke="white"
strokeWidth="1.5"
strokeOpacity="0.4"
transform={`rotate(${angle})`}
>
<animateTransform
attributeName="transform"
type="rotate"
from={`${angle} 0 0`}
to={`${angle + 360} 0 0`}
dur={`${3 + i}s`}
repeatCount="indefinite"
/>
))}
</g>
</line>
))}
</g>
</g>
);

View File

@@ -5,6 +5,7 @@ featuredImage: /uploads/2026/01/1767353529807.jpg
locale: de
category: Kabel Technologie
excerpt: 'KLZ Cables startet mit einer starken Verstärkung ins neue Jahr: Johannes Gleich übernimmt die Rolle des Senior Key Account Managers. Erfahren Sie mehr über unseren neuen Experten für Infrastruktur und Energieversorger.'
public: false
---
# Herzlich willkommen bei KLZ: Johannes Gleich startet als Senior Key Account Manager durch

View File

@@ -5,6 +5,7 @@ featuredImage: /uploads/2026/01/1767353529807.jpg
locale: en
category: Cable Technology
excerpt: 'KLZ Cables kicks off the new year with a strong addition: Johannes Gleich takes on the role of Senior Key Account Manager. Learn more about our new expert for infrastructure and energy suppliers.'
public: false
---
# Welcome to KLZ: Johannes Gleich starts as Senior Key Account Manager

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { mapSlugToFileSlug } from './slugs';
import { config } from '@/lib/config';
export interface PostFrontmatter {
title: string;
@@ -10,6 +11,7 @@ export interface PostFrontmatter {
featuredImage?: string | null;
category?: string;
locale: string;
public?: boolean;
}
export interface PostMdx {
@@ -18,6 +20,17 @@ export interface PostMdx {
content: string;
}
export function isPostVisible(post: { frontmatter: { date: string; public?: boolean } }) {
// If explicitly marked as not public, hide in production
if (post.frontmatter.public === false && config.isProduction) {
return false;
}
const postDate = new Date(post.frontmatter.date);
const now = new Date();
return !(postDate > now && config.isProduction);
}
export async function getPostBySlug(slug: string, locale: string): Promise<PostMdx | null> {
// Map translated slug to file slug
const fileSlug = await mapSlugToFileSlug(slug, locale);
@@ -31,11 +44,17 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
return {
const postInfo = {
slug: fileSlug,
frontmatter: data as PostFrontmatter,
content,
};
if (!isPostVisible(postInfo)) {
return null;
}
return postInfo;
}
export async function getAllPosts(locale: string): Promise<PostMdx[]> {
@@ -55,6 +74,7 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
content,
};
})
.filter(isPostVisible)
.sort(
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
);
@@ -78,6 +98,7 @@ export async function getAllPostsMetadata(locale: string): Promise<Partial<PostM
frontmatter: data as PostFrontmatter,
};
})
.filter(isPostVisible)
.sort(
(a, b) =>
new Date(b.frontmatter.date as string).getTime() -

View File

@@ -20,6 +20,8 @@ const booleanSchema = z.preprocess((val) => {
const envExtension = {
// Project specific overrides or additions
AUTH_COOKIE_NAME: z.string().default('klz_gatekeeper_session'),
TARGET: z.string().optional(),
NEXT_PUBLIC_TARGET: z.string().optional(),
// Gatekeeper specifics not in base
GATEKEEPER_URL: z.string().url().default('http://gatekeeper:3000'),

View File

@@ -23,11 +23,28 @@ export default function imgproxyLoader({
return src;
}
// Check if src contains custom gravity query parameter
let gravity = 'sm'; // Use smart gravity (content-aware) by default
let cleanSrc = src;
try {
// Dummy base needed for relative URLs
const url = new URL(src, 'http://localhost');
const customGravity = url.searchParams.get('gravity');
if (customGravity) {
gravity = customGravity;
url.searchParams.delete('gravity');
cleanSrc = src.startsWith('http') ? url.href : url.pathname + url.search;
}
} catch (e) {
// Fallback if parsing fails
}
// We use the width provided by Next.js for responsive images
// Height is set to 0 to maintain aspect ratio
return getImgproxyUrl(src, {
return getImgproxyUrl(cleanSrc, {
width,
resizing_type: 'fit',
gravity: 'sm', // Use smart gravity (content-aware) instead of face detection (requires ML)
gravity,
});
}

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.