Compare commits

..

29 Commits

Author SHA1 Message Date
57886a01d6 traefik
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m58s
2026-01-31 18:36:34 +01:00
c89bd8e80f staging deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m54s
2026-01-31 18:07:28 +01:00
9c54322654 datasheet download
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m1s
2026-01-31 10:40:17 +01:00
8a80eb7b9a og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m55s
2026-01-31 10:35:07 +01:00
c1773a7072 datasheets as downloads
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m59s
2026-01-31 10:29:39 +01:00
33ed13d255 og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m28s
2026-01-31 10:25:25 +01:00
0f5811edb9 og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m47s
2026-01-31 10:21:24 +01:00
e4eabd7a86 sheets
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 17m33s
2026-01-30 22:10:01 +01:00
757df76f36 terms
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m1s
2026-01-30 19:31:22 +01:00
14b2f83971 sheets 2026-01-30 00:17:46 +01:00
51565fdf41 sheets 2026-01-29 22:51:56 +01:00
506c8682fe umami
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-29 17:43:06 +01:00
a909de30f3 filter products
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-29 17:34:15 +01:00
a2f94f15bc og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m48s
2026-01-29 17:26:02 +01:00
13e56a88bc headings
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m3s
2026-01-29 16:51:11 +01:00
bb7d17001b og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m57s
2026-01-29 16:33:04 +01:00
920efa0083 pdf sheets 2026-01-29 16:27:09 +01:00
0b81d1a4cb og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 14s
2026-01-29 16:18:37 +01:00
1d5bdeba26 spacing 2026-01-29 15:59:53 +01:00
a0c3fbbc7e application field to mdx 2026-01-29 15:45:35 +01:00
8101a9f156 reveal
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m57s
2026-01-29 15:11:04 +01:00
7b6f4b5ea4 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m2s
2026-01-29 14:47:43 +01:00
658057cdb1 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 5m55s
2026-01-29 13:57:35 +01:00
2aa5d5b00e deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m53s
2026-01-29 13:52:37 +01:00
7f2f6f5aca deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 5m48s
2026-01-29 11:33:30 +01:00
4e50482769 remove redis
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 5m50s
2026-01-29 02:23:41 +01:00
1da1f05cdd remove varnish
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 4m55s
2026-01-29 02:12:39 +01:00
15cfb314b1 fixes
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m53s
2026-01-29 01:05:57 +01:00
a3da6192e3 remove smoke test 2026-01-29 00:34:41 +01:00
229 changed files with 1734 additions and 1383 deletions

4
.env
View File

@@ -12,10 +12,6 @@ WOOCOMMERCE_CONSUMER_KEY=ck_38d97df86880e8fefbd54ab5cdf47a9c5a9e5b39
WOOCOMMERCE_CONSUMER_SECRET=cs_d675ee2ac2ec7c22de84ae5451c07e42b1717759
WORDPRESS_APP_PASSWORD="DlJH 49dp fC3a Itc3 Sl7Z Wz0k"
# Redis Cache
REDIS_URL=redis://redis:6379/2
REDIS_KEY_PREFIX=klz:
# SMTP Configuration
MAIL_HOST=smtp.eu.mailgun.org
MAIL_PORT=587

View File

@@ -35,18 +35,19 @@ MAIL_PASSWORD=
MAIL_FROM=KLZ Cables <noreply@klz-cables.com>
MAIL_RECIPIENTS=info@klz-cables.com
# ────────────────────────────────────────────────────────────────────────────
# Redis Cache Configuration
# ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable Redis caching
REDIS_URL=redis://localhost:6379/2
REDIS_KEY_PREFIX=klz:
# ────────────────────────────────────────────────────────────────────────────
# Logging
# ────────────────────────────────────────────────────────────────────────────
LOG_LEVEL=info
# ────────────────────────────────────────────────────────────────────────────
# Deployment Configuration (CI/CD only)
# ────────────────────────────────────────────────────────────────────────────
# These are typically set by the CI/CD workflow
IMAGE_TAG=latest
TRAEFIK_HOST=klz-cables.com
ENV_FILE=.env
# ────────────────────────────────────────────────────────────────────────────
# Varnish Cache (Docker only)
# ────────────────────────────────────────────────────────────────────────────
@@ -68,7 +69,11 @@ VARNISH_CACHE_SIZE=256m
# ──────────────────
# 1. Build-time: Only NEXT_PUBLIC_* vars are needed (via --build-arg)
# 2. Runtime: All vars are loaded from .env file on the server
# 3. The .env file should exist at: /home/deploy/sites/klz-cables.com/.env
# 3. Branch Deployments:
# - main branch uses .env.prod
# - staging branch uses .env.staging
# - CI/CD supports STAGING_ prefix for all secrets to override defaults
# - TRAEFIK_HOST is automatically derived from NEXT_PUBLIC_BASE_URL
#
# Security:
# ─────────

View File

@@ -26,9 +26,6 @@ MAIL_PASSWORD=
MAIL_FROM=KLZ Cables <noreply@klz-cables.com>
MAIL_RECIPIENTS=info@klz-cables.com
# Redis Cache (optional)
REDIS_URL=redis://redis:6379/2
REDIS_KEY_PREFIX=klz:
# Varnish Cache Size (optional)
VARNISH_CACHE_SIZE=256m

View File

@@ -2,7 +2,7 @@ name: Build & Deploy KLZ Cables
on:
push:
branches: [main]
branches: [main, staging]
jobs:
build-and-deploy:
@@ -14,339 +14,146 @@ jobs:
# ═══════════════════════════════════════════════════════════════════════════════
- name: 📋 Log Workflow Start
run: |
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ KLZ Cables Deployment Workflow Started ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "📋 Workflow Information:"
echo " • Repository: ${{ github.repository }}"
echo " • Branch: ${{ github.ref }}"
echo "🚀 Starting deployment for ${{ github.repository }} (${{ github.ref }})"
echo " • Branch: ${{ github.ref_name }}"
echo " • Commit: ${{ github.sha }}"
echo " • Actor: ${{ github.actor }}"
echo " • Run ID: ${{ github.run_id }}"
echo " • Timestamp: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo ""
echo "🔍 Environment Details:"
echo " • Runner OS: ${{ runner.os }}"
echo " • Workspace: ${{ github.workspace }}"
echo ""
- name: Checkout repository
uses: actions/checkout@v4
- name: 🟢 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Registry Login Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🔐 Login to private registry
run: |
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ Step: Registry Login ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "🔐 Authenticating with private registry..."
echo " Registry: registry.infra.mintel.me"
echo " User: ${{ secrets.REGISTRY_USER != '' && '***' || 'NOT SET' }}"
echo ""
# Execute login with error handling
if echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin 2>&1; then
echo "✅ Registry login successful"
else
echo "❌ Registry login failed"
exit 1
fi
echo ""
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Build Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🏗️ Build application on runner
run: |
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ Step: Build Application on Runner ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "📦 Installing dependencies..."
npm ci
echo "🏗️ Building Next.js application..."
npm run build
env:
NEXT_PUBLIC_BASE_URL: ${{ secrets.NEXT_PUBLIC_BASE_URL }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Smoke Test Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🧪 Run Smoke Test
run: |
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ Step: Smoke Test ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "🧪 Starting local smoke test to verify build integrity..."
# Start the application in the background
# We use the standalone output which is what runs in production
export PORT=3001
export NODE_ENV=production
export NEXT_PUBLIC_BASE_URL="http://localhost:3001"
node .next/standalone/server.js &
APP_PID=$!
echo "⏳ Waiting for application to start on port 3001 (PID: $APP_PID)..."
# Wait for health check to pass
MAX_RETRIES=10
RETRY_COUNT=0
SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
if curl -s http://localhost:3001/health > /dev/null; then
SUCCESS=true
break
fi
echo " • Waiting... ($((RETRY_COUNT + 1))/$MAX_RETRIES)"
sleep 3
RETRY_COUNT=$((RETRY_COUNT + 1))
done
if [ "$SUCCESS" = true ]; then
echo "✅ Smoke test passed: Application started successfully"
kill $APP_PID
else
echo "❌ Smoke test failed: Application failed to start or health check timed out"
echo "🔍 Application Logs:"
# Try to get some logs before killing
kill -0 $APP_PID 2>/dev/null && sleep 2
kill $APP_PID
exit 1
fi
echo ""
echo "🔐 Authenticating with registry.infra.mintel.me..."
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Build Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🏗️ Build Docker image
env:
NEXT_PUBLIC_BASE_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_BASE_URL || (secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
run: |
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ Step: Build Docker Image ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "🏗️ Building Docker image with buildx..."
echo " Platform: linux/arm64"
echo " Target: registry.infra.mintel.me/mintel/klz-cables.com:latest"
echo ""
echo "📦 Build Arguments (NEXT_PUBLIC_* only - baked into client bundle):"
echo " • NEXT_PUBLIC_BASE_URL: ${{ secrets.NEXT_PUBLIC_BASE_URL != '' && '***' || 'NOT SET' }}"
echo " • NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID != '' && '***' || 'NOT SET' }}"
echo " • NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL != '' && '***' || 'NOT SET' }}"
echo ""
echo "⏱️ Build started at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo ""
# Execute build with detailed logging
set -e
echo "🏗️ Building Docker image (linux/arm64) for branch ${{ github.ref_name }}..."
docker buildx build \
--pull \
--platform linux/arm64 \
--build-arg NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}" \
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \
-t registry.infra.mintel.me/mintel/klz-cables.com:${{ github.sha }} \
-t registry.infra.mintel.me/mintel/klz-cables.com:latest \
--push .
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -eq 0 ]; then
echo ""
echo "✅ Build completed successfully at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo ""
echo "📊 Image Details:"
IMAGE_SIZE=$(docker inspect registry.infra.mintel.me/mintel/klz-cables.com:latest --format='{{.Size}}')
IMAGE_SIZE_MB=$((IMAGE_SIZE / 1024 / 1024))
echo " • Size: ${IMAGE_SIZE_MB}MB"
docker inspect registry.infra.mintel.me/mintel/klz-cables.com:latest --format=' • Created: {{.Created}}'
docker inspect registry.infra.mintel.me/mintel/klz-cables.com:latest --format=' • Architecture: {{.Architecture}}'
else
echo ""
echo "❌ Build failed with exit code: $BUILD_EXIT_CODE"
exit $BUILD_EXIT_CODE
fi
echo ""
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Deployment Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🚀 Deploy to production server
- name: 🚀 Deploy to server
env:
NEXT_PUBLIC_BASE_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_BASE_URL || (secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
SENTRY_DSN: ${{ github.ref_name == 'main' && secrets.SENTRY_DSN || (secrets.STAGING_SENTRY_DSN || secrets.SENTRY_DSN) }}
MAIL_HOST: ${{ github.ref_name == 'main' && secrets.MAIL_HOST || (secrets.STAGING_MAIL_HOST || secrets.MAIL_HOST) }}
MAIL_PORT: ${{ github.ref_name == 'main' && secrets.MAIL_PORT || (secrets.STAGING_MAIL_PORT || secrets.MAIL_PORT) }}
MAIL_USERNAME: ${{ github.ref_name == 'main' && secrets.MAIL_USERNAME || (secrets.STAGING_MAIL_USERNAME || secrets.MAIL_USERNAME) }}
MAIL_PASSWORD: ${{ github.ref_name == 'main' && secrets.MAIL_PASSWORD || (secrets.STAGING_MAIL_PASSWORD || secrets.MAIL_PASSWORD) }}
MAIL_FROM: ${{ github.ref_name == 'main' && secrets.MAIL_FROM || (secrets.STAGING_MAIL_FROM || secrets.MAIL_FROM) }}
MAIL_RECIPIENTS: ${{ github.ref_name == 'main' && secrets.MAIL_RECIPIENTS || (secrets.STAGING_MAIL_RECIPIENTS || secrets.MAIL_RECIPIENTS) }}
run: |
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ Step: Deploy to Production Server ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "🚀 Starting deployment process..."
echo " Target Server: alpha.mintel.me"
echo " Deploy User: deploy (via sudo from root)"
echo " Target Path: /home/deploy/sites/klz-cables.com"
echo ""
BRANCH=${{ github.ref_name }}
# Setup SSH with logging
echo "🔐 Setting up SSH connection..."
# Derive domain from NEXT_PUBLIC_BASE_URL (strip https:// and trailing slash)
DOMAIN=$(echo "$NEXT_PUBLIC_BASE_URL" | sed -E 's|https?://||' | sed -E 's|/.*||')
if [ "$BRANCH" = "main" ]; then
ENV_FILE=.env.prod
# For production, we want both root and www
TRAEFIK_HOST="\`$DOMAIN\`, \`www.$DOMAIN\`"
else
ENV_FILE=.env.staging
TRAEFIK_HOST="\`$DOMAIN\`"
fi
echo "🚀 Deploying branch $BRANCH to $ENV_FILE..."
echo "🌐 Domain: $DOMAIN"
# Setup SSH
mkdir -p ~/.ssh
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "🔑 Adding host to known_hosts..."
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
if [ $? -eq 0 ]; then
echo "✅ Host key added successfully"
else
echo "⚠️ Warning: Could not add host key"
fi
echo ""
# Create .env file content
echo "📝 Preparing environment configuration..."
cat > /tmp/klz-cables.env << EOF
# ============================================================================
# KLZ Cables - Production Environment Configuration
# KLZ Cables - Environment Configuration ($BRANCH)
# ============================================================================
# Auto-generated by CI/CD workflow
# DO NOT EDIT MANUALLY - Changes will be overwritten on next deployment
# ============================================================================
# Application
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
SENTRY_DSN=$SENTRY_DSN
MAIL_HOST=$MAIL_HOST
MAIL_PORT=$MAIL_PORT
MAIL_USERNAME=$MAIL_USERNAME
MAIL_PASSWORD=$MAIL_PASSWORD
MAIL_FROM=$MAIL_FROM
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL=${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}
# Error Tracking (GlitchTip/Sentry)
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
# Email Configuration (Mailgun)
MAIL_HOST=${{ secrets.MAIL_HOST }}
MAIL_PORT=${{ secrets.MAIL_PORT }}
MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}
MAIL_FROM=${{ secrets.MAIL_FROM }}
MAIL_RECIPIENTS=${{ secrets.MAIL_RECIPIENTS }}
# Redis Cache
REDIS_URL=${{ secrets.REDIS_URL }}
REDIS_KEY_PREFIX=${{ secrets.REDIS_KEY_PREFIX }}
# Varnish Cache Size
VARNISH_CACHE_SIZE=256m
# Deployment variables for docker-compose
IMAGE_TAG=${{ github.sha }}
TRAEFIK_HOST=$TRAEFIK_HOST
ENV_FILE=$ENV_FILE
EOF
echo "✅ Environment file prepared"
echo ""
# Upload .env and docker-compose.yml
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/$ENV_FILE
scp -o StrictHostKeyChecking=accept-new docker-compose.yml root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/docker-compose.yml
# Execute deployment commands with detailed logging
echo "📡 Connecting to server and executing deployment..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Copy .env file to server
echo "📤 Uploading environment configuration..."
scp -o StrictHostKeyChecking=accept-new \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ConnectTimeout=10 \
/tmp/klz-cables.env \
root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env
if [ $? -eq 0 ]; then
echo "✅ Environment file uploaded successfully"
else
echo "❌ Failed to upload environment file"
exit 1
fi
echo ""
# SSH to server and run deployment
echo "🚀 Executing deployment on server..."
ssh -o StrictHostKeyChecking=accept-new \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ConnectTimeout=10 \
root@alpha.mintel.me bash << EOF
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << EOF
set -e
cd /home/deploy/sites/klz-cables.com
PROJECT_DIR="/home/deploy/sites/klz-cables.com"
cd "\$PROJECT_DIR"
chmod 600 $ENV_FILE
chown deploy:deploy $ENV_FILE
echo "🔒 Securing environment file..."
chmod 600 .env
chown deploy:deploy .env
echo "🔐 Logging into Docker registry..."
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
echo "🔄 Pulling latest image..."
docker pull registry.infra.mintel.me/mintel/klz-cables.com:latest
echo "📥 Pulling images..."
IMAGE_TAG=${{ github.sha }} ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE pull
echo "🔄 Stopping existing containers..."
docker-compose down
echo "🚀 Starting containers..."
IMAGE_TAG=${{ github.sha }} ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE up -d
echo "🚀 Starting new containers..."
docker-compose up -d
echo "🧹 Cleaning up old images..."
docker system prune -f
echo "⏳ Waiting for services to be healthy..."
MAX_RETRIES=12
RETRY_COUNT=0
until curl -s -f http://localhost:3000/health > /dev/null || [ $RETRY_COUNT -eq $MAX_RETRIES ]; do
echo " • Waiting for health check... ($((RETRY_COUNT + 1))/$MAX_RETRIES)"
sleep 5
RETRY_COUNT=$((RETRY_COUNT + 1))
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo "❌ Health check failed after $MAX_RETRIES retries"
echo "🔍 Container logs:"
docker-compose logs --tail=50
echo "⏳ Giving the app a few seconds to warm up..."
sleep 10
echo "🔍 Checking container status..."
docker compose --env-file $ENV_FILE ps
if ! docker compose --env-file $ENV_FILE ps | grep -q "Up"; then
echo "❌ Container failed to start"
docker compose --env-file $ENV_FILE logs --tail=100
exit 1
fi
echo "✅ Health check passed!"
echo "🔍 Checking service status..."
docker-compose ps
echo ""
echo "✅ Deployment complete!"
EOF
DEPLOY_EXIT_CODE=$?
echo ""
# Clean up temporary env file
rm -f /tmp/klz-cables.env
if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
echo "✅ Deployment completed successfully at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
else
echo "❌ Deployment failed with exit code: $DEPLOY_EXIT_CODE"
echo ""
echo "🔍 Troubleshooting Tips:"
echo " • Check server connectivity: ping alpha.mintel.me"
echo " • Verify SSH key permissions on server"
echo " • Check disk space on target server"
echo " • Review docker compose configuration"
echo " • Verify all required secrets are set in Gitea"
exit $DEPLOY_EXIT_CODE
fi
echo ""
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Workflow Summary
@@ -354,33 +161,9 @@ jobs:
- name: 📊 Workflow Summary
if: always()
run: |
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ Workflow Summary ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "📊 Final Status:"
echo " • Workflow: ${{ job.status }}"
echo " • Completed: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo ""
echo "🎯 Deployment Target:"
echo " • Image: registry.infra.mintel.me/mintel/klz-cables.com:latest"
echo " • Server: alpha.mintel.me"
echo " • Service: klz-cables.com"
echo ""
echo "🔐 Security Notes:"
echo " • All secrets are masked (*** ) in logs"
echo " • SSH keys are created with 600 permissions"
echo " • Passwords are never displayed in plain text"
echo " • .env file is auto-generated from Gitea secrets"
echo " • .env file has 600 permissions on server"
echo ""
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
if [ "${{ job.status }}" == "success" ]; then
echo "║ ✅ DEPLOYMENT SUCCESSFUL ║"
else
echo "║ ❌ DEPLOYMENT FAILED ║"
fi
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo "📊 Status: ${{ job.status }}"
echo "🎯 Target: alpha.mintel.me"
echo "🌿 Branch: ${{ github.ref_name }}"
# ═══════════════════════════════════════════════════════════════════════════════
# NOTIFICATION: Gotify
@@ -391,7 +174,7 @@ jobs:
echo "Sending success notification to Gotify..."
RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=✅ Deployment Success: ${{ github.repository }}" \
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) was successful.
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref_name }}) was successful.
Commit: ${{ github.sha }}
Actor: ${{ github.actor }}
@@ -415,7 +198,7 @@ jobs:
echo "Sending failure notification to Gotify..."
RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=❌ Deployment Failed: ${{ github.repository }}" \
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) failed!
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref_name }}) failed!
Commit: ${{ github.sha }}
Actor: ${{ github.actor }}

