Compare commits
16 Commits
15cfb314b1
...
v1.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| 506c8682fe | |||
| a909de30f3 | |||
| a2f94f15bc | |||
| 13e56a88bc | |||
| bb7d17001b | |||
| 920efa0083 | |||
| 0b81d1a4cb | |||
| 1d5bdeba26 | |||
| a0c3fbbc7e | |||
| 8101a9f156 | |||
| 7b6f4b5ea4 | |||
| 658057cdb1 | |||
| 2aa5d5b00e | |||
| 7f2f6f5aca | |||
| 4e50482769 | |||
| 1da1f05cdd |
4
.env
4
.env
@@ -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
|
||||
|
||||
@@ -35,13 +35,6 @@ 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
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,22 +14,9 @@ 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 " • 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
|
||||
@@ -39,47 +26,15 @@ jobs:
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
- 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 ""
|
||||
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
|
||||
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)..."
|
||||
docker buildx build \
|
||||
--pull \
|
||||
--platform linux/arm64 \
|
||||
@@ -88,57 +43,21 @@ jobs:
|
||||
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}" \
|
||||
-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
|
||||
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 ""
|
||||
echo "🚀 Deploying to alpha.mintel.me..."
|
||||
|
||||
# Setup SSH with logging
|
||||
echo "🔐 Setting up SSH connection..."
|
||||
# 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
|
||||
@@ -166,112 +85,42 @@ jobs:
|
||||
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
|
||||
EOF
|
||||
|
||||
echo "✅ Environment file prepared"
|
||||
echo ""
|
||||
# Upload .env and deploy
|
||||
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env
|
||||
|
||||
# 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"
|
||||
|
||||
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 "🔄 Stopping existing containers..."
|
||||
docker-compose down
|
||||
|
||||
echo "🚀 Starting new containers..."
|
||||
echo "🚀 Starting containers..."
|
||||
docker-compose up -d
|
||||
|
||||
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 ps
|
||||
|
||||
if ! docker-compose ps | grep -q "Up"; then
|
||||
echo "❌ Container failed to start"
|
||||
docker-compose 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
|
||||
@@ -279,33 +128,8 @@ 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"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# NOTIFICATION: Gotify
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -266,7 +261,7 @@ bash scripts/deploy-webhook.sh
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Client → Traefik (TLS) → Varnish (Cache) → Next.js App
|
||||
Client → Traefik (TLS) → Next.js App
|
||||
```
|
||||
|
||||
**Domains**:
|
||||
@@ -276,7 +271,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).
|
||||
|
||||
@@ -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>
|
||||
|
||||
74
app/[locale]/api/og/product/route.tsx
Normal file
74
app/[locale]/api/og/product/route.tsx
Normal 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}
|
||||
/>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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, {
|
||||
|
||||
25
app/[locale]/blog/opengraph-image.tsx
Normal file
25
app/[locale]/blog/opengraph-image.tsx
Normal 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,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -5,12 +5,13 @@ 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 { 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 +52,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 +81,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 +167,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 +312,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>
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -46,6 +46,8 @@ export function OGImageTemplate({
|
||||
<img
|
||||
src={image}
|
||||
alt=""
|
||||
width="1200"
|
||||
height="630"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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]',
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 50 Hz 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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 630 mm² lässt sich das Kabel flexibel an die jeweilige Anwendung
|
||||
anpassen.
|
||||
locale: de
|
||||
---
|
||||
<ProductTabs technicalData={<ProductTechnicalData data={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 1 kV 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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -1,63 +1,32 @@
|
||||
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
|
||||
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"
|
||||
# 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/`)"
|
||||
- "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`)"
|
||||
- "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"
|
||||
# Forwarded Headers (für Apps, die HTTPS erwarten)
|
||||
- "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
|
||||
- "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:
|
||||
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
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# HTTP ⇒ HTTPS redirect
|
||||
- "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"
|
||||
# HTTPS router
|
||||
- "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"
|
||||
# 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
|
||||
- "traefik.http.routers.klz-cables.middlewares=klz-forward,compress"
|
||||
|
||||
networks:
|
||||
infra:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
26
lib/mdx.ts
26
lib/mdx.ts
@@ -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[]> {
|
||||
|
||||
24
lib/metadata.ts
Normal file
24
lib/metadata.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Metadata } from 'next';
|
||||
import { SITE_URL } from './schema';
|
||||
|
||||
export function getOGImageMetadata(path: string, title: string, locale: string): NonNullable<Metadata['openGraph']>['images'] {
|
||||
return [
|
||||
{
|
||||
url: `${SITE_URL}/${locale}/${path}/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,
|
||||
},
|
||||
];
|
||||
}
|
||||
54
lib/services/cache/redis-cache-service.ts
vendored
54
lib/services/cache/redis-cache-service.ts
vendored
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
101
package-lock.json
generated
101
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -159,26 +159,112 @@ function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
|
||||
}
|
||||
|
||||
function technicalFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
|
||||
if (args.locale === 'en') return normalizeValue(args.excelKey);
|
||||
const raw = normalizeValue(args.excelKey);
|
||||
if (!raw) return '';
|
||||
|
||||
if (args.locale === 'de') {
|
||||
return raw
|
||||
.replace(/\(approx\.?\)/gi, '(ca.)')
|
||||
.replace(/\bconductor material\b/gi, 'Leitermaterial')
|
||||
.replace(/\bconductor class\b/gi, 'Leiterklasse')
|
||||
.replace(/\bcore insulation\b/gi, 'Aderisolation')
|
||||
.replace(/\binsulation\b/gi, 'Aderisolation')
|
||||
.replace(/\bfield control\b/gi, 'Feldsteuerung')
|
||||
.replace(/\bscreen\b/gi, 'Schirm')
|
||||
.replace(/\blongitudinal water tightness\b/gi, 'Längswasserdichtigkeit')
|
||||
.replace(/\btransverse water tightness\b/gi, 'Querwasserdichtigkeit')
|
||||
.replace(/\bsheath material\b/gi, 'Mantelmaterial')
|
||||
.replace(/\bsheath color\b/gi, 'Mantelfarbe')
|
||||
.replace(/\bflame retardancy\b/gi, 'Flammwidrigkeit')
|
||||
.replace(/\buv resistant\b/gi, 'UV-bestandig')
|
||||
.replace(/\bmax\.? permissible conductor temperature\b/gi, 'Max. zulässige Leitertemperatur')
|
||||
.replace(/\bpermissible cable outer temperature, fixed\b/gi, 'Zul. Kabelaußentemperatur, fest verlegt')
|
||||
.replace(/\bpermissible cable outer temperature, in motion\b/gi, 'Zul. Kabelaußentemperatur, in Bewegung')
|
||||
.replace(/\bmaximum short-circuit temperature\b/gi, 'Maximale Kurzschlußtemperatur')
|
||||
.replace(/\bmin\.? bending radius, fixed\b/gi, 'Min. Biegeradius, fest verlegt')
|
||||
.replace(/\bminimum laying temperature\b/gi, 'Mindesttemperatur Verlegung')
|
||||
.replace(/\bmeter marking\b/gi, 'Metermarkierung')
|
||||
.replace(/\bpartial discharge\b/gi, 'Teilentladung')
|
||||
.replace(/\bcapacitance\b/gi, 'Kapazität')
|
||||
.replace(/\binductance\b/gi, 'Induktivität')
|
||||
.replace(/\breactance\b/gi, 'Reaktanz')
|
||||
.replace(/\btest voltage\b/gi, 'Prüfspannung')
|
||||
.replace(/\brated voltage\b/gi, 'Nennspannung')
|
||||
.replace(/\boperating temperature range\b/gi, 'Temperaturbereich')
|
||||
.replace(/\bminimum sheath thickness\b/gi, 'Manteldicke (min.)')
|
||||
.replace(/\bsheath thickness\b/gi, 'Manteldicke')
|
||||
.replace(/\bnominal insulation thickness\b/gi, 'Isolationsdicke (nom.)')
|
||||
.replace(/\binsulation thickness\b/gi, 'Isolationsdicke')
|
||||
.replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC-Leiterwiderstand (20 °C)')
|
||||
.replace(/\bouter diameter(?: of cable)?\b/gi, 'Außen-Ø')
|
||||
.replace(/\bbending radius\b/gi, 'Biegeradius')
|
||||
.replace(/\bpackaging\b/gi, 'Verpackung')
|
||||
.replace(/\bce\s*-?conformity\b/gi, 'CE-Konformität');
|
||||
}
|
||||
|
||||
return raw
|
||||
.replace(/\(approx\.?\)/gi, '(ca.)')
|
||||
.replace(/\bcapacitance\b/gi, 'Kapazität')
|
||||
.replace(/\binductance\b/gi, 'Induktivität')
|
||||
.replace(/\breactance\b/gi, 'Reaktanz')
|
||||
.replace(/\btest voltage\b/gi, 'Prüfspannung')
|
||||
.replace(/\brated voltage\b/gi, 'Nennspannung')
|
||||
.replace(/\boperating temperature range\b/gi, 'Temperaturbereich')
|
||||
.replace(/\bminimum sheath thickness\b/gi, 'Manteldicke (min.)')
|
||||
.replace(/\bsheath thickness\b/gi, 'Manteldicke')
|
||||
.replace(/\bnominal insulation thickness\b/gi, 'Isolationsdicke (nom.)')
|
||||
.replace(/\binsulation thickness\b/gi, 'Isolationsdicke')
|
||||
.replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC-Leiterwiderstand (20 °C)')
|
||||
.replace(/\bouter diameter(?: of cable)?\b/gi, 'Außen-Ø')
|
||||
.replace(/\bbending radius\b/gi, 'Biegeradius')
|
||||
.replace(/\bpackaging\b/gi, 'Verpackung')
|
||||
.replace(/\bce\s*-?conformity\b/gi, 'CE-Konformität');
|
||||
.replace(/\bconductor material\b/gi, 'Conductor material')
|
||||
.replace(/\bconductor class\b/gi, 'Conductor class')
|
||||
.replace(/\bcore insulation\b/gi, 'Core insulation')
|
||||
.replace(/\binsulation\b/gi, 'Core insulation')
|
||||
.replace(/\bfield control\b/gi, 'Field control')
|
||||
.replace(/\bscreen\b/gi, 'Screen')
|
||||
.replace(/\blongitudinal water tightness\b/gi, 'Longitudinal water tightness')
|
||||
.replace(/\btransverse water tightness\b/gi, 'Transverse water tightness')
|
||||
.replace(/\bsheath material\b/gi, 'Sheath material')
|
||||
.replace(/\bsheath color\b/gi, 'Sheath color')
|
||||
.replace(/\bflame retardancy\b/gi, 'Flame retardancy')
|
||||
.replace(/\buv resistant\b/gi, 'UV resistant')
|
||||
.replace(/\bmax\.? permissible conductor temperature\b/gi, 'Max. permissible conductor temperature')
|
||||
.replace(/\bpermissible cable outer temperature, fixed\b/gi, 'Permissible cable outer temperature, fixed')
|
||||
.replace(/\bpermissible cable outer temperature, in motion\b/gi, 'Permissible cable outer temperature, in motion')
|
||||
.replace(/\bmaximum short-circuit temperature\b/gi, 'Maximum short-circuit temperature')
|
||||
.replace(/\bmin\.? bending radius, fixed\b/gi, 'Min. bending radius, fixed')
|
||||
.replace(/\bminimum laying temperature\b/gi, 'Minimum laying temperature')
|
||||
.replace(/\bmeter marking\b/gi, 'Meter marking')
|
||||
.replace(/\bpartial discharge\b/gi, 'Partial discharge')
|
||||
.replace(/\bcapacitance\b/gi, 'Capacitance')
|
||||
.replace(/\binductance\b/gi, 'Inductance')
|
||||
.replace(/\breactance\b/gi, 'Reactance')
|
||||
.replace(/\btest voltage\b/gi, 'Test voltage')
|
||||
.replace(/\brated voltage\b/gi, 'Rated voltage')
|
||||
.replace(/\boperating temperature range\b/gi, 'Operating temperature range')
|
||||
.replace(/\bminimum sheath thickness\b/gi, 'Sheath thickness (min.)')
|
||||
.replace(/\bsheath thickness\b/gi, 'Sheath thickness')
|
||||
.replace(/\bnominal insulation thickness\b/gi, 'Insulation thickness (nom.)')
|
||||
.replace(/\binsulation thickness\b/gi, 'Insulation thickness')
|
||||
.replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC resistance (20 °C)')
|
||||
.replace(/\bouter diameter(?: of cable)?\b/gi, 'Outer diameter')
|
||||
.replace(/\bbending radius\b/gi, 'Bending radius')
|
||||
.replace(/\bpackaging\b/gi, 'Packaging')
|
||||
.replace(/\bce\s*-?conformity\b/gi, 'CE conformity');
|
||||
}
|
||||
|
||||
function technicalValueTranslation(args: { label: string; value: string; locale: 'en' | 'de' }): string {
|
||||
const v = normalizeValue(args.value);
|
||||
if (!v) return '';
|
||||
|
||||
if (args.locale === 'de') {
|
||||
if (/^yes$/i.test(v)) return 'ja';
|
||||
if (/^no$/i.test(v)) return 'nein';
|
||||
if (/^copper$/i.test(v)) return 'Kupfer';
|
||||
if (/^aluminum$/i.test(v)) return 'Aluminium';
|
||||
if (/^black$/i.test(v)) return 'schwarz';
|
||||
if (/^stranded$/i.test(v)) return 'mehrdrähtig';
|
||||
if (/^(\d+)xD$/i.test(v)) return v.replace(/^(\d+)xD$/i, '$1 facher Durchmesser');
|
||||
if (/^XLPE/i.test(v)) return v.replace(/^XLPE/i, 'VPE');
|
||||
return v;
|
||||
}
|
||||
|
||||
if (/^ja$/i.test(v)) return 'yes';
|
||||
if (/^nein$/i.test(v)) return 'no';
|
||||
if (/^kupfer$/i.test(v)) return 'Copper';
|
||||
if (/^aluminium$/i.test(v)) return 'Aluminum';
|
||||
if (/^schwarz$/i.test(v)) return 'black';
|
||||
if (/^mehrdrähtig$/i.test(v)) return 'stranded';
|
||||
if (/^(\d+)xD$/i.test(v)) return v.replace(/^(\d+)xD$/i, '$1 times diameter');
|
||||
if (/^VPE/i.test(v)) return v.replace(/^VPE/i, 'XLPE');
|
||||
return v;
|
||||
}
|
||||
|
||||
function metaFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
|
||||
@@ -507,6 +593,26 @@ function buildExcelModel(args: { product: ProductData; locale: 'en' | 'de' }): B
|
||||
'flame retardant': { header: 'Flame retardant', unit: '', key: 'flame' },
|
||||
'self-extinguishing of single cable': { header: 'Flame retardant', unit: '', key: 'flame' },
|
||||
|
||||
'conductor material': { header: 'Conductor material', unit: '', key: 'cond_mat' },
|
||||
'conductor class': { header: 'Conductor class', unit: '', key: 'cond_class' },
|
||||
'core insulation': { header: 'Core insulation', unit: '', key: 'core_ins' },
|
||||
'field control': { header: 'Field control', unit: '', key: 'field_ctrl' },
|
||||
'screen': { header: 'Screen', unit: '', key: 'screen' },
|
||||
'longitudinal water tightness': { header: 'Longitudinal water tightness', unit: '', key: 'long_water' },
|
||||
'transverse water tightness': { header: 'Transverse water tightness', unit: '', key: 'trans_water' },
|
||||
'sheath material': { header: 'Sheath material', unit: '', key: 'sheath_mat' },
|
||||
'sheath color': { header: 'Sheath color', unit: '', key: 'sheath_color' },
|
||||
'flame retardancy': { header: 'Flame retardancy', unit: '', key: 'flame_ret' },
|
||||
'uv resistant': { header: 'UV resistant', unit: '', key: 'uv_res' },
|
||||
'max. permissible conductor temperature': { header: 'Max. permissible conductor temperature', unit: '°C', key: 'max_cond_temp' },
|
||||
'permissible cable outer temperature, fixed': { header: 'Permissible cable outer temperature, fixed', unit: '°C', key: 'out_temp_fixed' },
|
||||
'permissible cable outer temperature, in motion': { header: 'Permissible cable outer temperature, in motion', unit: '°C', key: 'out_temp_motion' },
|
||||
'maximum short-circuit temperature': { header: 'Maximum short-circuit temperature', unit: '°C', key: 'max_sc_temp_val' },
|
||||
'min. bending radius, fixed': { header: 'Min. bending radius, fixed', unit: '', key: 'min_bend_fixed' },
|
||||
'minimum laying temperature': { header: 'Minimum laying temperature', unit: '°C', key: 'min_lay_temp_val' },
|
||||
'meter marking': { header: 'Meter marking', unit: '', key: 'meter_mark' },
|
||||
'partial discharge': { header: 'Partial discharge', unit: 'pC', key: 'partial_dis' },
|
||||
|
||||
// High-value electrical/screen columns
|
||||
'capacitance (approx.)': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
|
||||
'capacitance': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
|
||||
@@ -579,11 +685,66 @@ function buildExcelModel(args: { product: ProductData; locale: 'en' | 'de' }): B
|
||||
const unit = normalizeUnit(units[excelKey] || mapping.unit || '');
|
||||
const labelBase = technicalFullLabel({ key: mapping.key, excelKey, locale: args.locale });
|
||||
const label = formatExcelHeaderLabel(labelBase, unit);
|
||||
const value = compactCellForDenseTable(values[0], unit, args.locale);
|
||||
const rawValue = compactCellForDenseTable(values[0], unit, args.locale);
|
||||
const value = technicalValueTranslation({ label: labelBase, value: rawValue, locale: args.locale });
|
||||
if (!technicalItems.find(t => t.label === label)) technicalItems.push({ label, value, unit });
|
||||
}
|
||||
}
|
||||
technicalItems.sort((a, b) => a.label.localeCompare(b.label));
|
||||
const TECHNICAL_DATA_ORDER_DE = [
|
||||
'Leitermaterial',
|
||||
'Leiterklasse',
|
||||
'Aderisolation',
|
||||
'Feldsteuerung',
|
||||
'Schirm',
|
||||
'Längswasserdichtigkeit',
|
||||
'Querwasserdichtigkeit',
|
||||
'Mantelmaterial',
|
||||
'Mantelfarbe',
|
||||
'Flammwidrigkeit',
|
||||
'UV-bestandig',
|
||||
'Max. zulässige Leitertemperatur',
|
||||
'Zul. Kabelaußentemperatur, fest verlegt',
|
||||
'Zul. Kabelaußentemperatur, in Bewegung',
|
||||
'Maximale Kurzschlußtemperatur',
|
||||
'Min. Biegeradius, fest verlegt',
|
||||
'Mindesttemperatur Verlegung',
|
||||
'Metermarkierung',
|
||||
'Teilentladung',
|
||||
];
|
||||
|
||||
const TECHNICAL_DATA_ORDER_EN = [
|
||||
'Conductor material',
|
||||
'Conductor class',
|
||||
'Core insulation',
|
||||
'Field control',
|
||||
'Screen',
|
||||
'Longitudinal water tightness',
|
||||
'Transverse water tightness',
|
||||
'Sheath material',
|
||||
'Sheath color',
|
||||
'Flame retardancy',
|
||||
'UV resistant',
|
||||
'Max. permissible conductor temperature',
|
||||
'Permissible cable outer temperature, fixed',
|
||||
'Permissible cable outer temperature, in motion',
|
||||
'Maximum short-circuit temperature',
|
||||
'Min. bending radius, fixed',
|
||||
'Minimum laying temperature',
|
||||
'Meter marking',
|
||||
'Partial discharge',
|
||||
];
|
||||
|
||||
const order = args.locale === 'de' ? TECHNICAL_DATA_ORDER_DE : TECHNICAL_DATA_ORDER_EN;
|
||||
|
||||
technicalItems.sort((a, b) => {
|
||||
const indexA = order.findIndex(label => a.label.startsWith(label));
|
||||
const indexB = order.findIndex(label => b.label.startsWith(label));
|
||||
|
||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
||||
if (indexA !== -1) return -1;
|
||||
if (indexB !== -1) return 1;
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
|
||||
const voltageTables: VoltageTableModel[] = [];
|
||||
for (const vKey of voltageKeysSorted) {
|
||||
@@ -892,7 +1053,22 @@ export function buildDatasheetModel(args: { product: ProductData; locale: 'en' |
|
||||
productUrl,
|
||||
},
|
||||
labels,
|
||||
technicalItems: excelModel.ok ? excelModel.technicalItems : [],
|
||||
technicalItems: [
|
||||
...(excelModel.ok ? excelModel.technicalItems : []),
|
||||
...(isMediumVoltageProduct(args.product)
|
||||
? args.locale === 'de'
|
||||
? [
|
||||
{ label: 'Prüfspannung 6/10 kV', value: '21 kV' },
|
||||
{ label: 'Prüfspannung 12/20 kV', value: '42 kV' },
|
||||
{ label: 'Prüfspannung 18/30 kV', value: '63 kV' },
|
||||
]
|
||||
: [
|
||||
{ label: 'Test voltage 6/10 kV', value: '21 kV' },
|
||||
{ label: 'Test voltage 12/20 kV', value: '42 kV' },
|
||||
{ label: 'Test voltage 18/30 kV', value: '63 kV' },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
voltageTables,
|
||||
legendItems: crossSectionModel.legendItems || [],
|
||||
};
|
||||
|
||||
@@ -53,18 +53,18 @@ export function generateFileName(product: ProductData, locale: 'en' | 'de'): str
|
||||
export function getLabels(locale: 'en' | 'de') {
|
||||
return {
|
||||
en: {
|
||||
datasheet: 'PRODUCT DATASHEET',
|
||||
datasheet: 'Technical Datasheet',
|
||||
description: 'DESCRIPTION',
|
||||
technicalData: 'TECHNICAL DATA',
|
||||
crossSection: 'CROSS-SECTION DATA',
|
||||
crossSection: 'Cross-sections/Voltage',
|
||||
sku: 'SKU',
|
||||
noImage: 'No image available',
|
||||
},
|
||||
de: {
|
||||
datasheet: 'PRODUKTDATENBLATT',
|
||||
datasheet: 'Technisches Datenblatt',
|
||||
description: 'BESCHREIBUNG',
|
||||
technicalData: 'TECHNISCHE DATEN',
|
||||
crossSection: 'QUERSCHNITTSDATEN',
|
||||
crossSection: 'Querschnitte/Spannung',
|
||||
sku: 'ARTIKELNUMMER',
|
||||
noImage: 'Kein Bild verfügbar',
|
||||
},
|
||||
|
||||
@@ -6,8 +6,6 @@ Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0,
|
||||
// Ensure 500 errors are always captured
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
// AdjustingreplaysOnErrorSampleRate to 1.0 to capture 100% of errors with replays if enabled
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
@@ -6,7 +6,5 @@ Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0,
|
||||
// Ensure 500 errors are always captured
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
|
||||
@@ -6,9 +6,5 @@ Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0,
|
||||
// Ensure 500 errors are always captured
|
||||
// Next.js 14+ with App Router handles many errors automatically,
|
||||
// but we want to be explicit about capturing all unhandled exceptions.
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
|
||||
@@ -51,12 +51,12 @@
|
||||
to { transform: scale(1.1); }
|
||||
}
|
||||
@keyframes reveal {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from { opacity: 0; transform: translateY(20px); filter: blur(8px); }
|
||||
to { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
}
|
||||
@keyframes slight-fade-in-from-bottom {
|
||||
from { opacity: 0; transform: translateY(15px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from { opacity: 0; transform: translateY(10px); filter: blur(4px); }
|
||||
to { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,16 +166,16 @@
|
||||
|
||||
@utility reveal-on-scroll {
|
||||
opacity: 0;
|
||||
transform: translateY(40px) scale(0.95) rotate(-2deg);
|
||||
filter: blur(12px);
|
||||
transform: translateY(20px);
|
||||
filter: blur(8px);
|
||||
transition:
|
||||
opacity 1s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||
transform 1s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||
filter 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
opacity 0.6s ease-out,
|
||||
transform 0.8s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
filter 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
|
||||
&.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1) rotate(0deg);
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
|
||||
52
tests/og-image.test.ts
Normal file
52
tests/og-image.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
|
||||
|
||||
describe('OG Image Generation', () => {
|
||||
const locales = ['de', 'en'];
|
||||
const productSlugs = ['nay2y']; // Based on data/products/de/nay2y.mdx
|
||||
|
||||
async function verifyImageResponse(response: Response) {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('content-type')).toContain('image/png');
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
|
||||
// Check for PNG signature: 89 50 4E 47 0D 0A 1A 0A
|
||||
expect(bytes[0]).toBe(0x89);
|
||||
expect(bytes[1]).toBe(0x50);
|
||||
expect(bytes[2]).toBe(0x4E);
|
||||
expect(bytes[3]).toBe(0x47);
|
||||
|
||||
// Check that the image is not empty and has a reasonable size
|
||||
// A 1200x630 OG image should be at least 4KB
|
||||
expect(bytes.length).toBeGreaterThan(4000);
|
||||
}
|
||||
|
||||
locales.forEach((locale) => {
|
||||
it(`should generate main OG image for ${locale}`, async () => {
|
||||
const url = `${BASE_URL}/${locale}/opengraph-image`;
|
||||
const response = await fetch(url);
|
||||
await verifyImageResponse(response);
|
||||
}, 30000);
|
||||
|
||||
it(`should generate product OG image for ${locale} with slug ${productSlugs[0]}`, async () => {
|
||||
const url = `${BASE_URL}/${locale}/api/og/product?slug=${productSlugs[0]}`;
|
||||
const response = await fetch(url);
|
||||
await verifyImageResponse(response);
|
||||
}, 30000);
|
||||
|
||||
it(`should return 400 for product OG image without slug in ${locale}`, async () => {
|
||||
const url = `${BASE_URL}/${locale}/api/og/product`;
|
||||
const response = await fetch(url);
|
||||
expect(response.status).toBe(400);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
it('should generate blog OG image', async () => {
|
||||
const url = `${BASE_URL}/de/blog/opengraph-image`;
|
||||
const response = await fetch(url);
|
||||
await verifyImageResponse(response);
|
||||
}, 30000);
|
||||
});
|
||||
22
types/redis.d.ts
vendored
22
types/redis.d.ts
vendored
@@ -1,22 +0,0 @@
|
||||
// Fallback ambient types for `redis`.
|
||||
//
|
||||
// The official `redis` package ships its own types. In some editor setups
|
||||
// (especially with newer TS + `moduleResolution: bundler`) the TS server may
|
||||
// temporarily fail to resolve them. This keeps the project compiling.
|
||||
declare module 'redis' {
|
||||
export type RedisClientType = {
|
||||
connect(): Promise<void>;
|
||||
get(key: string): Promise<string | null>;
|
||||
set(
|
||||
key: string,
|
||||
value: string,
|
||||
options?: {
|
||||
EX?: number;
|
||||
}
|
||||
): Promise<unknown>;
|
||||
del(key: string): Promise<number>;
|
||||
};
|
||||
|
||||
export function createClient(options: { url: string }): RedisClientType;
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
vcl 4.1;
|
||||
|
||||
# Minimal, safe Varnish config for a Next.js-style app.
|
||||
# - Cache static assets aggressively
|
||||
# - Avoid caching HTML/auth/api by default
|
||||
# - Preserve websockets / upgrade
|
||||
|
||||
backend default {
|
||||
.host = "app";
|
||||
.port = "3000";
|
||||
}
|
||||
|
||||
sub vcl_recv {
|
||||
# Health endpoint should always work.
|
||||
if (req.url == "/health") {
|
||||
return (pass);
|
||||
}
|
||||
|
||||
# Websocket / Upgrade should not be cached.
|
||||
if (req.http.Upgrade ~ "(?i)websocket") {
|
||||
return (pipe);
|
||||
}
|
||||
|
||||
# Only cache GET/HEAD.
|
||||
if (req.method != "GET" && req.method != "HEAD") {
|
||||
return (pass);
|
||||
}
|
||||
|
||||
# If cookies are present, do not cache (safe default).
|
||||
if (req.http.Cookie) {
|
||||
return (pass);
|
||||
}
|
||||
|
||||
# Never cache Next.js data requests (often personalized) unless you explicitly want to.
|
||||
if (req.url ~ "^/_next/data/") {
|
||||
return (pass);
|
||||
}
|
||||
|
||||
# Cache immutable build assets.
|
||||
if (req.url ~ "^/_next/static/") {
|
||||
unset req.http.Cookie;
|
||||
return (hash);
|
||||
}
|
||||
|
||||
# Cache common static files.
|
||||
if (req.url ~ "\.(?:css|js|mjs|map|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|otf)$") {
|
||||
unset req.http.Cookie;
|
||||
return (hash);
|
||||
}
|
||||
|
||||
# Default: don't cache HTML.
|
||||
return (pass);
|
||||
}
|
||||
|
||||
sub vcl_backend_response {
|
||||
# Cache immutable Next build assets for a long time.
|
||||
if (bereq.url ~ "^/_next/static/") {
|
||||
set beresp.ttl = 365d;
|
||||
set beresp.grace = 1h;
|
||||
set beresp.http.Cache-Control = "public, max-age=31536000, immutable";
|
||||
unset beresp.http.Set-Cookie;
|
||||
return (deliver);
|
||||
}
|
||||
|
||||
# Cache static files for 7 days (safe default).
|
||||
if (bereq.url ~ "\.(?:css|js|mjs|map|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|otf)$") {
|
||||
set beresp.ttl = 7d;
|
||||
set beresp.grace = 1h;
|
||||
if (!beresp.http.Cache-Control) {
|
||||
set beresp.http.Cache-Control = "public, max-age=604800";
|
||||
}
|
||||
unset beresp.http.Set-Cookie;
|
||||
return (deliver);
|
||||
}
|
||||
|
||||
# Everything else: don't cache by default.
|
||||
set beresp.ttl = 0s;
|
||||
set beresp.uncacheable = true;
|
||||
return (deliver);
|
||||
}
|
||||
|
||||
sub vcl_deliver {
|
||||
# Helpful debug header; remove if you don't want this visible.
|
||||
if (obj.hits > 0) {
|
||||
set resp.http.X-Cache = "HIT";
|
||||
} else {
|
||||
set resp.http.X-Cache = "MISS";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user