Compare commits

...

6 Commits

Author SHA1 Message Date
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
11 changed files with 168 additions and 48 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
@@ -180,7 +192,7 @@ jobs:
# ──────────────────────────────────────────────────────────────────────────────
build:
name: 🏗️ Build
needs: [prepare, qa]
needs: prepare
if: needs.prepare.outputs.target != 'skip'
runs-on: docker
container:
@@ -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

@@ -127,6 +127,7 @@ 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;
const strokeMultiplier = isMobile ? 2.5 : 1;
return (
<div className="absolute inset-0 z-0 overflow-visible bg-primary w-full h-full">
@@ -184,7 +185,7 @@ export default function HeroIllustration() {
x2={end.x}
y2={end.y}
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
/>
);
})}
@@ -199,14 +200,14 @@ export default function HeroIllustration() {
x2={end.x}
y2={end.y}
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
/>
);
})}
</g>
{/* POWER LINES */}
<g stroke="white" strokeWidth="2" strokeOpacity="0.2">
<g stroke="white" strokeWidth={2 * strokeMultiplier} strokeOpacity="0.2">
{POWER_LINES.map((line, i) => {
const from = gridToScreen(line.from.col, line.from.row);
const to = gridToScreen(line.to.col, line.to.row);
@@ -230,7 +231,7 @@ export default function HeroIllustration() {
x2={to.x}
y2={to.y}
stroke="url(#energy-pulse)"
strokeWidth="3"
strokeWidth={3 * strokeMultiplier}
strokeLinecap="round"
style={{
strokeDasharray: `${length * 0.2} ${length * 0.8}`,
@@ -252,7 +253,7 @@ export default function HeroIllustration() {
fill="white"
fillOpacity="0.05"
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
strokeOpacity="0.2"
/>
<path
@@ -260,7 +261,7 @@ export default function HeroIllustration() {
fill="white"
fillOpacity="0.1"
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
strokeOpacity="0.3"
/>
<circle
@@ -285,7 +286,7 @@ export default function HeroIllustration() {
x2="0"
y2="-60"
stroke="white"
strokeWidth="2"
strokeWidth={2 * strokeMultiplier}
strokeOpacity="0.3"
/>
<g transform="translate(0, -60)">
@@ -303,7 +304,7 @@ export default function HeroIllustration() {
x2="0"
y2="-30"
stroke="white"
strokeWidth="1.5"
strokeWidth={1.5 * strokeMultiplier}
strokeOpacity="0.4"
transform={`rotate(${angle})`}
/>
@@ -325,7 +326,7 @@ export default function HeroIllustration() {
fill="white"
fillOpacity="0.05"
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
strokeOpacity="0.2"
/>
<path
@@ -337,7 +338,7 @@ export default function HeroIllustration() {
fill="white"
fillOpacity="0.08"
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
strokeOpacity="0.3"
/>
</g>
@@ -352,7 +353,7 @@ export default function HeroIllustration() {
<path
d="M -6 0 L -3 -45 M 6 0 L 3 -45"
stroke="white"
strokeWidth="1.5"
strokeWidth={1.5 * strokeMultiplier}
strokeOpacity="0.3"
/>
<line
@@ -361,7 +362,7 @@ export default function HeroIllustration() {
x2="12"
y2="-40"
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
strokeOpacity="0.2"
/>
</g>
@@ -380,7 +381,7 @@ export default function HeroIllustration() {
fill="white"
fillOpacity="0.08"
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
strokeOpacity="0.2"
/>
<path
@@ -388,7 +389,7 @@ export default function HeroIllustration() {
fill="white"
fillOpacity="0.05"
stroke="white"
strokeWidth="1"
strokeWidth={1 * strokeMultiplier}
strokeOpacity="0.15"
/>
</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,
});
}