View File

@@ -3,7 +3,7 @@ FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN apk add --no-cache libc6-compat curl
WORKDIR /app
# Install dependencies based on the preferred package manager
@@ -41,6 +41,9 @@ RUN npm run build
FROM base AS runner
WORKDIR /app
# Install curl for health checks
RUN apk add --no-cache curl
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

View File

@@ -145,8 +145,6 @@ Ensure these secrets are configured in your Gitea repository:
- `MAIL_PASSWORD` - SMTP password
- `MAIL_FROM` - Sender email
- `MAIL_RECIPIENTS` - Recipient emails (comma-separated)
- `REDIS_URL` - Redis connection URL
- `REDIS_KEY_PREFIX` - Redis key prefix (e.g., `klz:`)
**Infrastructure:**
- `REGISTRY_USER` - Docker registry username

View File

@@ -47,11 +47,6 @@ NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
# Redis (optional cache)
# Platform provides a shared redis container reachable as `redis`.
# Pick a dedicated DB index per app, e.g. redis://redis:6379/2
REDIS_URL=redis://redis:6379/2
REDIS_KEY_PREFIX=klz:
```
## 📊 Project Overview
@@ -108,7 +103,7 @@ app/
├── api/
│ └── contact/route.ts # Contact API
├── sitemap.ts # Sitemap generator
── robots.ts # Robots.txt generator
── robots.ts # Robots.txt generator
lib/
├── data.ts # Data access
@@ -119,7 +114,7 @@ components/
├── LocaleSwitcher.tsx # Language switcher
├── ContactForm.tsx # Contact form
├── CookieConsent.tsx # GDPR banner
── SEO.tsx # SEO utilities
── SEO.tsx # SEO utilities
data/
├── raw/ # WordPress export
@@ -227,21 +222,30 @@ GET /robots.txt
### Automatic Deployment (Current Setup)
The project uses **Gitea Actions** for CI/CD. Every push to `main` triggers:
The project uses **Gitea Actions** for CI/CD. Every push to `main` or `staging` triggers:
1. **Build**: Docker image built for `linux/arm64`
2. **Push**: Image pushed to `registry.infra.mintel.me`
3. **Deploy**: SSH to production server, pull and restart containers
1. **Build**: Docker image built for `linux/arm64` with branch-specific build args
2. **Push**: Image pushed to `registry.infra.mintel.me` with commit SHA tag
3. **Deploy**: SSH to production server, pull and restart containers using branch-specific `.env` files
**Workflow**: `.gitea/workflows/deploy.yml`
**Branch Deployments**:
- `main` branch: Deploys to production using `.env.prod`
- `staging` branch: Deploys to staging using `.env.staging`
**Environment Overrides**:
The CI/CD workflow supports `STAGING_`-prefixed secrets (e.g., `STAGING_NEXT_PUBLIC_BASE_URL`) to override default secrets when deploying the `staging` branch.
**Required Secrets** (configure in Gitea repository settings):
- `REGISTRY_USER` - Docker registry username
- `REGISTRY_PASS` - Docker registry password
- `ALPHA_SSH_KEY` - SSH private key for deployment
- `NEXT_PUBLIC_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`)
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
- `SENTRY_DSN` - Error tracking DSN
- `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `MAIL_RECIPIENTS` - Email configuration
### Manual Deployment
@@ -266,7 +270,7 @@ bash scripts/deploy-webhook.sh
### Architecture
```
Client → Traefik (TLS) → Varnish (Cache) → Next.js App
Client → Traefik (TLS) → Next.js App
```
**Domains**:
@@ -276,7 +280,6 @@ Client → Traefik (TLS) → Varnish (Cache) → Next.js App
**Services**:
- `app`: Next.js application (port 3000)
- `varnish`: HTTP cache layer
- `traefik`: Reverse proxy (external)
For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).

View File

@@ -1,10 +1,11 @@
import { notFound } from 'next/navigation';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { Container, Badge } from '@/components/ui';
import { Container, Badge, Heading } from '@/components/ui';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getPageBySlug, getAllPages } from '@/lib/pages';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { getOGImageMetadata } from '@/lib/metadata';
interface PageProps {
params: {
@@ -47,6 +48,7 @@ export async function generateMetadata({ params: { locale, slug } }: PageProps):
title: `${pageData.frontmatter.title} | KLZ Cables`,
description: pageData.frontmatter.excerpt || '',
url: `https://klz-cables.com/${locale}/${slug}`,
images: getOGImageMetadata(slug, pageData.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -74,9 +76,9 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="accent" className="mb-4 md:mb-6">{t('badge')}</Badge>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-0 leading-tight">
<Heading level={1} className="text-white mb-0">
{pageData.frontmatter.title}
</h1>
</Heading>
</div>
</Container>
</section>

View File

@@ -0,0 +1,74 @@
import { ImageResponse } from 'next/og';
import { getProductBySlug } from '@/lib/mdx';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export async function GET(
request: NextRequest,
{ params }: { params: { locale: string } }
) {
const { searchParams } = new URL(request.url);
const slug = searchParams.get('slug');
const locale = params.locale || 'en';
if (!slug) {
return new Response('Missing slug', { status: 400 });
}
const t = await getTranslations({ locale, namespace: 'Products' });
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
if (categories.includes(slug)) {
const categoryKey = slug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : slug;
const categoryDesc = t.has(`categories.${categoryKey}.description`) ? t(`categories.${categoryKey}.description`) : '';
return new ImageResponse(
(
<OGImageTemplate
title={categoryTitle}
description={categoryDesc}
label="Product Category"
/>
),
{
width: 1200,
height: 630,
}
);
}
const product = await getProductBySlug(slug, locale);
if (!product) {
return new ImageResponse(
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
}
const { origin } = new URL(request.url);
const featuredImage = product.frontmatter.images?.[0]
? (product.frontmatter.images[0].startsWith('http')
? product.frontmatter.images[0]
: `${origin}${product.frontmatter.images[0]}`)
: undefined;
return new ImageResponse(
(
<OGImageTemplate
title={product.frontmatter.title}
description={product.frontmatter.description}
label={product.frontmatter.categories?.[0] || 'Product'}
image={featuredImage?.startsWith('http') ? featuredImage : undefined}
/>
),
{
width: 1200,
height: 630,
}
);
}

View File

@@ -25,7 +25,7 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
image={featuredImage?.startsWith('http') ? featuredImage : undefined}
/>
),
{

View File

@@ -10,6 +10,8 @@ import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { Heading } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogPostProps {
params: {
@@ -42,6 +44,7 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
publishedTime: post.frontmatter.date,
authors: ['KLZ Cables'],
url: `https://klz-cables.com/${locale}/blog/${slug}`,
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -84,9 +87,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</div>
)}
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-8 leading-[1.1] drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
<Heading level={1} className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
{post.frontmatter.title}
</h1>
</Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
@@ -112,9 +115,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</div>
)}
<h1 className="text-4xl md:text-5xl font-bold text-text-primary mb-8 leading-tight">
<Heading level={1} className="mb-8">
{post.frontmatter.title}
</h1>
</Heading>
<div className="flex items-center gap-6 text-text-secondary font-medium">
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {

View File

@@ -0,0 +1,25 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const title = t('title');
const description = t('description');
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
label="Blog"
/>
),
{
width: 1200,
height: 630,
}
);
}

View File

