Compare commits
9 Commits
feature/st
...
v1.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| 5036c5fe28 | |||
| 50a524c515 | |||
| 57886a01d6 | |||
| c89bd8e80f | |||
| 9c54322654 | |||
| 8a80eb7b9a | |||
| c1773a7072 | |||
| 33ed13d255 | |||
| 0f5811edb9 |
14
.env.example
14
.env.example
@@ -40,6 +40,14 @@ MAIL_RECIPIENTS=info@klz-cables.com
|
|||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Deployment Configuration (CI/CD only)
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# These are typically set by the CI/CD workflow
|
||||||
|
IMAGE_TAG=latest
|
||||||
|
TRAEFIK_HOST=klz-cables.com
|
||||||
|
ENV_FILE=.env
|
||||||
|
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
# Varnish Cache (Docker only)
|
# Varnish Cache (Docker only)
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -61,7 +69,11 @@ VARNISH_CACHE_SIZE=256m
|
|||||||
# ──────────────────
|
# ──────────────────
|
||||||
# 1. Build-time: Only NEXT_PUBLIC_* vars are needed (via --build-arg)
|
# 1. Build-time: Only NEXT_PUBLIC_* vars are needed (via --build-arg)
|
||||||
# 2. Runtime: All vars are loaded from .env file on the server
|
# 2. Runtime: All vars are loaded from .env file on the server
|
||||||
# 3. The .env file should exist at: /home/deploy/sites/klz-cables.com/.env
|
# 3. Branch Deployments:
|
||||||
|
# - main branch uses .env.prod
|
||||||
|
# - staging branch uses .env.staging
|
||||||
|
# - CI/CD supports STAGING_ prefix for all secrets to override defaults
|
||||||
|
# - TRAEFIK_HOST is automatically derived from NEXT_PUBLIC_BASE_URL
|
||||||
#
|
#
|
||||||
# Security:
|
# Security:
|
||||||
# ─────────
|
# ─────────
|
||||||
|
|||||||
@@ -2,184 +2,241 @@ name: Build & Deploy KLZ Cables
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# LOGGING: Workflow Start - Full Transparency
|
# Workflow Start & Basic Info
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
- name: 📋 Log Workflow Start
|
- name: 📢 Workflow Start
|
||||||
run: |
|
run: |
|
||||||
echo "🚀 Starting deployment for ${{ github.repository }} (${{ github.ref }})"
|
echo "┌──────────────────────────────────────────────────────────────┐"
|
||||||
echo " • Commit: ${{ github.sha }}"
|
echo "│ 🚀 KLZ Cables Deployment Workflow gestartet │"
|
||||||
echo " • Timestamp: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
|
echo "├──────────────────────────────────────────────────────────────┤"
|
||||||
|
echo "│ Repository: ${{ github.repository }} │"
|
||||||
|
echo "│ Ref: ${{ github.ref }} │"
|
||||||
|
echo "│ Ref-Name: ${{ github.ref_name }} │"
|
||||||
|
echo "│ Commit: ${{ github.sha }} │"
|
||||||
|
echo "│ Actor: ${{ github.actor }} │"
|
||||||
|
echo "│ Datum: $(date -u +'%Y-%m-%d %H:%M:%S UTC') │"
|
||||||
|
echo "└──────────────────────────────────────────────────────────────┘"
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# LOGGING: Registry Login Phase
|
# Environment bestimmen + Commit-Message holen
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
- name: 🔐 Login to private registry
|
- name: 🔍 Environment & Version ermitteln
|
||||||
|
id: determine
|
||||||
run: |
|
run: |
|
||||||
echo "🔐 Authenticating with registry.infra.mintel.me..."
|
TAG="${{ github.ref_name }}"
|
||||||
|
SHORT_SHA="${{ github.sha }}"
|
||||||
|
SHORT_SHA="${SHORT_SHA:0:9}"
|
||||||
|
|
||||||
|
# Commit-Message holen (erste Zeile)
|
||||||
|
COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available")
|
||||||
|
|
||||||
|
if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then
|
||||||
|
TARGET="testing"
|
||||||
|
IMAGE_TAG="main-${SHORT_SHA}"
|
||||||
|
ENV_FILE=".env.testing"
|
||||||
|
TRAEFIK_HOST="\`testing.klz-cables.com\`"
|
||||||
|
IS_PROD="false"
|
||||||
|
GOTIFY_TITLE="🧪 Testing-Deploy"
|
||||||
|
GOTIFY_PRIORITY=4
|
||||||
|
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||||
|
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
TARGET="production"
|
||||||
|
IMAGE_TAG="$TAG"
|
||||||
|
ENV_FILE=".env.prod"
|
||||||
|
TRAEFIK_HOST="\`klz-cables.com\`, \`www.klz-cables.com\`"
|
||||||
|
IS_PROD="true"
|
||||||
|
GOTIFY_TITLE="🚀 Production-Release"
|
||||||
|
GOTIFY_PRIORITY=6
|
||||||
|
elif [[ "$TAG" =~ -rc\. || "$TAG" =~ -beta\. || "$TAG" =~ -alpha\. ]]; then
|
||||||
|
TARGET="staging"
|
||||||
|
IMAGE_TAG="$TAG"
|
||||||
|
ENV_FILE=".env.staging"
|
||||||
|
TRAEFIK_HOST="\`staging.klz-cables.com\`"
|
||||||
|
IS_PROD="false"
|
||||||
|
GOTIFY_TITLE="🧪 Staging-Deploy (Pre-Release)"
|
||||||
|
GOTIFY_PRIORITY=5
|
||||||
|
else
|
||||||
|
TARGET="skip"
|
||||||
|
GOTIFY_TITLE="❓ Unbekannter Tag"
|
||||||
|
GOTIFY_PRIORITY=3
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TARGET="skip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "target=$TARGET" >> $GITHUB_OUTPUT
|
||||||
|
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "env_file=$ENV_FILE" >> $GITHUB_OUTPUT
|
||||||
|
echo "traefik_host=$TRAEFIK_HOST" >> $GITHUB_OUTPUT
|
||||||
|
echo "is_prod=$IS_PROD" >> $GITHUB_OUTPUT
|
||||||
|
echo "gotify_title=$GOTIFY_TITLE" >> $GITHUB_OUTPUT
|
||||||
|
echo "gotify_priority=$GOTIFY_PRIORITY" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: ⏭️ Skip Deployment
|
||||||
|
if: steps.determine.outputs.target == 'skip'
|
||||||
|
run: |
|
||||||
|
echo "Deployment übersprungen – kein passender Trigger (main oder v*-Tag)"
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Registry Login
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: 🔐 Registry Login
|
||||||
|
run: |
|
||||||
|
echo "🔐 Login zu registry.infra.mintel.me ..."
|
||||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# LOGGING: Build Phase
|
# Build & Push
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
- name: 🏗️ Build Docker image
|
- name: 🏗️ Docker Image bauen & pushen
|
||||||
|
env:
|
||||||
|
IMAGE_TAG: ${{ steps.determine.outputs.image_tag }}
|
||||||
|
NEXT_PUBLIC_BASE_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }}
|
||||||
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
|
||||||
|
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
|
||||||
run: |
|
run: |
|
||||||
echo "🏗️ Building Docker image (linux/arm64)..."
|
echo "🏗️ Building → ${{ steps.determine.outputs.target }} / $IMAGE_TAG"
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--pull \
|
--pull \
|
||||||
--platform linux/arm64 \
|
--platform linux/arm64 \
|
||||||
--build-arg NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \
|
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
|
||||||
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \
|
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \
|
||||||
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}" \
|
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \
|
||||||
-t registry.infra.mintel.me/mintel/klz-cables.com:latest \
|
-t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# LOGGING: Deployment Phase
|
# Deploy via SSH
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
- name: 🚀 Deploy to production server
|
- name: 🚀 Deploy to ${{ steps.determine.outputs.target }}
|
||||||
|
env:
|
||||||
|
IMAGE_TAG: ${{ steps.determine.outputs.image_tag }}
|
||||||
|
ENV_FILE: ${{ steps.determine.outputs.env_file }}
|
||||||
|
TRAEFIK_HOST: ${{ steps.determine.outputs.traefik_host }}
|
||||||
|
# Secrets wie vorher – mit Fallback-Logik pro Umgebung
|
||||||
|
NEXT_PUBLIC_BASE_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }}
|
||||||
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
|
||||||
|
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
|
||||||
|
SENTRY_DSN: ${{ steps.determine.outputs.target == 'production' && secrets.SENTRY_DSN || (steps.determine.outputs.target == 'staging' && secrets.STAGING_SENTRY_DSN || secrets.TESTING_SENTRY_DSN || secrets.SENTRY_DSN) }}
|
||||||
|
MAIL_HOST: ${{ secrets.MAIL_HOST }}
|
||||||
|
MAIL_PORT: ${{ secrets.MAIL_PORT }}
|
||||||
|
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
|
||||||
|
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
|
||||||
|
MAIL_FROM: ${{ secrets.MAIL_FROM }}
|
||||||
|
MAIL_RECIPIENTS: ${{ secrets.MAIL_RECIPIENTS }}
|
||||||
run: |
|
run: |
|
||||||
echo "🚀 Deploying to alpha.mintel.me..."
|
echo "Deploying ${{ steps.determine.outputs.target }} → $IMAGE_TAG"
|
||||||
|
|
||||||
# Setup SSH
|
# SSH vorbereiten
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
# Create .env file content
|
|
||||||
cat > /tmp/klz-cables.env << EOF
|
|
||||||
# ============================================================================
|
|
||||||
# KLZ Cables - Production Environment Configuration
|
|
||||||
# ============================================================================
|
|
||||||
# Auto-generated by CI/CD workflow
|
|
||||||
# DO NOT EDIT MANUALLY - Changes will be overwritten on next deployment
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Application
|
|
||||||
NODE_ENV=production
|
|
||||||
NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}
|
|
||||||
|
|
||||||
# Analytics (Umami)
|
|
||||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
|
||||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL=${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}
|
|
||||||
|
|
||||||
# Error Tracking (GlitchTip/Sentry)
|
|
||||||
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
|
|
||||||
|
|
||||||
# Email Configuration (Mailgun)
|
|
||||||
MAIL_HOST=${{ secrets.MAIL_HOST }}
|
|
||||||
MAIL_PORT=${{ secrets.MAIL_PORT }}
|
|
||||||
MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}
|
|
||||||
MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}
|
|
||||||
MAIL_FROM=${{ secrets.MAIL_FROM }}
|
|
||||||
MAIL_RECIPIENTS=${{ secrets.MAIL_RECIPIENTS }}
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Upload .env and deploy
|
|
||||||
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env
|
|
||||||
|
|
||||||
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << EOF
|
|
||||||
set -e
|
|
||||||
cd /home/deploy/sites/klz-cables.com
|
|
||||||
|
|
||||||
chmod 600 .env
|
|
||||||
chown deploy:deploy .env
|
|
||||||
|
|
||||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
|
||||||
docker pull registry.infra.mintel.me/mintel/klz-cables.com:latest
|
|
||||||
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
echo "🚀 Starting containers..."
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
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 "✅ Deployment complete!"
|
# .env-Datei erstellen
|
||||||
|
cat > /tmp/klz-cables.env << EOF
|
||||||
|
# Generated by CI - ${{ steps.determine.outputs.target }} - $(date -u)
|
||||||
|
NODE_ENV=production
|
||||||
|
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||||
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||||
|
NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
|
||||||
|
SENTRY_DSN=$SENTRY_DSN
|
||||||
|
MAIL_HOST=$MAIL_HOST
|
||||||
|
MAIL_PORT=$MAIL_PORT
|
||||||
|
MAIL_USERNAME=$MAIL_USERNAME
|
||||||
|
MAIL_PASSWORD=$MAIL_PASSWORD
|
||||||
|
MAIL_FROM=$MAIL_FROM
|
||||||
|
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
|
||||||
|
|
||||||
|
IMAGE_TAG=$IMAGE_TAG
|
||||||
|
TRAEFIK_HOST=$TRAEFIK_HOST
|
||||||
|
ENV_FILE=$ENV_FILE
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/$ENV_FILE
|
||||||
|
scp -o StrictHostKeyChecking=accept-new docker-compose.yml root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/docker-compose.yml
|
||||||
|
|
||||||
|
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" TRAEFIK_HOST="$TRAEFIK_HOST" bash << 'EOF'
|
||||||
|
set -e
|
||||||
|
cd /home/deploy/sites/klz-cables.com
|
||||||
|
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
chown deploy:deploy "$ENV_FILE"
|
||||||
|
|
||||||
|
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
|
||||||
|
echo "→ Pulling image: $IMAGE_TAG"
|
||||||
|
docker compose --env-file "$ENV_FILE" pull
|
||||||
|
|
||||||
|
echo "→ Starting containers..."
|
||||||
|
docker compose --env-file "$ENV_FILE" up -d
|
||||||
|
|
||||||
|
docker system prune -f --filter "until=168h"
|
||||||
|
|
||||||
|
echo "→ Waiting 15s for warmup..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
echo "→ Container status:"
|
||||||
|
docker compose --env-file "$ENV_FILE" ps
|
||||||
|
|
||||||
|
if ! docker compose --env-file "$ENV_FILE" ps | grep -q "Up"; then
|
||||||
|
echo "❌ Fehler: Container nicht Up!"
|
||||||
|
docker compose --env-file "$ENV_FILE" logs --tail=150
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Deployment erfolgreich!"
|
||||||
|
EOF
|
||||||
|
|
||||||
rm -f /tmp/klz-cables.env
|
rm -f /tmp/klz-cables.env
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# LOGGING: Workflow Summary
|
# Summary & Gotify
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
- name: 📊 Workflow Summary
|
- name: 📊 Deployment Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "📊 Status: ${{ job.status }}"
|
echo "┌──────────────────────────────┐"
|
||||||
echo "🎯 Target: alpha.mintel.me"
|
echo "│ Deployment Summary │"
|
||||||
|
echo "├──────────────────────────────┤"
|
||||||
|
echo "│ Status: ${{ job.status }} │"
|
||||||
|
echo "│ Umgebung: ${{ steps.determine.outputs.target || 'skipped' }} │"
|
||||||
|
echo "│ Version: ${{ steps.determine.outputs.image_tag }} │"
|
||||||
|
echo "│ Commit: ${{ steps.determine.outputs.short_sha }} │"
|
||||||
|
echo "│ Message: ${{ steps.determine.outputs.commit_msg }} │"
|
||||||
|
echo "└──────────────────────────────┘"
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
- name: 🔔 Gotify - Success
|
||||||
# NOTIFICATION: Gotify
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
- name: 🔔 Gotify Notification (Success)
|
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
echo "Sending success notification to Gotify..."
|
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||||
RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
-F "title=${{ steps.determine.outputs.gotify_title }}" \
|
||||||
-F "title=✅ Deployment Success: ${{ github.repository }}" \
|
-F "message=Erfolgreich deployt auf **${{ steps.determine.outputs.target }}**\n\nVersion: **${{ steps.determine.outputs.image_tag }}**\nCommit: ${{ steps.determine.outputs.short_sha }} (${{ steps.determine.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}" \
|
||||||
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) was successful.
|
-F "priority=${{ steps.determine.outputs.gotify_priority }}" || true
|
||||||
|
|
||||||
Commit: ${{ github.sha }}
|
|
||||||
Actor: ${{ github.actor }}
|
|
||||||
Run ID: ${{ github.run_id }}" \
|
|
||||||
-F "priority=5")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
||||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
||||||
|
|
||||||
echo "HTTP Status: $HTTP_CODE"
|
|
||||||
echo "Response Body: $BODY"
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
|
|
||||||
echo "Failed to send Gotify notification"
|
|
||||||
exit 0 # Don't fail the workflow because of notification failure
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 🔔 Gotify Notification (Failure)
|
- name: 🔔 Gotify - Failure
|
||||||
if: failure()
|
if: failure()
|
||||||
run: |
|
run: |
|
||||||
echo "Sending failure notification to Gotify..."
|
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||||
RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
-F "title=❌ Deployment FEHLGESCHLAGEN – ${{ steps.determine.outputs.target || 'unknown' }}" \
|
||||||
-F "title=❌ Deployment Failed: ${{ github.repository }}" \
|
-F "message=**Fehler beim Deploy auf ${{ steps.determine.outputs.target }}**\n\nVersion: ${{ steps.determine.outputs.image_tag || '?' }}\nCommit: ${{ steps.determine.outputs.short_sha || '?' }}\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}\n\nBitte Logs prüfen!" \
|
||||||
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) failed!
|
-F "priority=8" || true
|
||||||
|
|
||||||
Commit: ${{ github.sha }}
|
|
||||||
Actor: ${{ github.actor }}
|
|
||||||
Run ID: ${{ github.run_id }}
|
|
||||||
|
|
||||||
Please check the logs for details." \
|
|
||||||
-F "priority=8")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
||||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
||||||
|
|
||||||
echo "HTTP Status: $HTTP_CODE"
|
|
||||||
echo "Response Body: $BODY"
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
|
|
||||||
echo "Failed to send Gotify notification"
|
|
||||||
exit 0 # Don't fail the workflow because of notification failure
|
|
||||||
fi
|
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -103,7 +103,7 @@ app/
|
|||||||
├── api/
|
├── api/
|
||||||
│ └── contact/route.ts # Contact API
|
│ └── contact/route.ts # Contact API
|
||||||
├── sitemap.ts # Sitemap generator
|
├── sitemap.ts # Sitemap generator
|
||||||
└── robots.ts # Robots.txt generator
|
├── robots.ts # Robots.txt generator
|
||||||
|
|
||||||
lib/
|
lib/
|
||||||
├── data.ts # Data access
|
├── data.ts # Data access
|
||||||
@@ -114,7 +114,7 @@ components/
|
|||||||
├── LocaleSwitcher.tsx # Language switcher
|
├── LocaleSwitcher.tsx # Language switcher
|
||||||
├── ContactForm.tsx # Contact form
|
├── ContactForm.tsx # Contact form
|
||||||
├── CookieConsent.tsx # GDPR banner
|
├── CookieConsent.tsx # GDPR banner
|
||||||
└── SEO.tsx # SEO utilities
|
├── SEO.tsx # SEO utilities
|
||||||
|
|
||||||
data/
|
data/
|
||||||
├── raw/ # WordPress export
|
├── raw/ # WordPress export
|
||||||
@@ -222,21 +222,30 @@ GET /robots.txt
|
|||||||
|
|
||||||
### Automatic Deployment (Current Setup)
|
### Automatic Deployment (Current Setup)
|
||||||
|
|
||||||
The project uses **Gitea Actions** for CI/CD. Every push to `main` triggers:
|
The project uses **Gitea Actions** for CI/CD. Every push to `main` or `staging` triggers:
|
||||||
|
|
||||||
1. **Build**: Docker image built for `linux/arm64`
|
1. **Build**: Docker image built for `linux/arm64` with branch-specific build args
|
||||||
2. **Push**: Image pushed to `registry.infra.mintel.me`
|
2. **Push**: Image pushed to `registry.infra.mintel.me` with commit SHA tag
|
||||||
3. **Deploy**: SSH to production server, pull and restart containers
|
3. **Deploy**: SSH to production server, pull and restart containers using branch-specific `.env` files
|
||||||
|
|
||||||
**Workflow**: `.gitea/workflows/deploy.yml`
|
**Workflow**: `.gitea/workflows/deploy.yml`
|
||||||
|
|
||||||
|
**Branch Deployments**:
|
||||||
|
- `main` branch: Deploys to production using `.env.prod`
|
||||||
|
- `staging` branch: Deploys to staging using `.env.staging`
|
||||||
|
|
||||||
|
**Environment Overrides**:
|
||||||
|
The CI/CD workflow supports `STAGING_`-prefixed secrets (e.g., `STAGING_NEXT_PUBLIC_BASE_URL`) to override default secrets when deploying the `staging` branch.
|
||||||
|
|
||||||
**Required Secrets** (configure in Gitea repository settings):
|
**Required Secrets** (configure in Gitea repository settings):
|
||||||
- `REGISTRY_USER` - Docker registry username
|
- `REGISTRY_USER` - Docker registry username
|
||||||
- `REGISTRY_PASS` - Docker registry password
|
- `REGISTRY_PASS` - Docker registry password
|
||||||
- `ALPHA_SSH_KEY` - SSH private key for deployment
|
- `ALPHA_SSH_KEY` - SSH private key for deployment
|
||||||
|
- `NEXT_PUBLIC_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`)
|
||||||
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
|
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
|
||||||
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
|
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
|
||||||
- `SENTRY_DSN` - Error tracking DSN
|
- `SENTRY_DSN` - Error tracking DSN
|
||||||
|
- `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `MAIL_RECIPIENTS` - Email configuration
|
||||||
|
|
||||||
### Manual Deployment
|
### Manual Deployment
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export async function GET(
|
|||||||
title={product.frontmatter.title}
|
title={product.frontmatter.title}
|
||||||
description={product.frontmatter.description}
|
description={product.frontmatter.description}
|
||||||
label={product.frontmatter.categories?.[0] || 'Product'}
|
label={product.frontmatter.categories?.[0] || 'Product'}
|
||||||
image={featuredImage}
|
image={featuredImage?.startsWith('http') ? featuredImage : undefined}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
|
|||||||
title={post.frontmatter.title}
|
title={post.frontmatter.title}
|
||||||
description={post.frontmatter.excerpt}
|
description={post.frontmatter.excerpt}
|
||||||
label={post.frontmatter.category || 'Blog'}
|
label={post.frontmatter.category || 'Blog'}
|
||||||
image={featuredImage}
|
image={featuredImage?.startsWith('http') ? featuredImage : undefined}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ProductSidebar from '@/components/ProductSidebar';
|
|||||||
import ProductTabs from '@/components/ProductTabs';
|
import ProductTabs from '@/components/ProductTabs';
|
||||||
import ProductTechnicalData from '@/components/ProductTechnicalData';
|
import ProductTechnicalData from '@/components/ProductTechnicalData';
|
||||||
import RelatedProducts from '@/components/RelatedProducts';
|
import RelatedProducts from '@/components/RelatedProducts';
|
||||||
|
import DatasheetDownload from '@/components/DatasheetDownload';
|
||||||
import { Badge, Container, Heading, Section } from '@/components/ui';
|
import { Badge, Container, Heading, Section } from '@/components/ui';
|
||||||
import { getDatasheetPath } from '@/lib/datasheets';
|
import { getDatasheetPath } from '@/lib/datasheets';
|
||||||
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
|
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
|
||||||
@@ -362,6 +363,19 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<MDXRemote source={processedContent} components={productComponents} />
|
<MDXRemote source={processedContent} components={productComponents} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Datasheet Download Section - Only for Medium Voltage for now */}
|
||||||
|
{categoryFileSlug === 'medium-voltage-cables' && datasheetPath && (
|
||||||
|
<div className="mt-24 pt-24 border-t-2 border-neutral-dark/5">
|
||||||
|
<div className="mb-12">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-4">
|
||||||
|
{t('downloadDatasheet')}
|
||||||
|
</h2>
|
||||||
|
<div className="h-1.5 w-24 bg-accent rounded-full" />
|
||||||
|
</div>
|
||||||
|
<DatasheetDownload datasheetPath={datasheetPath} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Structured Data */}
|
{/* Structured Data */}
|
||||||
<JsonLd
|
<JsonLd
|
||||||
id={`jsonld-${product.slug}`}
|
id={`jsonld-${product.slug}`}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
|
|||||||
title={product.frontmatter.title}
|
title={product.frontmatter.title}
|
||||||
description={product.frontmatter.description}
|
description={product.frontmatter.description}
|
||||||
label={product.frontmatter.categories?.[0] || 'Product'}
|
label={product.frontmatter.categories?.[0] || 'Product'}
|
||||||
image={featuredImage}
|
image={featuredImage?.startsWith('http') ? featuredImage : undefined}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export default async function Image({ params: { locale } }: { params: { locale:
|
|||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
label="Our Team"
|
label="Our Team"
|
||||||
image="https://klz-cables.com/uploads/2024/12/DSC07655-Large.webp"
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|||||||
68
components/DatasheetDownload.tsx
Normal file
68
components/DatasheetDownload.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@/components/ui/utils';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
interface DatasheetDownloadProps {
|
||||||
|
datasheetPath: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DatasheetDownload({ datasheetPath, className }: DatasheetDownloadProps) {
|
||||||
|
const t = useTranslations('Products');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("mt-8 animate-slight-fade-in-from-bottom", className)}>
|
||||||
|
<a
|
||||||
|
href={datasheetPath}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group relative block w-full overflow-hidden rounded-[32px] bg-primary-dark p-1 transition-all duration-500 hover:shadow-[0_20px_50px_rgba(0,0,0,0.2)] hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
{/* Animated Background Gradient */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-accent via-saturated to-accent opacity-20 group-hover:opacity-40 transition-opacity duration-500 animate-gradient-x" />
|
||||||
|
|
||||||
|
{/* Inner Content */}
|
||||||
|
<div className="relative flex items-center gap-6 rounded-[31px] bg-primary-dark/90 backdrop-blur-xl p-6 md:p-8 border border-white/10">
|
||||||
|
{/* Icon Container */}
|
||||||
|
<div className="relative flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-2xl bg-white/5 border border-white/10 group-hover:bg-accent group-hover:border-white/20 transition-all duration-500">
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-accent/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
|
<svg
|
||||||
|
className="relative h-8 w-8 text-white transition-transform duration-500 group-hover:scale-110 group-hover:rotate-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text Content */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-accent">PDF Datasheet</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl md:text-2xl font-black text-white uppercase tracking-tighter leading-none group-hover:text-accent transition-colors duration-300">
|
||||||
|
{t('downloadDatasheet')}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm font-medium text-white/60 leading-relaxed">
|
||||||
|
{t('downloadDatasheetDesc')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arrow Icon */}
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/5 text-white/20 group-hover:bg-accent group-hover:text-white group-hover:translate-x-1 transition-all duration-500">
|
||||||
|
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@ export function OGImageTemplate({
|
|||||||
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
|
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
|
||||||
padding: '80px',
|
padding: '80px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
fontFamily: 'Inter, sans-serif',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,7 +38,10 @@ export function OGImageTemplate({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
inset: 0,
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -57,8 +59,11 @@ export function OGImageTemplate({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
inset: 0,
|
top: 0,
|
||||||
background: 'linear-gradient(to right, rgba(0,26,77,0.9) 0%, rgba(0,26,77,0.4) 100%)',
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'linear-gradient(to right, rgba(0,26,77,0.9), rgba(0,26,77,0.4))',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,8 +77,8 @@ export function OGImageTemplate({
|
|||||||
right: '-100px',
|
right: '-100px',
|
||||||
width: '600px',
|
width: '600px',
|
||||||
height: '600px',
|
height: '600px',
|
||||||
borderRadius: '50%',
|
borderRadius: '300px',
|
||||||
background: `radial-gradient(circle, ${accentGreen}1a 0%, transparent 70%)`,
|
backgroundColor: `${accentGreen}1a`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import RequestQuoteForm from '@/components/RequestQuoteForm';
|
import RequestQuoteForm from '@/components/RequestQuoteForm';
|
||||||
|
import DatasheetDownload from '@/components/DatasheetDownload';
|
||||||
import Scribble from '@/components/Scribble';
|
import Scribble from '@/components/Scribble';
|
||||||
import { cn } from '@/components/ui/utils';
|
import { cn } from '@/components/ui/utils';
|
||||||
|
|
||||||
@@ -64,33 +65,7 @@ export default function ProductSidebar({ productName, productImage, datasheetPat
|
|||||||
|
|
||||||
{/* Datasheet Download */}
|
{/* Datasheet Download */}
|
||||||
{datasheetPath && (
|
{datasheetPath && (
|
||||||
<a
|
<DatasheetDownload datasheetPath={datasheetPath} className="mt-0" />
|
||||||
href={datasheetPath}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block bg-white rounded-2xl border border-neutral-medium overflow-hidden group transition-all duration-500 hover:shadow-xl hover:border-saturated/30 hover:-translate-y-0.5"
|
|
||||||
>
|
|
||||||
<div className="p-4 flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 rounded-xl bg-neutral-medium/20 flex items-center justify-center flex-shrink-0 group-hover:bg-saturated group-hover:text-white transition-all duration-500 text-saturated border border-transparent group-hover:border-white/20">
|
|
||||||
<svg className="w-6 h-6 transition-transform duration-500 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="text-sm md:text-base font-heading font-black text-neutral-dark m-0 uppercase tracking-tighter leading-tight group-hover:text-saturated transition-colors duration-300">
|
|
||||||
{t('downloadDatasheet')}
|
|
||||||
</h3>
|
|
||||||
<p className="text-text-secondary text-[10px] md:text-xs m-0 mt-0.5 font-semibold leading-tight truncate uppercase tracking-widest opacity-60">
|
|
||||||
{t('downloadDatasheetDesc')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-neutral-dark/20 group-hover:text-saturated transition-all duration-500 transform group-hover:translate-x-1">
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: registry.infra.mintel.me/mintel/klz-cables.com:latest
|
image: registry.infra.mintel.me/mintel/klz-cables.com:${IMAGE_TAG:-latest}
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- infra
|
- infra
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ${ENV_FILE:-.env}
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# HTTP ⇒ HTTPS redirect
|
# 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.rule=Host(${TRAEFIK_HOST}) && !PathPrefix(`/.well-known/acme-challenge/`)"
|
||||||
- "traefik.http.routers.klz-cables-web.entrypoints=web"
|
- "traefik.http.routers.klz-cables-web.entrypoints=web"
|
||||||
- "traefik.http.routers.klz-cables-web.middlewares=redirect-https"
|
- "traefik.http.routers.klz-cables-web.middlewares=redirect-https"
|
||||||
# HTTPS router
|
# 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.rule=Host(${TRAEFIK_HOST})"
|
||||||
- "traefik.http.routers.klz-cables.entrypoints=websecure"
|
- "traefik.http.routers.klz-cables.entrypoints=websecure"
|
||||||
- "traefik.http.routers.klz-cables.tls.certresolver=le"
|
- "traefik.http.routers.klz-cables.tls.certresolver=le"
|
||||||
- "traefik.http.routers.klz-cables.tls=true"
|
- "traefik.http.routers.klz-cables.tls=true"
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export function getDatasheetPath(slug: string, locale: string): string | null {
|
|||||||
// Normalize slug: remove common suffixes that might not be in the PDF filename
|
// Normalize slug: remove common suffixes that might not be in the PDF filename
|
||||||
const normalizedSlug = slug.replace(/-hv$|-mv$/, '');
|
const normalizedSlug = slug.replace(/-hv$|-mv$/, '');
|
||||||
|
|
||||||
|
// Subdirectories to search in
|
||||||
|
const subdirs = ['', 'low-voltage', 'medium-voltage', 'high-voltage', 'solar'];
|
||||||
|
|
||||||
// List of patterns to try for the current locale
|
// List of patterns to try for the current locale
|
||||||
const patterns = [
|
const patterns = [
|
||||||
`${slug}-${locale}.pdf`,
|
`${slug}-${locale}.pdf`,
|
||||||
@@ -25,10 +28,13 @@ export function getDatasheetPath(slug: string, locale: string): string | null {
|
|||||||
`${normalizedSlug}-3-${locale}.pdf`,
|
`${normalizedSlug}-3-${locale}.pdf`,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const subdir of subdirs) {
|
||||||
const filePath = path.join(datasheetsDir, pattern);
|
for (const pattern of patterns) {
|
||||||
if (fs.existsSync(filePath)) {
|
const relativePath = path.join(subdir, pattern);
|
||||||
return `/datasheets/${pattern}`;
|
const filePath = path.join(datasheetsDir, relativePath);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
return `/datasheets/${relativePath}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +48,13 @@ export function getDatasheetPath(slug: string, locale: string): string | null {
|
|||||||
`${normalizedSlug}-2-en.pdf`,
|
`${normalizedSlug}-2-en.pdf`,
|
||||||
`${normalizedSlug}-3-en.pdf`,
|
`${normalizedSlug}-3-en.pdf`,
|
||||||
];
|
];
|
||||||
for (const pattern of enPatterns) {
|
for (const subdir of subdirs) {
|
||||||
const filePath = path.join(datasheetsDir, pattern);
|
for (const pattern of enPatterns) {
|
||||||
if (fs.existsSync(filePath)) {
|
const relativePath = path.join(subdir, pattern);
|
||||||
return `/datasheets/${pattern}`;
|
const filePath = path.join(datasheetsDir, relativePath);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
return `/datasheets/${relativePath}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { Metadata } from 'next';
|
|||||||
import { SITE_URL } from './schema';
|
import { SITE_URL } from './schema';
|
||||||
|
|
||||||
export function getOGImageMetadata(path: string, title: string, locale: string): NonNullable<Metadata['openGraph']>['images'] {
|
export function getOGImageMetadata(path: string, title: string, locale: string): NonNullable<Metadata['openGraph']>['images'] {
|
||||||
|
const cleanPath = path ? (path.startsWith('/') ? path : `/${path}`) : '';
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: `${SITE_URL}/${locale}/${path}/opengraph-image`,
|
url: `${SITE_URL}/${locale}${cleanPath}/opengraph-image`,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: title,
|
alt: title,
|
||||||
|
|||||||
46
scripts/test-og-images.ts
Normal file
46
scripts/test-og-images.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
|
const baseUrl = 'http://localhost:3010';
|
||||||
|
const paths = [
|
||||||
|
'/en/opengraph-image',
|
||||||
|
'/de/opengraph-image',
|
||||||
|
'/en/blog/opengraph-image',
|
||||||
|
'/en/contact/opengraph-image',
|
||||||
|
'/en/products/opengraph-image',
|
||||||
|
'/en/team/opengraph-image',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function testUrl(path: string) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const url = `${baseUrl}${path}`;
|
||||||
|
console.log(`Testing ${url}...`);
|
||||||
|
const req = http.get(url, (res) => {
|
||||||
|
console.log(` Status: ${res.statusCode}`);
|
||||||
|
console.log(` Content-Type: ${res.headers['content-type']}`);
|
||||||
|
resolve(res.statusCode === 200);
|
||||||
|
});
|
||||||
|
req.on('error', (e) => {
|
||||||
|
console.error(` Error: ${e.message}`);
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
let allPassed = true;
|
||||||
|
for (const path of paths) {
|
||||||
|
const passed = await testUrl(path);
|
||||||
|
if (!passed) allPassed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allPassed) {
|
||||||
|
console.log('\n✅ All OG images are working!');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log('\n❌ Some OG images failed.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -37,7 +37,12 @@
|
|||||||
--animate-slow-zoom: slow-zoom 20s linear infinite;
|
--animate-slow-zoom: slow-zoom 20s linear infinite;
|
||||||
--animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
--animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
--animate-slight-fade-in-from-bottom: slight-fade-in-from-bottom 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
--animate-slight-fade-in-from-bottom: slight-fade-in-from-bottom 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
--animate-gradient-x: gradient-x 15s ease infinite;
|
||||||
|
|
||||||
|
@keyframes gradient-x {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user