@@ -3,6 +3,7 @@ import { getAllPosts } from '@/lib/blog';
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogIndexProps {
params: {
@@ -27,6 +28,7 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
title: `${t('title')} | KLZ Cables`,
description: t('description'),
url: `https://klz-cables.com/${locale}/blog`,
images: getOGImageMetadata('blog', t('title'), locale),
},
twitter: {
card: 'summary_large_image',
@@ -69,9 +71,9 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
<Badge variant="saturated" className="mb-4 md:mb-6">{t('featuredPost')}</Badge>
{featuredPost && (
<>
<h1 className="text-3xl md:text-6xl font-extrabold text-white mb-4 md:mb-8 leading-[1.1] line-clamp-3 md:line-clamp-none">
<Heading level={1} className="text-white mb-4 md:mb-8">
{featuredPost.frontmatter.title}
</h1>
</Heading>
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
{featuredPost.frontmatter.excerpt}
</p>

View File

@@ -4,6 +4,8 @@ import Reveal from '@/components/Reveal';
import { Container, Heading, Section } from '@/components/ui';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { SITE_URL } from '@/lib/schema';
import { getOGImageMetadata } from '@/lib/metadata';
import { Suspense } from 'react';
import dynamic from 'next/dynamic';
const LeafletMap = dynamic(() => import('@/components/LeafletMap'), {
@@ -40,14 +42,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
description,
url: `https://klz-cables.com/${locale}/contact`,
siteName: 'KLZ Cables',
images: [
{
url: 'https://klz-cables.com/logo.png',
width: 1200,
height: 630,
alt: 'KLZ Cables Contact',
},
],
images: getOGImageMetadata('contact', title, locale),
locale: `${locale.toUpperCase()}_DE`,
type: 'website',
},
@@ -55,7 +50,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
card: 'summary_large_image',
title: `${title} | KLZ Cables`,
description,
images: ['https://klz-cables.com/logo.png'],
images: [`${SITE_URL}/${locale}/contact/opengraph-image`],
},
robots: {
index: true,

View File

@@ -2,11 +2,15 @@ import Footer from '@/components/Footer';
import Header from '@/components/Header';
import JsonLd from '@/components/JsonLd';
import AnalyticsProvider from '@/components/analytics/AnalyticsProvider';
import { Viewport } from 'next';
import { Metadata, Viewport } from 'next';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
};
export const viewport: Viewport = {
width: 'device-width',

View File

@@ -2,7 +2,7 @@ import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
export const runtime = 'edge';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Index.meta' });

View File

@@ -1,6 +1,6 @@
import Hero from '@/components/home/Hero';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema } from '@/lib/schema';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import ProductCategories from '@/components/home/ProductCategories';
import WhatWeDo from '@/components/home/WhatWeDo';
import RecentPosts from '@/components/home/RecentPosts';
@@ -13,6 +13,7 @@ import CTA from '@/components/home/CTA';
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getOGImageMetadata } from '@/lib/metadata';
export default function HomePage({ params: { locale } }: { params: { locale: string } }) {
return (
@@ -70,6 +71,7 @@ export async function generateMetadata({ params: { locale } }: { params: { local
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}`,
images: getOGImageMetadata('', title, locale),
},
twitter: {
card: 'summary_large_image',

View File

@@ -5,12 +5,14 @@ import ProductSidebar from '@/components/ProductSidebar';
import ProductTabs from '@/components/ProductTabs';
import ProductTechnicalData from '@/components/ProductTechnicalData';
import RelatedProducts from '@/components/RelatedProducts';
import { Badge, Container, Section } from '@/components/ui';
import DatasheetDownload from '@/components/DatasheetDownload';
import { Badge, Container, Heading, Section } from '@/components/ui';
import { getDatasheetPath } from '@/lib/datasheets';
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { getProductOGImageMetadata } from '@/lib/metadata';
import { MDXRemote } from 'next-mdx-remote/rsc';
import Image from 'next/image';
import Link from 'next/link';
@@ -51,6 +53,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: `${categoryTitle} | KLZ Cables`,
description: categoryDesc,
url: `https://klz-cables.com/${locale}/products/${productSlug}`,
images: getProductOGImageMetadata(fileSlug, categoryTitle, locale),
},
twitter: {
card: 'summary_large_image',
@@ -79,6 +82,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
description: product.frontmatter.description,
type: 'website',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -164,9 +168,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
<span className="mx-3 opacity-30">/</span>
<span className="text-white/90">{categoryTitle}</span>
</nav>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-extrabold text-white mb-8 tracking-tight leading-[1.05]">
<Heading level={1} className="text-white mb-8">
{categoryTitle}
</h1>
</Heading>
<div className="h-1.5 w-24 bg-accent rounded-full" />
</div>
</Container>
@@ -309,9 +313,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
</Badge>
))}
</div>
<h1 className="text-6xl md:text-8xl lg:text-9xl font-black text-white mb-8 tracking-tighter leading-[0.9] uppercase">
<Heading level={1} className="text-white mb-8 uppercase">
{product.frontmatter.title}
</h1>
</Heading>
<p className="text-xl md:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
{product.frontmatter.description}
</p>
@@ -359,6 +363,19 @@ export default async function ProductPage({ params }: ProductPageProps) {
<MDXRemote source={processedContent} components={productComponents} />
</div>
{/* Datasheet Download Section - Only for Medium Voltage for now */}
{categoryFileSlug === 'medium-voltage-cables' && datasheetPath && (
<div className="mt-24 pt-24 border-t-2 border-neutral-dark/5">
<div className="mb-12">
<h2 className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-4">
{t('downloadDatasheet')}
</h2>
<div className="h-1.5 w-24 bg-accent rounded-full" />
</div>
<DatasheetDownload datasheetPath={datasheetPath} />
</div>
)}
{/* Structured Data */}
<JsonLd
id={`jsonld-${product.slug}`}

View File

@@ -5,10 +5,31 @@ import { OGImageTemplate } from '@/components/OGImageTemplate';
export const runtime = 'nodejs';
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug: string[] } }) {
const productSlug = slug[slug.length - 1];
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
const t = await getTranslations('Products');
// If no slug, it's the main products page
if (!slug || slug.length === 0) {
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
label="Products"
/>
),
{
width: 1200,
height: 630,
}
);
}
const productSlug = slug[slug.length - 1];
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
if (categories.includes(productSlug)) {
@@ -51,7 +72,7 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
title={product.frontmatter.title}
description={product.frontmatter.description}
label={product.frontmatter.categories?.[0] || 'Product'}
image={featuredImage}
image={featuredImage?.startsWith('http') ? featuredImage : undefined}
/>
),
{

View File

@@ -1,11 +1,12 @@
import Reveal from '@/components/Reveal';
import Scribble from '@/components/Scribble';
import { Badge, Button, Card, Container, Section } from '@/components/ui';
import { Badge, Button, Card, Container, Heading, Section } from '@/components/ui';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { mapFileSlugToTranslated } from '@/lib/slugs';
import { getOGImageMetadata } from '@/lib/metadata';
interface ProductsPageProps {
params: {
@@ -32,6 +33,7 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/products`,
images: getOGImageMetadata('products', title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -90,7 +92,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5">
{t('heroSubtitle')}
</Badge>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-extrabold text-white mb-4 md:mb-8 tracking-tight leading-[1.05]">
<Heading level={1} className="text-white mb-4 md:mb-8">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
@@ -99,7 +101,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
</span>
)
})}
</h1>
</Heading>
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
{t('subtitle')}
</p>

View File

@@ -15,7 +15,6 @@ export default async function Image({ params: { locale } }: { params: { locale:
title={title}
description={description}
label="Our Team"
image="https://klz-cables.com/uploads/2024/12/DSC07655-Large.webp"
/>
),
{

View File

@@ -3,6 +3,7 @@ import { Metadata } from 'next';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import { Section, Container, Heading, Badge, Button } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
import Image from 'next/image';
import Reveal from '@/components/Reveal';
import Gallery from '@/components/team/Gallery';
@@ -32,6 +33,7 @@ export async function generateMetadata({ params: { locale } }: TeamPageProps): P
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/team`,
images: getOGImageMetadata('team', title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -102,9 +104,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<Container className="relative z-10 text-center text-white max-w-5xl">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{t('hero.badge')}</Badge>
<h1 className="text-3xl md:text-6xl lg:text-7xl font-extrabold tracking-tight leading-[1.1] mb-4 md:mb-8">
<Heading level={1} className="text-white mb-4 md:mb-8">
{t('hero.subtitle')}
</h1>
</Heading>
<p className="text-lg md:text-2xl text-white/70 font-medium italic">
{t('hero.title')}
</p>

View File

@@ -0,0 +1,68 @@
'use client';
import { cn } from '@/components/ui/utils';
import { useTranslations } from 'next-intl';
interface DatasheetDownloadProps {
datasheetPath: string;
className?: string;
}
export default function DatasheetDownload({ datasheetPath, className }: DatasheetDownloadProps) {
const t = useTranslations('Products');
return (
<div className={cn("mt-8 animate-slight-fade-in-from-bottom", className)}>
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="group relative block w-full overflow-hidden rounded-[32px] bg-primary-dark p-1 transition-all duration-500 hover:shadow-[0_20px_50px_rgba(0,0,0,0.2)] hover:-translate-y-1"
>
{/* Animated Background Gradient */}
<div className="absolute inset-0 bg-gradient-to-r from-accent via-saturated to-accent opacity-20 group-hover:opacity-40 transition-opacity duration-500 animate-gradient-x" />
{/* Inner Content */}
<div className="relative flex items-center gap-6 rounded-[31px] bg-primary-dark/90 backdrop-blur-xl p-6 md:p-8 border border-white/10">
{/* Icon Container */}
<div className="relative flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-2xl bg-white/5 border border-white/10 group-hover:bg-accent group-hover:border-white/20 transition-all duration-500">
<div className="absolute inset-0 rounded-2xl bg-accent/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<svg
className="relative h-8 w-8 text-white transition-transform duration-500 group-hover:scale-110 group-hover:rotate-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
{/* Text Content */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-accent">PDF Datasheet</span>
</div>
<h3 className="text-xl md:text-2xl font-black text-white uppercase tracking-tighter leading-none group-hover:text-accent transition-colors duration-300">
{t('downloadDatasheet')}
</h3>
<p className="mt-2 text-sm font-medium text-white/60 leading-relaxed">
{t('downloadDatasheetDesc')}
</p>
</div>
{/* Arrow Icon */}
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/5 text-white/20 group-hover:bg-accent group-hover:text-white group-hover:translate-x-1 transition-all duration-500">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</a>
</div>
);
}

View File

@@ -29,7 +29,6 @@ export function OGImageTemplate({
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
padding: '80px',
position: 'relative',
fontFamily: 'Inter, sans-serif',
};
return (
@@ -39,13 +38,18 @@ export function OGImageTemplate({
<div
style={{
position: 'absolute',
inset: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
}}
>
<img
src={image}
alt=""
width="1200"
height="630"
style={{
width: '100%',
height: '100%',
@@ -55,8 +59,11 @@ export function OGImageTemplate({
<div
style={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.9) 0%, rgba(0,26,77,0.4) 100%)',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.9), rgba(0,26,77,0.4))',
}}
/>
</div>
@@ -70,8 +77,8 @@ export function OGImageTemplate({
right: '-100px',
width: '600px',
height: '600px',
borderRadius: '50%',
background: `radial-gradient(circle, ${accentGreen}1a 0%, transparent 70%)`,
borderRadius: '300px',
backgroundColor: `${accentGreen}1a`,
display: 'flex',
}}
/>

View File

@@ -3,6 +3,7 @@
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import RequestQuoteForm from '@/components/RequestQuoteForm';
import DatasheetDownload from '@/components/DatasheetDownload';
import Scribble from '@/components/Scribble';
import { cn } from '@/components/ui/utils';
@@ -64,33 +65,7 @@ export default function ProductSidebar({ productName, productImage, datasheetPat
{/* Datasheet Download */}
{datasheetPath && (
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="block bg-white rounded-2xl border border-neutral-medium overflow-hidden group transition-all duration-500 hover:shadow-xl hover:border-saturated/30 hover:-translate-y-0.5"
>
<div className="p-4 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-neutral-medium/20 flex items-center justify-center flex-shrink-0 group-hover:bg-saturated group-hover:text-white transition-all duration-500 text-saturated border border-transparent group-hover:border-white/20">
<svg className="w-6 h-6 transition-transform duration-500 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm md:text-base font-heading font-black text-neutral-dark m-0 uppercase tracking-tighter leading-tight group-hover:text-saturated transition-colors duration-300">
{t('downloadDatasheet')}
</h3>
<p className="text-text-secondary text-[10px] md:text-xs m-0 mt-0.5 font-semibold leading-tight truncate uppercase tracking-widest opacity-60">
{t('downloadDatasheetDesc')}
</p>
</div>
<div className="text-neutral-dark/20 group-hover:text-saturated transition-all duration-500 transform group-hover:translate-x-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</a>
<DatasheetDownload datasheetPath={datasheetPath} className="mt-0" />
)}
</div>
);

View File

@@ -49,6 +49,7 @@ export default function AnalyticsProvider() {
id="umami-analytics"
src="/stats/script.js"
data-website-id={websiteId}
data-host-url="/stats"
strategy="afterInteractive"
data-domains="klz-cables.com"
defer

View File

@@ -19,7 +19,7 @@ export default function Hero() {
variants={containerVariants}
>
<motion.div variants={headingVariants}>
<Heading level={1} className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]">
<Heading level={1} className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
@@ -65,7 +65,7 @@ export default function Hero() {
</Container>
<motion.div
className="relative md:absolute inset-0 z-0 w-full h-[40vh] md:h-full order-1 md:order-none md:mb-0 mt-[40px] md:mt-0 overflow-visible pointer-events-none"
className="relative md:absolute inset-0 z-0 w-full h-[40vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none"
initial={{ opacity: 0, scale: 0.95, filter: 'blur(20px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
transition={{ duration: 2.2, ease: 'easeOut', delay: 0.05 }}

View File

@@ -17,9 +17,9 @@ export function Heading({
const Tag = `h${level}` as any;
const sizes = {
1: 'text-3xl md:text-5xl lg:text-6xl font-extrabold leading-[1.1] tracking-tight',
2: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.2] tracking-tight',
3: 'text-xl md:text-2xl lg:text-3xl font-bold leading-[1.3] tracking-tight',
1: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.1] tracking-tight',
2: 'text-xl md:text-3xl lg:text-4xl font-bold leading-[1.2] tracking-tight',
3: 'text-lg md:text-2xl lg:text-3xl font-bold leading-[1.3] tracking-tight',
4: 'text-lg md:text-xl lg:text-2xl font-bold leading-[1.4]',
5: 'text-base md:text-lg font-bold leading-[1.5]',
6: 'text-base md:text-lg font-semibold leading-[1.6]',

View File

@@ -5,7 +5,7 @@ featuredImage: null
locale: de
---
*Stand November 2024*
*Stand Januar 2026*
## 1. Allgemeines
@@ -21,13 +21,12 @@ Sofern nicht ausdrücklich als bindend bezeichnet, sind unsere Angebote freiblei
## 3. Preise
Alle von uns genannten Preise verstehen sich zzgl. der jeweiligen gesetzlichen Mehrwertsteuer vor Metallzuschlag fracht- frei innerhalb der Bundesrepublik Deutschland (Festland), jedoch ohne Abladen. Die Verkaufspreise, soweit sie als Hohlpreis deklariert sind, enthalten keinerlei Metallwerte. Diese werden zusätzlich separat berechnet.
Die Preise gelten für den in unseren Angeboten und Auftragsbestätigungen aufgeführten Leistungs- und Lieferumfang. Mehrleistungen werden gesondert berechnet. Die Hohlpreise verstehen sich in Euro zuzüglich Metallzuschlag, gegebenenfalls Verpackung, auftragsspezifischer Schnittkosten und der gesetzlichen Mehrwertsteuer.
## 4. Metallnotierung
Basis zur Kupferabrechnung ist die Notierung "LME Copper official price cash offer", Durchschnitt des Liefervormonats zuzüglich der dann aktuellen von uns benannten Kupfer-Prämie.
Basis zur Aluminiumabrechnung ist die Notierung "LME Aluminium official price cash offer", Durchschnitt des Liefervormonats zuzüglich der dann von uns benannten Aluminium-Prämie. USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die entsprechenden Notierungen können Sie der Web-Seite [www.westmetall.com](https://www.westmetall.com) entnehmen. Die Prämienzuschläge können stark variieren und KLZ behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
Basis zur Kupferabrechnung ist die Notierung LME Copper official price cash offer, Durchschnitt des Liefervormonats zuzüglich der dann aktuellen von uns benannten Kupfer-Prämie.
Basis zur Aluminiumabrechnung ist die Notierung „LME Aluminium official price cash offer“, Durchschnitt des Liefervormonats zuzüglich der dann von uns benannten Aluminium-Prämie. USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die entsprechenden Notierungen können Sie der Web-Seite [www.westmetall.com](https://www.westmetall.com) entnehmen. Die Prämienzuschläge können stark variieren und KLZ behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
## 5. Metallzahl
@@ -43,7 +42,7 @@ Wir behalten uns an den von uns gelieferten Waren nachfolgend: Vorbehaltswar
## 8. Zahlungsbedingungen | Aufrechnung | Zurückbehaltungsrechte
Unsere Rechnungen sind 14 Tage nach Rechnungsdatum ohne jeden Abzug zahlbar. Bei Nichteinhaltung der vereinbarten Zahlungsbedingungen sind wir berechtigt, Zinsen in Höhe von 7 %-Punkten über dem Basiszinssatz zu berechnen; das Recht zur Geltendmachung weitergehender Schäden, insbesondere nachgewiesener höherer Zinsen, bleibt hiervon unberührt.
Unsere Rechnungen sind 10 Tage nach Rechnungsdatum ohne jeden Abzug zahlbar. Rechnungsstellung bzw. Datum ist grundsätzlich der Tag der Übergabe an den Spediteur soweit wir aus unseren deutschen Lägern liefern. Ansonsten gilt bei Direktimporten der Tag der Verzollung, der zeitnah zum Anliefertag liegt. Bei Nichteinhaltung der vereinbarten Zahlungsbedingungen sind wir berechtigt, Zinsen in Höhe von 7 %-Punkten über dem Basiszinssatz zu berechnen; das Recht zur Geltendmachung weitergehender Schäden, insbesondere nachgewiesener höherer Zinsen, bleibt hiervon unberührt.
## 9. Liefervorbehalt | Teillieferungen
@@ -69,13 +68,13 @@ Wird uns ein Abrufauftrag erteilt und werden über die Abruftermine keine gesond
## 12. Maß- und Gewichtsangaben
Alle Angaben über Durchmesser, Gewicht, technische Gestaltung, Herstellung und Umfang der von uns zu liefernden Ware stehen unter dem Vorbehalt der Abweichung innerhalb der handelsüblichen zulässigen Toleranzen. Darüber hinaus behalten wir uns Änderungen, die einer technischen Verbesserung dienen, jederzeit vor. Farbabweichungen und/oder Abweichungen in der äußeren Beschaffenheit der von uns zu liefernden Ware, die jedoch deren Qualität und technische Wirksamkeit unbeeinflusst lassen, begründen keine Mängelhaftungsansprüche des Bestellers.
Alle Angaben über Durchmesser, Gewicht, technische Gestaltung, Herstellung und Umfang der von uns zu liefernden Ware stehen unter dem Vorbehalt der Abweichung innerhalb der handelsüblichen zulässigen Toleranzen. Darüber hinaus behalten wir uns Änderungen, die einer technischen Verbesserung dienen, jederzeit vor. Farbabweichungen und/oder Abweichungen in der äußeren Beschaffenheit der von uns zu liefernden Ware, die jedoch deren Qualität und technische Wirksamkeit unbeeinflusst lässt, begründen keine Mängelhaftungsansprüche des Bestellers.
## 13. Gefahrübergang und -tragung
Die Lieferung erfolgt DAP frei Bestimmungsort Deutschland, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist.
Wird die bestellte Ware von uns versandbereit gestellt und/oder verzögert sich die Versendung und/oder der Abruf aus Gründen, die vom Besteller zu vertreten sind, sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen zu verlangen. Hierfür berechnen wir eine pauschale Entschädigung i.H.v 2% des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt beginnend mit der Lieferfrist bzw. mangels einer Lieferfrist mit der Mitteilung der Versandbereitschaft der Ware.
Wird die bestellte Ware von uns versandbereit gestellt und/oder verzögert sich die Versendung und/oder der Abruf aus Gründen, die vom Besteller zu vertreten sind, sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen für Einlagerungen zu verlangen. Hierfür berechnen wir eine pauschale Entschädigung i.H. von 2% des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt beginnend mit der Lieferfrist bzw. mangels einer Lieferfrist mit der Mitteilung der Versandbereitschaft der Ware.
Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt; die Pauschale ist aber auf weitergehende Geldansprüche anzurechnen. Dem Besteller bleibt der Nachweis gestattet, dass uns überhaupt kein oder nur ein wesentlich geringerer Schaden als vorstehende Pauschale entstanden ist. Rücksendungen an uns, die nicht vorher von uns schriftlich bestätigt worden sind, erfolgen auf alleinige Gefahr des Bestellers.
@@ -93,7 +92,9 @@ Weitergehende Ansprüche des Bestellers, gleich aus welchem Rechtsgrund, sind na
Die Verjährungsfristen für Mängelhaftungsansprüche beträgt 24 Monate ab Übergabe der Ware.
Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des Kabelschaden kommen, gilt hier im Zweifelsfall nur die Expertise des VDE-Instituts selbst. Andere, auch akkreditierte Testlabore, akzeptieren wir nicht. Wir weisen ausdrücklich daraufhin, dass beim Verlegen des Kabels in den Graben oder in Rohren, bzw. in Bauwerke eine ständige Sichtkontrolle durch den Kabelverleger vorzunehmen ist, ob Auffälligkeiten zu vermerken sind. Eine spätere Reklamation, die fahrlässiges Verhalten vermuten lässt, schränkt sich damit ein.
Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des Kabelschaden kommen, gilt hier im Zweifelsfall nur die Expertise des VDE-Instituts selbst. Andere, auch akkreditierte Testlabore, akzeptieren wir nicht.
Wir weisen ausdrücklich daraufhin, dass beim Verlegen des Kabels in den Gräben oder in Rohren, bzw. in Bauwerke eine ständige Sichtkontrolle durch den Kabelverleger vorzunehmen ist, ob Auffälligkeiten zu vermerken sind. Eine spätere Reklamation, die fahrlässiges Verhalten vermuten lässt, schränkt sich damit ein. Dies gilt auch bei der Annahme der Ware, wo offensichtliche Beschädigungen direkt zu kommunizieren sind. Spätere Ansprüche nach Akzeptanz einer einwandfreien Belieferung sind detailliert zu beweisen.
## 15. Schadenersatz | Gesamthaftung
@@ -109,4 +110,6 @@ Es gilt ausschließlich das Recht der Bundesrepublik Deutschland unter Ausschlus
Mit der Veröffentlichung der vorliegenden L&Z im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos.
Remshalden, 28.1.2026
[Download als PDF](/AGB-KLZ-1-2026.pdf)

View File

@@ -5,7 +5,7 @@ featuredImage: null
locale: en
---
*Status November 2024*
*Status January 2026*
## 1. General
@@ -21,7 +21,7 @@ Unless expressly designated as binding, our offers are non-binding; the customer
## 3. Prices
All prices stated by us are understood plus the respective statutory value-added tax, before metal surcharge, freight-free within the Federal Republic of Germany (mainland), however without unloading. The sales prices, insofar as they are declared as hollow prices, contain no metal values whatsoever. These are additionally calculated separately.
The prices apply to the scope of services and deliveries listed in our offers and order confirmations. Additional services will be charged separately. The hollow prices are in Euro plus metal surcharge, if applicable packaging, order-specific cutting costs and the statutory value-added tax.
## 4. Metal quotation
@@ -43,7 +43,7 @@ We retain title to the goods delivered by us hereinafter: reserved goods
## 8. Payment terms | Offsetting | Right of retention
Our invoices are payable 14 days after invoice date without any deduction. In case of non-compliance with the agreed payment terms, we are entitled to calculate interest at a rate of 7 percentage points above the base interest rate; the right to assert further damages, in particular proven higher interest, remains unaffected by this.
Our invoices are payable 10 days after invoice date without any deduction. Invoicing or date is basically the day of handover to the forwarder as far as we deliver from our German warehouses. Otherwise, in the case of direct imports, the day of customs clearance, which is close to the delivery day, applies. In case of non-compliance with the agreed payment terms, we are entitled to calculate interest at a rate of 7 percentage points above the base interest rate; the right to assert further damages, in particular proven higher interest, remains unaffected by this.
## 9. Delivery reservation | Partial deliveries
@@ -69,13 +69,13 @@ If a call-off order is issued to us and no separate written agreements are made
## 12. Dimension and weight specifications
All information about diameter, weight, technical design, manufacture and scope of the goods to be delivered by us are subject to the reservation of deviation within the commercially usual permissible tolerances. Darüber hinaus behalten wir uns Änderungen, die einer technischen Verbesserung dienen, jederzeit vor. Color deviations and/or deviations in the external characteristics of the goods to be delivered by us, which however leave their quality and technical effectiveness unaffected, do not give rise to any claims for defects by the orderer.
All information about diameter, weight, technical design, manufacture and scope of the goods to be delivered by us are subject to the reservation of deviation within the commercially usual permissible tolerances. Furthermore, we reserve the right to make changes that serve technical improvement at any time. Color deviations and/or deviations in the external characteristics of the goods to be delivered by us, which however leave their quality and technical effectiveness unaffected, do not give rise to any claims for defects by the orderer.
## 13. Transfer of risk and burden
Delivery is made DAP free destination Germany, where the place of performance for delivery and any subsequent performance is also located.
If the ordered goods are made ready for shipment by us and/or the dispatch and/or the call-off is delayed for reasons for which the orderer is responsible, we are entitled to demand compensation for the damage resulting therefrom including additional expenses. For this, we calculate a flat-rate compensation of 2% of the invoice amount for each month started, however maximum 10% in total, starting with the delivery period or lacking a delivery period with the notification of readiness for shipment of the goods.
If the ordered goods are made ready for shipment by us and/or the dispatch and/or the call-off is delayed for reasons for which the orderer is responsible, we are entitled to demand compensation for the damage resulting therefrom including additional expenses for storage. For this, we calculate a flat-rate compensation of 2% of the invoice amount for each month started, however maximum 10% in total, starting with the delivery period or lacking a delivery period with the notification of readiness for shipment of the goods.
Proof of higher damage and our legal claims (in particular compensation for additional expenses, reasonable compensation, termination) remain unaffected; the flat-rate is however to be credited against further monetary claims. The orderer is permitted to prove that no damage or only a substantially lower damage than the aforementioned flat-rate has arisen for us. Returns to us, which have not been confirmed by us in writing beforehand, are at the sole risk of the orderer.
@@ -93,7 +93,9 @@ Further claims of the orderer, regardless of the legal basis, are excluded or li
The limitation periods for claims for defects are 24 months from delivery of the goods.
If there are different opinions regarding cable damage in the event of a defect notification, only the expertise of the VDE Institute itself applies in case of doubt. We do not accept other, even accredited test laboratories. We expressly point out that when laying the cable in the trench or in pipes, or in buildings, a constant visual inspection must be carried out by the cable layer to check for any noticeable features. A later complaint that suggests negligent behavior is thus restricted.
If there are different opinions regarding cable damage in the event of a defect notification, only the expertise of the VDE Institute itself applies in case of doubt. We do not accept other, even accredited test laboratories.
We expressly point out that when laying the cable in the trench or in pipes, or in buildings, a constant visual inspection must be carried out by the cable layer to check for any noticeable features. A later complaint that suggests negligent behavior is thus restricted. This also applies to the acceptance of the goods, where obvious damage must be communicated directly. Subsequent claims after acceptance of a faultless delivery must be proven in detail.
## 15. Damages | Total liability
@@ -109,4 +111,6 @@ Only the law of the Federal Republic of Germany applies, excluding the UN Conven
With the publication of these DPT on the Internet, all previously used conditions of ours become void.
Remshalden, January 28, 2026
[Download as PDF](/AGB-KLZ-1-2026.pdf)

View File

@@ -10,6 +10,12 @@ categories:
- Solarkabel
images:
- /uploads/2025/06/H1Z2Z2-K-scaled.webp
application: >
Das H1Z2Z2-K entspricht der Norm DIN EN 50618 (VDE 0283-618) und ist speziell für die
Verkabelung von Photovoltaiksystemen konzipiert. Es kann fest verlegt oder flexibel
geführt werden im Gebäude, im Freien, in Industrieanlagen, landwirtschaftlichen
Betrieben oder sogar in explosionsgefährdeten Bereichen. Die Leitung ist UV-, ozon-
und wasserbeständig (AD7) und darf direkt in der Erde verlegt werden.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/N2X2Y-scaled.webp
application: >
Das N2X2Y entspricht den Normen HD 603 S1 Teil 5G und HD 627 S1 Teil 4H (gleichlautend
mit DIN VDE 0276-603 und -627) und ist für eine Betriebsfrequenz von 50Hz ausgelegt.
Es eignet sich für die feste Verlegung in Innenräumen, im Erdreich, im Freien und in
Industrieumgebungen mit hohen Temperatur- und Belastungsanforderungen. Die maximale
Betriebstemperatur liegt bei +90°C, im Kurzschlussfall sind +250°C zulässig.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die N2X(F)K2Y-Hochspannungskabelserie ist speziell für den Einsatz in
Hochspannungsanwendungen konzipiert, wobei sie eine optimale Leistung durch die
Verwendung von hochleitfähigem Kupfer und einer fortschrittlichen XLPE-Isolierung
bietet. Diese Kombination gewährleistet eine hohe Durchschlagsfestigkeit und eine
effiziente Thermozyklierung unter verschiedenen Betriebsbedingungen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,13 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die N2X(F)KLD2Y-Hochspannungskabel Serie 2 sind speziell für den Einsatz in
Hochspannungsanwendungen konzipiert, wobei sie eine optimale Leistung durch die
Verwendung von hochleitfähigen Kupferleitern und einer fortschrittlichen
XLPE-Isolierung bieten. Diese Kabelserie ist besonders geeignet für anspruchsvolle
industrielle Umgebungen, wo hohe Durchschlagsfestigkeit und
Thermozyklierungsfähigkeit erforderlich sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XS2Y-scaled.webp
application: >
Das N2XS2Y erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es eignet sich
zur Verlegung in Innenräumen, Kabelkanälen, im Freien, im Wasser, auf Kabelpritschen
und insbesondere im Erdreich. Aufgrund seines widerstandsfähigen Mantels wird es
häufig in Industrieanlagen, Kraftwerken und Schaltstationen eingesetzt, wo Stabilität
und Langlebigkeit gefordert sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XSF2Y-3-scaled.webp
application: >
Das N2XS(F)2Y erfüllt die gängigen Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502 und
ist für die Verlegung in Innenräumen, Kabelkanälen, im Freien, in Wasser, Erde und auf
Kabelpritschen geeignet. Besonders in EVU-Netzen, Industrieanlagen und Kraftwerken
spielt dieses Kabel seine Stärken aus überall dort, wo Langlebigkeit,
Wasserdichtigkeit und Sicherheit gefragt sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Hochspannungskabel
images:
- /uploads/2025/06/N2XSFL2Y-3-scaled.webp
application: >
Das N2XS(FL)2Y ist konzipiert für die Verlegung im Erdreich, in Kabelkanälen, in Rohren,
im Freien und in Innenräumen. Es entspricht der Norm IEC 60840 und lässt sich
individuell auf projektspezifische Anforderungen anpassen. Typisch eingesetzt wird es
in Übertragungsnetzen, Umspannwerken und großen Industrieanlagen, wo maximale
Zuverlässigkeit gefordert ist.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XSFL2Y-2-scaled.webp
application: >
Das N2XS(FL)2Y erfüllt die Standards DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es
eignet sich hervorragend für die Verlegung in Innenräumen, Kabelkanälen, im Freien, in
Erde, im Wasser sowie auf Kabelpritschen insbesondere in EVU-Netzen,
Industrieanlagen und Schaltstationen, wo erhöhte Anforderungen an mechanische
Belastbarkeit und Wasserdichtigkeit bestehen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XSY-scaled.webp
application: >
Das N2XSY erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es ist
ausgelegt für die Verlegung in Innenräumen, Kabelkanälen, im Wasser, im Erdreich oder
im Freien (bei geschützter Installation). Ob in Industrieanlagen, Kraftwerken oder
Schaltanlagen dieses Kabel sorgt für eine sichere und verlustarme
Energieübertragung im Mittelspannungsbereich.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,13 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/N2XY-scaled.webp
application: >
Das N2XY wird in Niederspannungsanlagen zur Energieverteilung eingesetzt zum Beispiel
in Kabeltrassen, Rohren, auf Wänden oder direkt im Erdreich. Es lässt sich sowohl im
Innen- als auch im Außenbereich installieren und ist auch für feuchte Umgebungen
geeignet. Dank verschiedener Aderkonfigurationen (einadrig bis vieradrig) und
Querschnitten bis 630mm² lässt sich das Kabel flexibel an die jeweilige Anwendung
anpassen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NA2X2Y-scaled.webp
application: >
Das NA2X2Y entspricht der Norm DIN VDE 0276-603 (HD 603) und ist ausgelegt für die
feste Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im Wasser oder im Freien.
Es kommt bevorzugt in Kraftwerken, Industrieanlagen, Schaltanlagen und Ortsnetzen zum
Einsatz überall dort, wo robuste Kabel gefragt sind, die im Betrieb und bei der
Verlegung hohen mechanischen Belastungen standhalten.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die NA2X(F)K2Y-Hochspannungskabelserie ist speziell für den Einsatz in
Hochspannungsanwendungen entwickelt worden, wobei sie sich durch eine hohe
Strombelastbarkeit und exzellente Kurzschlussstromfestigkeit auszeichnet. Diese Kabel
sind mit einer fortschrittlichen XLPE-Isolierung ausgestattet, die eine hohe
Durchschlagsfestigkeit and Thermozyklierungsfähigkeit bietet.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die NA2X(F)KLD2Y-Hochspannungskabelserie ist für den Einsatz in
Hochspannungsanwendungen konzipiert und bietet eine optimale Lösung für
anspruchsvolle industrielle Umgebungen. Mit ihrer fortschrittlichen
XLPE-Isolierung und Kupferleitern erfüllt sie hohe Anforderungen an die
Durchschlagsfestigkeit und Thermozyklierung.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XS2Y-scaled.webp
application: >
Das NA2XS2Y erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502 und ist
speziell für die feste Verlegung in Innenräumen, Kabelkanälen, im Freien, in Erde und
in Wasser ausgelegt. Es findet seinen Einsatz in Industrieanlagen, Schaltstationen
und Kraftwerken, besonders dort, wo das Kabel beim Verlegen oder im Betrieb
mechanisch stark beansprucht wird.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XSF2Y-3-scaled.webp
application: >
Das NA2XS(F)2Y entspricht den Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es
eignet sich für die Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im Wasser, im
Freien oder auf Kabelpritschen. Der Einsatzschwerpunkt liegt in EVU-Netzen,
Industrieanlagen und Umspannwerken, wo zusätzliche Sicherheitsreserven gegen
eindringende Feuchtigkeit und mechanische Belastung erforderlich sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Hochspannungskabel
images:
- /uploads/2025/06/NA2XSFL2Y-3-scaled.webp
application: >
Das NA2XS(FL)2Y erfüllt die Anforderungen der IEC 60840 und eignet sich für die
Verlegung im Erdreich, in Kabelkanälen, in Innenräumen, in Rohren und im Freien. Es
wird projektbezogen gefertigt und kommt insbesondere in Übertragungsnetzen,
Versorgungs-Infrastrukturen und Umspannwerken zum Einsatz, wo Sicherheit und
Langlebigkeit an erster Stelle stehen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,13 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XSFL2Y-3-scaled.webp
application: >
Das NA2XS(FL)2Y erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es ist
ideal für die Verlegung in Energieversorgungsnetzen (EVU), Innenräumen, Kabelkanälen,
im Freien, in Erde und in Wasser geeignet. Dank seiner Konstruktion mit
längswasserdichter Ausführung und Alu-PE-Schichtenmantel bleibt es auch bei
Beschädigungen betriebssicher der Wassereinfluss wird gezielt auf die Schadstelle
begrenzt.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XSY-scaled.webp
application: >
Das NA2XSY erfüllt die Anforderungen der Normen DIN VDE 0276-620, HD 620 S2 und IEC
60502. Es eignet sich für die Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im
Wasser oder im Freien allerdings nur bei geschützter Verlegung. Typische
Einsatzorte sind Industrieanlagen, Kraftwerke und Schaltanlagen, in denen
Mittelspannung mit hoher Betriebssicherheit transportiert werden muss.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,11 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NA2XY-scaled.webp
application: >
Das NA2XY entspricht der Norm DIN VDE 0276-603 (HD 603) und ist ideal für die feste
Verlegung in Innenräumen, Kabelkanälen, im Freien, im Wasser oder im Erdreich
geeignet. Typische Einsatzorte sind Kraftwerke, Industrieanlagen, Schaltanlagen sowie
Ortsnetze, bei denen mechanische Belastung im Betrieb berücksichtigt werden muss.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NAY2Y-scaled.webp
application: >
Das NAY2Y erfüllt die Anforderungen der Norm TP PRAKAB 12/03 in Anlehnung an VDE
0276-603 und eignet sich für die feste Verlegung in Innenräumen, Kabelkanälen, im
Erdreich, im Wasser und im Außenbereich. Es ist ideal für Anwendungen in Kraftwerken,
Industrie- und Schaltanlagen sowie in lokalen Versorgungsnetzen überall dort, wo
mechanische Belastung im Betrieb oder bei der Verlegung eine Rolle spielt.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NAYCWY-scaled.webp
application: >
Das NAYCWY entspricht der Norm DIN VDE 0276-603 (HD 603) und eignet sich für den
Einsatz in Kraftwerken, Industrieanlagen, Schaltanlagen und Ortsnetzen. Es lässt sich
fest verlegen in Innenräumen, Kabelkanälen, im Freien, im Erdreich oder in Wasser.
Dank des konzentrischen Leiters bietet es zusätzlichen Schutz bei mechanischer
Beschädigung und ermöglicht eine sichere Potenzialführung.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NAYY-scaled.webp
application: >
Das NAYY ist ein Energieverteilungskabel nach VDE 0276-603, das sich besonders für
Anwendungen in Kraftwerken, Ortsnetzen, Industrie- und Schaltanlagen eignet. Dank
seiner robusten Konstruktion lässt es sich fest verlegen sei es im Innenraum, im
Kabelkanal, im Freien oder im Erdreich. Auch bei Installation in Wasser bleibt das
Kabel zuverlässig im Betrieb.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NY2Y-scaled.webp
application: >
Das NY2Y ist ein Niederspannungskabel für den Einsatz in Kraftwerken, Industrie- und
Schaltanlagen sowie in Ortsnetzen. Es eignet sich für die feste Verlegung in
Innenräumen, Kabelkanälen, im Freien, im Wasser und im Erdreich überall dort, wo
starke mechanische Belastungen beim Verlegen und im Betrieb zu erwarten sind. Die
Konstruktion erfüllt die Vorgaben gemäß TP PRAKAB 16/03 in Anlehnung an VDE 0276-603.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,13 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NYCWY-scaled.webp
application: >
Das NYCWY gehört zu den klassischen Niederspannungskabeln nach VDE-Standard und ist
für Nennspannungen bis 1kV ausgelegt. Es kommt überall dort zum Einsatz, wo Energie
zuverlässig verteilt werden muss in Gebäuden, Industrieanlagen, Trafostationen oder
direkt im Erdreich. Auch in Kabeltrassen, Betonumgebungen oder unter Wasser lässt es
sich problemlos verlegen. Die Materialwahl sorgt dafür, dass dieses Kabel selbst
unter rauen Bedingungen durchhält ganz ohne zusätzliche Schutzmaßnahmen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,9 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NYY-scaled.webp
application: |
Verwendung
Das Kabel entspricht den Normen DIN VDE 0276-603. Es eignet sich für die Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im Wasser, im Freien oder auf Kabelpritschen, sofern keine besonderen mechanischen Beanspruchungen zu erwarten sind. Der Einsatzschwerpunkt liegt in Kraftwerken, Industrieanlagen und Ortsnetzen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ categories:
- Solar Cables
images:
- /uploads/2025/06/H1Z2Z2-K-scaled.webp
application: >
The H1Z2Z2-K complies with DIN EN 50618 (VDE 0283-618) and is specifically designed for
the cabling of photovoltaic systems. It can be installed permanently or used flexibly
indoors, outdoors, in industrial facilities, agricultural operations, or even in
hazardous (explosive) areas. The cable is UV-, ozone- and water-resistant (AD7) and
can be laid directly in the ground.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/N2X2Y-scaled.webp
application: >
The N2X2Y complies with HD 603 S1 Part 5G and HD 627 S1 Part 4H (equivalent to DIN VDE
0276-603 and -627) and is designed for an operating frequency of 50 Hz. It is suitable
for fixed installation indoors, underground, outdoors, and in industrial environments
with high temperature and load requirements. The maximum operating temperature is
+90 °C, and +250 °C is permissible under short-circuit conditions.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The N2X(F)K2Y high-voltage cable series is tailored for robust performance in
high-voltage power systems, featuring copper conductors and cross-linked
polyethylene (XLPE) insulation. This combination ensures high dielectric strength
and excellent thermal cycling resistance, crucial for maintaining integrity and
functionality over long operational periods.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -8,6 +8,11 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The N2X(F)KLD2Y-high-voltage-cables-2 series is engineered to meet the rigorous
demands of high-voltage power transmission with a focus on durability, efficiency,
and safety. This series is ideal for applications requiring high dielectric
strength and excellent thermal cycling resistance.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XS2Y-scaled.webp
application: >
The N2XS2Y complies with the standards DIN VDE 0276-620, HD 620 S2 and IEC 60502. It is
suitable for installation indoors, in cable ducts, outdoors, in water, on cable
trays, and especially underground. Thanks to its robust sheath, it is frequently
used in industrial plants, power stations, and switching stations, where stability
and durability are essential.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XSF2Y-3-scaled.webp
application: >
The N2XSF2Y complies with common standards DIN VDE 0276-620, HD 620 S2 and IEC 60502,
and is suitable for installation indoors, in cable ducts, outdoors, in water,
underground, and on cable trays. This cable proves its strengths especially in
utility grids, industrial plants, and power stations wherever durability,
watertightness, and safety are essential.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- High Voltage Cables
images:
- /uploads/2025/06/N2XSFL2Y-3-scaled.webp
application: >
The N2XS(FL)2Y is designed for installation in the ground, in cable ducts, pipes,
outdoor areas, and indoor spaces. It complies with the IEC 60840 standard and can be
tailored to specific project requirements. It is typically used in transmission
networks, substations, and large industrial facilities where maximum reliability is
essential.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,11 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XSFL2Y-2-scaled.webp
application: >
The N2XS(FL)2Y meets the standards DIN VDE 0276-620, HD 620 S2 and IEC 60502. It is
ideally suited for installation indoors, in cable ducts, outdoors, in soil, in water,
and on cable trays especially in utility grids, industrial plants, and switching
stations, where high demands on mechanical strength and water resistance apply.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,11 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XSY-scaled.webp
application: >
The N2XSY meets the standards DIN VDE 0276-620, HD 620 S2 and IEC 60502. It is designed
for installation indoors, in cable ducts, in water, underground, or outdoors (when
protected). Whether in industrial plants, power stations, or substations this cable
ensures safe and low-loss power transmission in medium-voltage networks.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/N2XY-scaled.webp
application: >
The N2XY is used in low-voltage systems for power distribution for example in cable
trays, conduits, on walls, or directly underground. It can be installed both indoors
and outdoors and is also suitable for humid environments. Thanks to various core
configurations (single-core to four-core) and cross-sections up to 630 mm², the cable
can be flexibly adapted to the respective application.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NA2X2Y-scaled.webp
application: >
The NA2X2Y complies with DIN VDE 0276-603 (HD 603) and is designed for fixed
installation indoors, in cable ducts, underground, in water, or outdoors. It is
primarily used in power plants, industrial facilities, and switching stations as well
as local distribution networks wherever robust cables are needed that can withstand
high mechanical stress during installation and operation.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,11 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The NA2X(F)K2Y high-voltage cable series is engineered to meet the rigorous demands
of modern industrial and power distribution applications. It combines high-grade
copper conductors with cross-linked polyethylene (XLPE) insulation, ensuring
superior dielectric strength and thermal cycling resilience.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,11 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The NA2X(F)KLD2Y high-voltage cable series is engineered to meet the rigorous
demands of modern industrial electrical networks, offering high dielectric
strength and excellent thermal cycling resistance. This series is ideal for
applications that require reliable power distribution in high-voltage settings.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/NA2XS2Y-scaled.webp
application: >
The NA2XS2Y complies with DIN VDE 0276-620, HD 620 S2 and IEC 60502 standards, and is
specifically designed for fixed installation indoors, in cable ducts, outdoors, in
soil and water. It is commonly used in industrial facilities, switching stations, and
power plants, especially where the cable is exposed to high mechanical stress during
installation or operation.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/NA2XSF2Y-3-scaled.webp
application: >
The NA2XS(F)2Y complies with standards DIN VDE 0276-620, HD 620 S2 and IEC 60502. It is
suitable for installation indoors, in cable ducts, underground, in water, outdoors,
or on cable trays. Its main applications are in utility grids, industrial facilities,
and substations, where additional safety reserves against moisture ingress and
mechanical stress are required.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- High Voltage Cables
images:
- /uploads/2025/06/NA2XSFL2Y-3-scaled.webp
application: >
The NA2XS(FL)2Y meets the requirements of IEC 60840 and is suitable for installation
in the ground, in cable ducts, indoors, in pipes, and outdoors. It is manufactured
based on project specifications and is particularly used in transmission networks,
utility infrastructures, and substations where safety and durability are top
priorities.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/NA2XSFL2Y-3-scaled.webp
application: >
The NA2XS(FL)2Y cable complies with standards DIN VDE 0276-620, HD 620 S2 and IEC
60502. It is ideal for installation in power supply networks (utilities), indoors, in
cable ducts, outdoors, in soil, and in water. Thanks to its longitudinally watertight
design and Al/PE sheath construction, it remains operational even when damaged
water ingress is effectively limited to the affected area.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/NA2XSY-scaled.webp
application: >
The NA2XSY meets the requirements of DIN VDE 0276-620, HD 620 S2, and IEC 60502. It is
suitable for installation indoors, in cable ducts, underground, in water, or outdoors
but only when installed with protection. Typical areas of application include
industrial plants, power stations, and switching stations where medium voltage must
be transported with high operational safety.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NA2XY-scaled.webp
application: >
The NA2XY complies with DIN VDE 0276-603 (HD 603) and is ideal for fixed installation
in indoor spaces, cable ducts, outdoors, in water, or underground. Typical areas of
application include power plants, industrial facilities, and switching stations as
well as local distribution networks, where mechanical stress during operation must be
considered.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NAY2Y-scaled.webp
application: >
The NAY2Y meets the requirements of the TP PRAKAB 12/03 standard based on VDE 0276-603
and is suitable for fixed installation indoors, in cable ducts, underground, in
water, and outdoors. It is ideal for applications in power plants, industrial and
switching stations, as well as local supply networks wherever mechanical stress
during installation or operation plays a significant role.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NAYCWY-scaled.webp
application: >
The NAYCWY complies with DIN VDE 0276-603 (HD 603) and is suitable for use in power
plants, industrial facilities, switching stations, and local networks. It can be
permanently installed indoors, in cable ducts, outdoors, underground, or in water.
Thanks to its concentric conductor, it offers additional protection in case of
mechanical damage and enables safe potential equalization.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NAYY-scaled.webp
application: >
The NAYY is a power distribution cable according to VDE 0276-603, particularly
suitable for use in power plants, local networks, industrial facilities, and
switching stations. Thanks to its robust design, it can be installed permanently
whether indoors, in cable ducts, outdoors, or underground. Even when installed in
water, the cable remains reliable in operation.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NY2Y-scaled.webp
application: >
The NY2Y is a low-voltage cable for use in power plants, industrial facilities,
switching stations, and local distribution networks. It is suitable for fixed
installation indoors, in cable ducts, outdoors, in water, and underground wherever
strong mechanical stress is expected during installation and operation. The
construction complies with TP PRAKAB 16/03 based on VDE 0276-603.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,14 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NYCWY-scaled.webp
application: >
The NYCWY is one of the classic low-voltage cables according to VDE standard and is
designed for rated voltages up to 1 kV. It is used wherever reliable energy
distribution is required in buildings, industrial plants, transformer stations, or
directly underground. It can also be easily installed in cable trays, concrete
environments, or underwater. The choice of materials ensures that this cable
withstands even harsh conditions without the need for additional protective
measures.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,9 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NYY-scaled.webp
application: |
Application
The cable complies with the standards DIN VDE 0276-603. It is suitable for installation in interiors, cable ducts, in the ground, in water, outdoors or on cable trays, provided that no special mechanical stresses are to be expected. The main focus of application is in power plants, industrial plants and local networks.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -1,64 +1,33 @@
services:
# Varnish sits between Traefik and the application.
#
# Flow:
# Client -> Traefik -> Varnish -> app
#
# Traefik keeps TLS + compression; Varnish adds HTTP caching for static assets.
varnish:
image: varnish:7
app:
image: registry.infra.mintel.me/mintel/klz-cables.com:${IMAGE_TAG:-latest}
restart: always
networks:
- infra
depends_on:
- app
command: >-
varnishd
-F
-f /etc/varnish/default.vcl
-s malloc,${VARNISH_CACHE_SIZE:-256m}
volumes:
- ./varnish/default.vcl:/etc/varnish/default.vcl:ro
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:80/health || wget --quiet --tries=1 --spider http://localhost:80/ || true"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
ports:
- "3000:3000"
env_file:
- ${ENV_FILE:-.env}
labels:
- "traefik.enable=true"
# HTTP HTTPS redirect (Challenge-Schutz für ALLE)
- "traefik.http.routers.klz-cables-web.rule=(Host(`klz-cables.com`) || Host(`www.klz-cables.com`) || Host(`staging.klz-cables.com`)) && !PathPrefix(`/.well-known/acme-challenge/`)"
# HTTP HTTPS redirect
- "traefik.http.routers.klz-cables-web.rule=Host(${TRAEFIK_HOST}) && !PathPrefix(`/.well-known/acme-challenge/`)"
- "traefik.http.routers.klz-cables-web.entrypoints=web"
- "traefik.http.routers.klz-cables-web.middlewares=redirect-https"
# HTTPS router (für ALLE drei Domains)
- "traefik.http.routers.klz-cables.rule=Host(`klz-cables.com`) || Host(`www.klz-cables.com`) || Host(`staging.klz-cables.com`)"
# HTTPS router
- "traefik.http.routers.klz-cables.rule=Host(${TRAEFIK_HOST})"
- "traefik.http.routers.klz-cables.entrypoints=websecure"
- "traefik.http.routers.klz-cables.tls.certresolver=le"
- "traefik.http.routers.klz-cables.tls=true"
- "traefik.http.routers.klz-cables.service=klz-cables"
- "traefik.http.services.klz-cables.loadbalancer.server.port=80"
- "traefik.http.services.klz-cables.loadbalancer.server.port=3000"
- "traefik.http.services.klz-cables.loadbalancer.server.scheme=http"
# Forwarded Headers (für Apps, die HTTPS erwarten)
# Forwarded Headers
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
# Middlewares anhängen
# Middlewares
- "traefik.http.routers.klz-cables.middlewares=klz-forward,compress"
app:
image: registry.infra.mintel.me/mintel/klz-cables.com:latest
restart: always
networks:
- infra
env_file:
- .env
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:3000/health || wget --quiet --tries=1 --spider http://localhost:3000/ || true"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
infra:
external: true

View File

@@ -36,8 +36,7 @@ The application uses a clean, robust, **fully automated** environment variable s
│ │
│ /home/deploy/sites/klz-cables.com/ │
│ ├── .env ← Runtime environment vars │
── docker-compose.yml ← Loads .env file │
│ └── varnish/ │
── docker-compose.yml ← Loads .env file │
└─────────────────────────────────────────────────────────────┘
```
@@ -71,7 +70,6 @@ These are loaded from the `.env` file at runtime and are only available on the s
| `MAIL_RECIPIENTS` | ❌ No | Comma-separated list of recipient emails |
| `REDIS_URL` | ❌ No | Redis connection URL (e.g., `redis://redis:6379/2`) |
| `REDIS_KEY_PREFIX` | ❌ No | Redis key prefix (default: `klz:`) |
| `VARNISH_CACHE_SIZE` | ❌ No | Varnish cache size (default: `256m`) |
## Local Development

View File

@@ -56,9 +56,6 @@ MAIL_RECIPIENTS=info@klz-cables.com
# Redis Cache
REDIS_URL=redis://redis:6379/2
REDIS_KEY_PREFIX=klz:
# Varnish Cache Size
VARNISH_CACHE_SIZE=256m
EOF
```
@@ -86,47 +83,13 @@ ls -la /home/deploy/sites/klz-cables.com/.env
# Or manually:
cat > /home/deploy/sites/klz-cables.com/docker-compose.yml << 'EOF'
services:
varnish:
image: varnish:7
restart: always
networks:
- infra
depends_on:
- app
command: >-
varnishd
-F
-f /etc/varnish/default.vcl
-s malloc,${VARNISH_CACHE_SIZE:-256m}
volumes:
- ./varnish/default.vcl:/etc/varnish/default.vcl:ro
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:80/health || wget --quiet --tries=1 --spider http://localhost:80/ || true"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
labels:
- "traefik.enable=true"
- "traefik.http.routers.klz-cables-web.rule=(Host(\`klz-cables.com\`) || Host(\`www.klz-cables.com\`) || Host(\`staging.klz-cables.com\`)) && !PathPrefix(\`/.well-known/acme-challenge/\`)"
- "traefik.http.routers.klz-cables-web.entrypoints=web"
- "traefik.http.routers.klz-cables-web.middlewares=redirect-https"
- "traefik.http.routers.klz-cables.rule=Host(\`klz-cables.com\`) || Host(\`www.klz-cables.com\`) || Host(\`staging.klz-cables.com\`)"
- "traefik.http.routers.klz-cables.entrypoints=websecure"
- "traefik.http.routers.klz-cables.tls.certresolver=le"
- "traefik.http.routers.klz-cables.tls=true"
- "traefik.http.routers.klz-cables.service=klz-cables"
- "traefik.http.services.klz-cables.loadbalancer.server.port=80"
- "traefik.http.services.klz-cables.loadbalancer.server.scheme=http"
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
- "traefik.http.routers.klz-cables.middlewares=klz-forward,compress"
app:
image: registry.infra.mintel.me/mintel/klz-cables.com:latest
restart: always
networks:
- infra
ports:
- "3000:3000"
env_file:
- .env
healthcheck:
@@ -135,6 +98,21 @@ services:
timeout: 5s
retries: 5
start_period: 30s
labels:
- "traefik.enable=true"
- "traefik.http.routers.klz-cables-web.rule=(Host(`klz-cables.com`) || Host(`www.klz-cables.com`) || Host(`staging.klz-cables.com`)) && !PathPrefix(`/.well-known/acme-challenge/`)"
- "traefik.http.routers.klz-cables-web.entrypoints=web"
- "traefik.http.routers.klz-cables-web.middlewares=redirect-https"
- "traefik.http.routers.klz-cables.rule=Host(`klz-cables.com`) || Host(`www.klz-cables.com`) || Host(`staging.klz-cables.com`)"
- "traefik.http.routers.klz-cables.entrypoints=websecure"
- "traefik.http.routers.klz-cables.tls.certresolver=le"
- "traefik.http.routers.klz-cables.tls=true"
- "traefik.http.routers.klz-cables.service=klz-cables"
- "traefik.http.services.klz-cables.loadbalancer.server.port=3000"
- "traefik.http.services.klz-cables.loadbalancer.server.scheme=http"
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
- "traefik.http.routers.klz-cables.middlewares=klz-forward,compress"
networks:
infra:
@@ -142,17 +120,7 @@ networks:
EOF
```
### 5. Create Varnish Configuration
```bash
# Create varnish directory
mkdir -p /home/deploy/sites/klz-cables.com/varnish
# Copy varnish configuration from repository
# This should be in the repository at varnish/default.vcl
```
### 6. Create Deployment Script
### 5. Create Deployment Script
```bash
cat > /home/deploy/deploy.sh << 'EOF'
@@ -162,7 +130,7 @@ set -e
PROJECT_DIR="/home/deploy/sites/klz-cables.com"
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ KLZ Cables - Deployment Script ║"
echo "║ KLZ Cables - Deployment Script ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
@@ -206,7 +174,7 @@ EOF
chmod +x /home/deploy/deploy.sh
```
### 7. Configure Docker Registry Access
### 6. Configure Docker Registry Access
The deployment script needs registry credentials. These are passed as environment variables from the CI/CD workflow:

View File

@@ -42,11 +42,7 @@ function createConfig() {
},
cache: {
redis: {
url: env.REDIS_URL,
keyPrefix: env.REDIS_KEY_PREFIX,
enabled: Boolean(env.REDIS_URL),
},
enabled: false,
},
logging: {
@@ -116,11 +112,7 @@ export function getMaskedConfig() {
},
},
cache: {
redis: {
url: mask(c.cache.redis.url),
keyPrefix: c.cache.redis.keyPrefix,
enabled: c.cache.redis.enabled,
},
enabled: c.cache.enabled,
},
logging: {
level: c.logging.level,

View File

@@ -15,6 +15,9 @@ export function getDatasheetPath(slug: string, locale: string): string | null {
// Normalize slug: remove common suffixes that might not be in the PDF filename
const normalizedSlug = slug.replace(/-hv$|-mv$/, '');
// Subdirectories to search in
const subdirs = ['', 'low-voltage', 'medium-voltage', 'high-voltage', 'solar'];
// List of patterns to try for the current locale
const patterns = [
`${slug}-${locale}.pdf`,
@@ -25,10 +28,13 @@ export function getDatasheetPath(slug: string, locale: string): string | null {
`${normalizedSlug}-3-${locale}.pdf`,
];
for (const pattern of patterns) {
const filePath = path.join(datasheetsDir, pattern);
if (fs.existsSync(filePath)) {
return `/datasheets/${pattern}`;
for (const subdir of subdirs) {
for (const pattern of patterns) {
const relativePath = path.join(subdir, pattern);
const filePath = path.join(datasheetsDir, relativePath);
if (fs.existsSync(filePath)) {
return `/datasheets/${relativePath}`;
}
}
}
@@ -42,10 +48,13 @@ export function getDatasheetPath(slug: string, locale: string): string | null {
`${normalizedSlug}-2-en.pdf`,
`${normalizedSlug}-3-en.pdf`,
];
for (const pattern of enPatterns) {
const filePath = path.join(datasheetsDir, pattern);
if (fs.existsSync(filePath)) {
return `/datasheets/${pattern}`;
for (const subdir of subdirs) {
for (const pattern of enPatterns) {
const relativePath = path.join(subdir, pattern);
const filePath = path.join(datasheetsDir, relativePath);
if (fs.existsSync(filePath)) {
return `/datasheets/${relativePath}`;
}
}
}
}

View File

@@ -19,10 +19,6 @@ export const envSchema = z.object({
// Error Tracking
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
// Cache
REDIS_URL: z.preprocess(preprocessEmptyString, z.string().optional()),
REDIS_KEY_PREFIX: z.preprocess(preprocessEmptyString, z.string().default('klz:')),
// Logging
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
@@ -51,8 +47,6 @@ export function getRawEnv() {
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
SENTRY_DSN: process.env.SENTRY_DSN,
REDIS_URL: process.env.REDIS_URL,
REDIS_KEY_PREFIX: process.env.REDIS_KEY_PREFIX,
LOG_LEVEL: process.env.LOG_LEVEL,
MAIL_HOST: process.env.MAIL_HOST,
MAIL_PORT: process.env.MAIL_PORT,

View File

@@ -31,6 +31,8 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
filePath = path.join(productsDir, `${fileSlug}-2.mdx`);
}
let product: ProductMdx | null = null;
if (!fs.existsSync(filePath)) {
// Fallback to English if locale is not 'en'
if (locale !== 'en') {
@@ -43,7 +45,7 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
if (fs.existsSync(enFilePath)) {
const fileContent = fs.readFileSync(enFilePath, 'utf8');
const { data, content } = matter(fileContent);
return {
product = {
slug: fileSlug,
frontmatter: {
...data,
@@ -53,17 +55,23 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
};
}
}
} else {
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
product = {
slug: fileSlug,
frontmatter: data as ProductFrontmatter,
content,
};
}
// Filter out products without images
if (product && (!product.frontmatter.images || product.frontmatter.images.length === 0 || !product.frontmatter.images[0])) {
return null;
}
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
return {
slug: fileSlug,
frontmatter: data as ProductFrontmatter,
content,
};
return product;
}
export async function getAllProductSlugs(locale: string): Promise<string[]> {

25
lib/metadata.ts Normal file
View File

@@ -0,0 +1,25 @@
import { Metadata } from 'next';
import { SITE_URL } from './schema';
export function getOGImageMetadata(path: string, title: string, locale: string): NonNullable<Metadata['openGraph']>['images'] {
const cleanPath = path ? (path.startsWith('/') ? path : `/${path}`) : '';
return [
{
url: `${SITE_URL}/${locale}${cleanPath}/opengraph-image`,
width: 1200,
height: 630,
alt: title,
},
];
}
export function getProductOGImageMetadata(slug: string, title: string, locale: string): NonNullable<Metadata['openGraph']>['images'] {
return [
{
url: `${SITE_URL}/${locale}/api/og/product?slug=${slug}`,
width: 1200,
height: 630,
alt: title,
},
];
}

View File

@@ -21,234 +21,149 @@ Font.register({
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
const styles = StyleSheet.create({
page: {
// Large margins for engineering documentation feel.
// Extra bottom padding reserves space for the fixed footer so content
// (esp. long descriptions) doesn't render underneath it.
paddingTop: 72,
paddingLeft: 72,
paddingRight: 72,
paddingBottom: 140,
color: '#111827', // Text Primary
lineHeight: 1.5,
backgroundColor: '#FFFFFF',
paddingTop: 0,
paddingBottom: 100,
fontFamily: 'Helvetica',
fontSize: 10,
color: '#1F2933', // Dark gray text
lineHeight: 1.5, // Generous line height
backgroundColor: '#F8F9FA', // Almost white background
},
// Engineering documentation header
// Hero-style header
hero: {
backgroundColor: '#FFFFFF',
paddingTop: 24,
paddingBottom: 0,
paddingHorizontal: 72,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
borderBottomColor: '#e5e7eb',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 48, // Large spacing
paddingBottom: 24,
borderBottom: '2px solid #E6E9ED', // Light gray separator
},
// Logo area - industrial style
logoArea: {
flexDirection: 'column',
alignItems: 'flex-start',
},
// Optional image logo container (keeps header height stable)
logoContainer: {
width: 120,
height: 32,
justifyContent: 'center',
},
// Image logo (preferred when available)
logo: {
width: 110,
height: 28,
objectFit: 'contain',
alignItems: 'center',
marginBottom: 16,
},
logoText: {
fontSize: 20,
fontSize: 24,
fontWeight: 700,
color: '#0E2A47', // Dark navy
color: '#000d26',
letterSpacing: 1,
textTransform: 'uppercase',
},
logoSubtext: {
fontSize: 10,
fontWeight: 400,
color: '#6B7280', // Medium gray
letterSpacing: 0.5,
marginTop: 2,
},
// Document info - technical style
docInfo: {
textAlign: 'right',
alignItems: 'flex-end',
},
docTitle: {
fontSize: 16,
fontSize: 10,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 8,
letterSpacing: 0.5,
color: '#001a4d',
letterSpacing: 2,
textTransform: 'uppercase',
},
skuContainer: {
backgroundColor: '#E6E9ED', // Light gray background
paddingHorizontal: 16,
paddingVertical: 8,
border: '1px solid #E6E9ED',
productRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
productInfoCol: {
flex: 1,
justifyContent: 'center',
},
productImageCol: {
flex: 1,
height: 120,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
borderWidth: 1,
borderColor: '#e5e7eb',
backgroundColor: '#FFFFFF',
overflow: 'hidden',
},
skuLabel: {
fontSize: 8,
color: '#6B7280', // Medium gray
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 4,
},
skuValue: {
fontSize: 14,
fontWeight: 700,
color: '#0E2A47', // Dark navy
},
// Product section - technical specification style
productSection: {
marginBottom: 40,
backgroundColor: '#FFFFFF', // White background for content blocks
padding: 24,
border: '1px solid #E6E9ED',
// Product Hero Info
productHero: {
marginTop: 0,
},
productName: {
fontSize: 24,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 12,
lineHeight: 1.2,
color: '#000d26',
marginBottom: 0,
textTransform: 'uppercase',
letterSpacing: 0.5,
letterSpacing: -0.5,
},
productMeta: {
fontSize: 12,
color: '#6B7280', // Medium gray
fontWeight: 500,
fontSize: 10,
color: '#4b5563',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 1,
},
// Content sections - rectangular blocks
heroImage: {
width: '100%',
height: '100%',
objectFit: 'contain',
},
noImage: {
fontSize: 8,
color: '#9ca3af',
textAlign: 'center',
},
// Content Area
content: {
paddingHorizontal: 72,
},
// Content sections
section: {
marginBottom: 32,
backgroundColor: '#FFFFFF',
padding: 24,
border: '1px solid #E6E9ED',
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 16,
letterSpacing: 0.5,
color: '#000d26', // Primary Dark
marginBottom: 8,
textTransform: 'uppercase',
borderBottom: '1px solid #E6E9ED',
paddingBottom: 8,
letterSpacing: -0.2,
},
sectionAccent: {
width: 30,
height: 3,
backgroundColor: '#82ed20', // Accent Green
marginBottom: 8,
borderRadius: 1.5,
},
// Description - technical documentation style
description: {
fontSize: 10,
lineHeight: 1.6,
color: '#1F2933', // Dark gray text
marginBottom: 0,
fontSize: 11,
lineHeight: 1.7,
color: '#4b5563', // Text Secondary
},
// Cross-section table - engineering specification style
table: {
marginTop: 16,
borderWidth: 1,
borderColor: '#E6E9ED',
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#E6E9ED',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableHeaderCell: {
flex: 1,
padding: 8,
fontSize: 10,
fontWeight: 700,
color: '#0E2A47',
textTransform: 'uppercase',
letterSpacing: 0.3,
},
tableHeaderCellLast: {
borderRightWidth: 0,
},
tableHeaderCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableCell: {
flex: 1,
padding: 8,
fontSize: 10,
color: '#1F2933',
},
tableCellLast: {
borderRightWidth: 0,
},
tableCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRowAlt: {
backgroundColor: '#F8F9FA',
},
// Specifications - technical data style
specsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
// Backwards-compatible alias used by the component markup
specsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
// Technical data table (used for the metagrid)
// Technical data table
specsTable: {
borderWidth: 1,
borderColor: '#E6E9ED',
marginTop: 8,
border: '1px solid #e5e7eb',
borderRadius: 8,
overflow: 'hidden',
},
specsTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
borderBottomColor: '#e5e7eb',
},
specsTableRowLast: {
@@ -256,63 +171,35 @@ const styles = StyleSheet.create({
},
specsTableLabelCell: {
flex: 3,
paddingVertical: 8,
paddingHorizontal: 8,
backgroundColor: '#F8F9FA',
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
backgroundColor: '#f8f9fa',
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
justifyContent: 'center',
borderRightColor: '#e5e7eb',
},
specsTableValueCell: {
flex: 4,
paddingVertical: 8,
paddingHorizontal: 8,
justifyContent: 'center',
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
},
specsTableLabelText: {
fontSize: 9,
fontWeight: 700,
color: '#0E2A47',
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 0.3,
lineHeight: 1.2,
letterSpacing: 0.5,
},
specsTableValueText: {
fontSize: 10,
color: '#1F2933',
lineHeight: 1.4,
color: '#111827',
fontWeight: 500,
},
specColumn: {
width: '48%',
marginRight: '4%',
marginBottom: 16,
},
specItem: {
marginBottom: 12,
},
specLabel: {
fontSize: 9,
fontWeight: 700,
color: '#0E2A47',
marginBottom: 4,
textTransform: 'uppercase',
letterSpacing: 0.3,
},
specValue: {
fontSize: 10,
color: '#1F2933',
lineHeight: 1.4,
},
// Categories - technical classification
// Categories
categories: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -320,42 +207,48 @@ const styles = StyleSheet.create({
},
categoryTag: {
backgroundColor: '#E6E9ED',
backgroundColor: '#f8f9fa',
paddingHorizontal: 12,
paddingVertical: 6,
border: '1px solid #E6E9ED',
border: '1px solid #e5e7eb',
borderRadius: 100,
},
categoryText: {
fontSize: 9,
color: '#6B7280',
fontWeight: 500,
fontSize: 8,
color: '#4b5563',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 0.3,
letterSpacing: 0.5,
},
// Engineering documentation footer
// Footer
footer: {
position: 'absolute',
bottom: 48,
bottom: 40,
left: 72,
right: 72,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 24,
borderTop: '2px solid #E6E9ED',
fontSize: 9,
color: '#6B7280',
borderTop: '1px solid #e5e7eb',
},
footerLeft: {
footerText: {
fontSize: 8,
color: '#9ca3af',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 1,
},
footerBrand: {
fontSize: 10,
fontWeight: 700,
color: '#0E2A47',
},
footerRight: {
color: '#6B7280',
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 1,
},
});
@@ -364,6 +257,7 @@ interface ProductData {
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
applicationHtml?: string;
images: string[];
featuredImage: string | null;
sku: string;
@@ -389,19 +283,19 @@ const stripHtml = (html: string): string => {
const getLabels = (locale: 'en' | 'de') => {
const labels = {
en: {
productDatasheet: 'Product Datasheet',
description: 'Description',
specifications: 'Technical Specifications',
categories: 'Categories',
productDatasheet: 'Technical Datasheet',
description: 'APPLICATION',
specifications: 'TECHNICAL DATA',
categories: 'CATEGORIES',
sku: 'SKU',
noImage: 'No image available',
},
de: {
productDatasheet: 'Produktdatenblatt',
description: 'Beschreibung',
specifications: 'Technische Spezifikationen',
categories: 'Kategorien',
sku: 'Artikelnummer',
productDatasheet: 'Technisches Datenblatt',
description: 'ANWENDUNG',
specifications: 'TECHNISCHE DATEN',
categories: 'KATEGORIEN',
sku: 'ARTIKELNUMMER',
noImage: 'Kein Bild verfügbar',
},
};
@@ -418,99 +312,101 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Clean, minimal header */}
<View style={styles.header}>
<View style={styles.logoArea}>
<View style={styles.logoContainer}>
{logoUrl ? (
/* eslint-disable-next-line jsx-a11y/alt-text */
<Image src={logoUrl} style={styles.logo} />
) : (
<View>
<Text style={styles.logoText}>KLZ</Text>
<Text style={styles.logoSubtext}>Cables</Text>
{/* Hero Header */}
<View style={styles.hero}>
<View style={styles.header}>
<View>
<Text style={styles.logoText}>KLZ</Text>
</View>
<Text style={styles.docTitle}>
{labels.productDatasheet}
</Text>
</View>
<View style={styles.productRow}>
<View style={styles.productInfoCol}>
<View style={styles.productHero}>
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<Text key={index} style={styles.productMeta}>
{cat.name}{index < product.categories.length - 1 ? ' • ' : ''}
</Text>
))}
</View>
<Text style={styles.productName}>{product.name}</Text>
</View>
</View>
<View style={styles.productImageCol}>
{product.featuredImage ? (
<Image src={product.featuredImage} style={styles.heroImage} />
) : (
<Text style={styles.noImage}>{labels.noImage}</Text>
)}
</View>
</View>
<View style={styles.docInfo}>
<Text style={styles.docTitle}>
{locale === 'en' ? 'Product Datasheet' : 'Produktdatenblatt'}
</Text>
<View style={styles.skuContainer}>
<Text style={styles.skuLabel}>{labels.sku}</Text>
<Text style={styles.skuValue}>{product.sku}</Text>
</View>
</View>
</View>
{/* Product section - clean and prominent */}
<View style={styles.productSection}>
<Text style={styles.productName}>{product.name}</Text>
<Text style={styles.productMeta}>
{product.categories.map(cat => cat.name).join(' • ')}
</Text>
<View style={styles.content}>
{/* Description section */}
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.description}</Text>
<View style={styles.sectionAccent} />
<Text style={styles.description}>
{stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml)}
</Text>
</View>
)}
{/* Technical specifications */}
{product.attributes && product.attributes.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
<View style={styles.sectionAccent} />
<View style={styles.specsTable}>
{product.attributes.map((attr, index) => (
<View
key={index}
style={[
styles.specsTableRow,
index === product.attributes.length - 1 &&
styles.specsTableRowLast,
]}
>
<View style={styles.specsTableLabelCell}>
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
</View>
<View style={styles.specsTableValueCell}>
<Text style={styles.specsTableValueText}>
{attr.options.join(', ')}
</Text>
</View>
</View>
))}
</View>
</View>
)}
{/* Categories as clean tags */}
{product.categories && product.categories.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.categories}</Text>
<View style={styles.sectionAccent} />
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<View key={index} style={styles.categoryTag}>
<Text style={styles.categoryText}>{cat.name}</Text>
</View>
))}
</View>
</View>
)}
</View>
{/* Description section */}
{(product.shortDescriptionHtml || product.descriptionHtml) && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.description}</Text>
<Text style={styles.description}>
{stripHtml(product.shortDescriptionHtml || product.descriptionHtml)}
</Text>
</View>
)}
{/* Technical specifications */}
{product.attributes && product.attributes.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
<View style={styles.specsTable}>
{product.attributes.map((attr, index) => (
<View
key={index}
style={[
styles.specsTableRow,
index === product.attributes.length - 1 &&
styles.specsTableRowLast,
]}
>
<View style={styles.specsTableLabelCell}>
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
</View>
<View style={styles.specsTableValueCell}>
<Text style={styles.specsTableValueText}>
{attr.options.join(', ')}
</Text>
</View>
</View>
))}
</View>
</View>
)}
{/* Categories as clean tags */}
{product.categories && product.categories.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.categories}</Text>
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<View key={index} style={styles.categoryTag}>
<Text style={styles.categoryText}>{cat.name}</Text>
</View>
))}
</View>
</View>
)}
{/* Minimal footer */}
<View style={styles.footer} fixed>
<Text style={styles.footerLeft}>
{labels.sku}: {product.sku}
</Text>
<Text style={styles.footerRight}>
<Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text style={styles.footerText}>
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric',
month: 'long',

View File

@@ -1,54 +0,0 @@
import { createClient, type RedisClientType } from 'redis';
import type { CacheService, CacheSetOptions } from './cache-service';
export type RedisCacheServiceOptions = {
url: string;
keyPrefix?: string;
};
// Thin wrapper around shared Redis (platform provides host `redis`).
// Values are JSON-serialized.
export class RedisCacheService implements CacheService {
private readonly client: RedisClientType;
private readonly keyPrefix: string;
constructor(options: RedisCacheServiceOptions) {
this.client = createClient({ url: options.url });
this.keyPrefix = options.keyPrefix ?? '';
// Fire-and-forget connect.
this.client.connect().catch((err) => {
// We can't use getServerAppServices() here because it might cause a circular dependency
// during initialization. But we can log to console as a fallback or use a global logger if we had one.
// For now, let's just use console.error as this is a low-level service.
console.error('Redis connection error:', err);
});
}
private k(key: string) {
return `${this.keyPrefix}${key}`;
}
async get<T>(key: string): Promise<T | undefined> {
const raw = await this.client.get(this.k(key));
if (raw == null) return undefined;
return JSON.parse(raw) as T;
}
async set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void> {
const ttl = options?.ttlSeconds;
const raw = JSON.stringify(value);
if (ttl && ttl > 0) {
await this.client.set(this.k(key), raw, { EX: ttl });
return;
}
await this.client.set(this.k(key), raw);
}
async del(key: string): Promise<void> {
await this.client.del(this.k(key));
}
}

View File

@@ -2,7 +2,6 @@ import { AppServices } from './app-services';
import { NoopAnalyticsService } from './analytics/noop-analytics-service';
import { UmamiAnalyticsService } from './analytics/umami-analytics-service';
import { MemoryCacheService } from './cache/memory-cache-service';
import { RedisCacheService } from './cache/redis-cache-service';
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
import { PinoLoggerService } from './logging/pino-logger-service';
@@ -23,7 +22,6 @@ export function getServerAppServices(): AppServices {
logger.info('Service configuration', {
umamiEnabled: config.analytics.umami.enabled,
sentryEnabled: config.errors.glitchtip.enabled,
redisEnabled: config.cache.redis.enabled,
mailEnabled: Boolean(config.mail.host && config.mail.user),
});
@@ -47,20 +45,8 @@ export function getServerAppServices(): AppServices {
logger.info('Noop error reporting service initialized (error reporting disabled)');
}
const cache = config.cache.redis.enabled && config.cache.redis.url
? new RedisCacheService({
url: config.cache.redis.url,
keyPrefix: config.cache.redis.keyPrefix,
})
: new MemoryCacheService();
if (config.cache.redis.enabled) {
logger.info('Redis cache service initialized', {
keyPrefix: config.cache.redis.keyPrefix
});
} else {
logger.info('Memory cache service initialized (Redis not configured)');
}
const cache = new MemoryCacheService();
logger.info('Memory cache service initialized');
logger.info('Pino logger service initialized', {
name: 'server',

View File

@@ -1,7 +1,6 @@
import createMiddleware from 'next-intl/middleware';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getServerAppServices } from '@/lib/services/create-services.server';
// Create the internationalization middleware
const intlMiddleware = createMiddleware({
@@ -20,40 +19,20 @@ export default function middleware(request: NextRequest) {
const referer = headers.get('referer') || 'none';
const ip = headers.get('x-forwarded-for') || headers.get('x-real-ip') || 'unknown';
// Get logger service
const services = getServerAppServices();
const logger = services.logger.child({ middleware: 'request-logger' });
// Log incoming request
logger.info(`Incoming request: method=${method} url=${url} ip=${ip} ua="${userAgent.slice(0,60)}${userAgent.length>60 ? '...' : ''}" ref="${referer.slice(0,80)}${referer.length>80 ? '...' : ''}"`);
console.log(`Incoming request: method=${method} url=${url}`);
try {
// Apply internationalization middleware
const response = intlMiddleware(request);
const duration = Date.now() - startTime;
// Log successful response
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location') || 'unknown';
logger.info(`Redirect detected: status=${response.status} location="${location.slice(0,100)}${location.length>100?'...':''}" method=${method} url=${url} duration=${duration}ms ip=${ip} ua="${userAgent.slice(0,60)}${userAgent.length>60?'...':''}" ref="${referer.slice(0,80)}${referer.length>80?'...':''}"`);
} else {
logger.info(`Request completed: status=${response.status} method=${method} url=${url} duration=${duration}ms ip=${ip} ua="${userAgent.slice(0,60)}${userAgent.length>60?'...':''}" ref="${referer.slice(0,80)}${referer.length>80?'...':''}"`);
}
return response;
} catch (error) {
const duration = Date.now() - startTime;
// Log error
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error(`Request failed: method=${method} url=${url} duration=${duration}ms error="${errorMsg}" ip=${ip} ua="${userAgent.slice(0,60)}${userAgent.length>60?'...':''}" ref="${referer.slice(0,80)}${referer.length>80?'...':''}"`);
// Re-throw the error to let Next.js handle it
console.error(`Request failed: method=${method} url=${url}`, error);
throw error;
}
}
export const config = {
// Match only internationalized pathnames
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)', '/', '/(de|en)/:path*']
matcher: ['/((?!api|_next|_vercel|health|.*\\..*).*)', '/', '/(de|en)/:path*']
};

101
package-lock.json generated
View File

@@ -35,7 +35,6 @@
"react-dom": "^18.3.1",
"react-email": "^5.2.5",
"react-leaflet": "^4.2.1",
"redis": "^4.7.1",
"resend": "^3.5.0",
"schema-dts": "^1.1.5",
"sharp": "^0.34.5",
@@ -4459,71 +4458,6 @@
"@react-pdf/stylesheet": "^6.1.2"
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/client/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/@redis/graph": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz",
"integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz",
"integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz",
"integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz",
"integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz",
@@ -8721,15 +8655,6 @@
"node": ">=6"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
@@ -10874,15 +10799,6 @@
"node": ">= 0.4"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -15991,23 +15907,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/redis": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz",
"integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==",
"license": "MIT",
"workspaces": [
"./packages/*"
],
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.6.1",
"@redis/graph": "1.1.1",
"@redis/json": "1.0.7",
"@redis/search": "1.2.0",
"@redis/time-series": "1.1.0"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",

View File

@@ -27,7 +27,6 @@
"react-dom": "^18.3.1",
"react-email": "^5.2.5",
"react-leaflet": "^4.2.1",
"redis": "^4.7.1",
"resend": "^3.5.0",
"schema-dts": "^1.1.5",
"sharp": "^0.34.5",
@@ -64,6 +63,7 @@
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test:og": "vitest run tests/og-image.test.ts",
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts"
},

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More