diff --git a/.env.example b/.env.example index aedabe8..d61bb11 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,84 @@ +# ============================================================================== +# PROJECT SETTINGS +# ============================================================================== +PROJECT_NAME=mb-grid-solutions.com +PROJECT_COLOR=#82ed20 + +# ============================================================================== +# HOST CONFIGURATION (LOCAL DEV) +# ============================================================================== +# These are used by Traefik in local development. +# In CI/CD, these are automatically set by the deployment pipeline. +TRAEFIK_HOST=mb-grid-solutions.localhost +DIRECTUS_HOST=cms.mb-grid-solutions.localhost + +# ============================================================================== +# NEXT.JS SETTINGS +# ============================================================================== +# The public URL of the frontend. Used for absolute links and meta tags. +NEXT_PUBLIC_BASE_URL=http://mb-grid-solutions.localhost + +# ============================================================================== +# DIRECTUS CMS SETTINGS +# ============================================================================== +# Public URL of the CMS (must be accessible from the browser) +# Automatisierung: Wird in CI/CD automatisch basierend auf der Umgebung gesetzt. +DIRECTUS_URL=http://cms.mb-grid-solutions.localhost + +# CMS Authentication - Create a Static Token in Directus User Settings +# Automatisierung: Wird in CI/CD aus den Gitea Secrets (DIRECTUS_API_TOKEN) gelesen. +# Smart Fallback: Wenn kein Token gesetzt ist, wird automatisch der Admin-Login verwendet. +DIRECTUS_API_TOKEN= + +# Initial Setup (Admin User) +DIRECTUS_ADMIN_EMAIL=marc@mintel.me +DIRECTUS_ADMIN_PASSWORD=Tim300493. + +# Database Settings (Local Docker) +DIRECTUS_DB_NAME=directus +DIRECTUS_DB_USER=directus +DIRECTUS_DB_PASSWORD=mintel-db-pass + +# Security Keys (Generate random strings for production) +# Automatisierung: Werden in CI/CD aus Gitea Secrets gelesen. +# DIRECTUS_KEY= +# DIRECTUS_SECRET= + +# ============================================================================== +# SMTP CONFIGURATION (CONTACT FORM) +# ============================================================================== SMTP_HOST=smtp.example.com SMTP_PORT=587 + +# SMTP_SECURE: +# - true: Use SSL/TLS (usually Port 465). +# - false: Use STARTTLS (usually Port 587) or no encryption. SMTP_SECURE=false + SMTP_USER=user@example.com SMTP_PASS=your_password SMTP_FROM="MB Grid Solutions " + +# Comma-separated list of recipients for contact form submissions CONTACT_RECIPIENT=info@mb-grid-solutions.com,admin@mb-grid-solutions.com + +# ============================================================================== +# AUTHENTICATION (GATEKEEPER) +# ============================================================================== +GATEKEEPER_PASSWORD=lassmichrein +AUTH_COOKIE_NAME=mintel_gatekeeper_session + +# ============================================================================== +# EXTERNAL SERVICES +# ============================================================================== + +# Sentry / Glitchtip (Error Tracking) +SENTRY_DSN= + +# Gotify (In-App Notifications) +# GOTIFY_URL= +# GOTIFY_TOKEN= + +# Analytics (Umami) +NEXT_PUBLIC_UMAMI_WEBSITE_ID= +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 2ada274..b64014d 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,280 +1,169 @@ -name: Build & Deploy MB Grid Solutions +name: Build & Deploy on: push: - branches: [main] + branches: + - main + tags: + - 'v*' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false jobs: - build-and-deploy: - # ──────────────────────────────────────────────── - # WICHTIG: Kein "docker" mehr – sondern eines der neuen Labels + prepare: + name: 🔍 Prepare Environment runs-on: docker - + outputs: + target: ${{ steps.determine.outputs.target }} + image_tag: ${{ steps.determine.outputs.image_tag }} + env_file: ${{ steps.determine.outputs.env_file }} + traefik_host: ${{ steps.determine.outputs.traefik_host }} + next_public_base_url: ${{ steps.determine.outputs.next_public_base_url }} + directus_url: ${{ steps.determine.outputs.directus_url }} + directus_host: ${{ steps.determine.outputs.directus_host }} + project_name: ${{ steps.determine.outputs.project_name }} steps: - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Workflow Start - Full Transparency - # ═══════════════════════════════════════════════════════════════════════════════ - - name: 📋 Log Workflow Start - run: | - echo "╔══════════════════════════════════════════════════════════════════════════════╗" - echo "║ MB Grid Solutions Deployment Workflow Started ║" - echo "╚══════════════════════════════════════════════════════════════════════════════╝" - echo "" - echo "📋 Workflow Information:" - echo " • Repository: ${{ github.repository }}" - echo " • Branch: ${{ 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 + with: + fetch-depth: 2 - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Registry Login Phase - # ═══════════════════════════════════════════════════════════════════════════════ - - name: 🔐 Login to private registry + - name: 🔍 Determine Environment + id: determine 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 "" + TAG="${{ github.ref_name }}" + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + DOMAIN_BASE="mb-grid-solutions.com" + PRJ_ID="mb-grid-solutions" - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Build Phase - # ═══════════════════════════════════════════════════════════════════════════════ - - name: 🏗️ Build Docker image + if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then + TARGET="staging" + IMAGE_TAG="staging-${SHORT_SHA}" + ENV_FILE=".env.staging" + TRAEFIK_HOST="\`staging.${DOMAIN_BASE}\`" + NEXT_PUBLIC_BASE_URL="https://staging.${DOMAIN_BASE}" + DIRECTUS_URL="https://cms.staging.${DOMAIN_BASE}" + DIRECTUS_HOST="\`cms.staging.${DOMAIN_BASE}\`" + elif [[ "${{ github.ref_type }}" == "tag" ]]; then + TARGET="production" + IMAGE_TAG="$TAG" + ENV_FILE=".env.prod" + TRAEFIK_HOST="\`${DOMAIN_BASE}\`, \`www.${DOMAIN_BASE}\`" + NEXT_PUBLIC_BASE_URL="https://${DOMAIN_BASE}" + DIRECTUS_URL="https://cms.${DOMAIN_BASE}" + DIRECTUS_HOST="\`cms.${DOMAIN_BASE}\`" + 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 "next_public_base_url=$NEXT_PUBLIC_BASE_URL" >> "$GITHUB_OUTPUT" + echo "directus_url=$DIRECTUS_URL" >> "$GITHUB_OUTPUT" + echo "directus_host=$DIRECTUS_HOST" >> "$GITHUB_OUTPUT" + echo "project_name=$PRJ_ID-$TARGET" >> "$GITHUB_OUTPUT" + + qa: + name: 🧪 QA + needs: prepare + if: needs.prepare.outputs.target != 'skip' + runs-on: docker + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: pnpm lint + - run: pnpm build + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NEXT_PUBLIC_BASE_URL: https://dummy.test + + build: + name: 🏗️ Build + needs: prepare + if: needs.prepare.outputs.target != 'skip' + runs-on: docker + steps: + - uses: actions/checkout@v4 + - name: 🔐 Registry Login + run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + + - name: 🏗️ Build and Push 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/mb-grid-solutions:latest" - echo "" - echo "⏱️ Build started at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" - echo "" - - # Execute build with detailed logging - set -e - docker buildx build \ - --pull \ - --platform linux/arm64 \ - --build-arg NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \ - --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ - --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}" \ - -t registry.infra.mintel.me/mintel/mb-grid-solutions: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/mb-grid-solutions:latest --format='{{.Size}}') - IMAGE_SIZE_MB=$((IMAGE_SIZE / 1024 / 1024)) - echo " • Size: ${IMAGE_SIZE_MB}MB" - docker inspect registry.infra.mintel.me/mintel/mb-grid-solutions:latest --format=' • Created: {{.Created}}' - docker inspect registry.infra.mintel.me/mintel/mb-grid-solutions:latest --format=' • Architecture: {{.Architecture}}' - else - echo "" - echo "❌ Build failed with exit code: $BUILD_EXIT_CODE" - exit $BUILD_EXIT_CODE - fi - echo "" + docker build \ + --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ + --build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} \ + --build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \ + -t registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} . + docker push registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} - # ═══════════════════════════════════════════════════════════════════════════════ - # 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/mb-grid-solutions.com" - echo "" - - # Setup SSH with logging - echo "🔐 Setting up SSH connection..." - 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 "" - - # Sync docker-compose.yaml first - echo "📦 Syncing docker-compose.yaml..." - tar czf - docker-compose.yaml | \ - ssh -o StrictHostKeyChecking=accept-new \ - -o IPQoS=0x00 \ - root@alpha.mintel.me \ - "mkdir -p /home/deploy/sites/mb-grid-solutions.com/ && tar xzf - -C /home/deploy/sites/mb-grid-solutions.com/ && chown -R deploy:deploy /home/deploy/sites/mb-grid-solutions.com/" - - if [ $? -eq 0 ]; then - echo "✅ Files synced successfully" - else - echo "❌ File sync failed" - exit 1 - fi - echo "" - - # Execute deployment commands with detailed logging - echo "📡 Connecting to server and executing deployment commands..." - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - - # SSH as root and use sudo to run deployment script as deploy user - # This works around the broken SSH output issue with deploy user - ssh -o StrictHostKeyChecking=accept-new \ - -o ServerAliveInterval=30 \ - -o ServerAliveCountMax=3 \ - -o ConnectTimeout=10 \ - root@alpha.mintel.me \ - "CONTACT_RECIPIENT='${{ secrets.CONTACT_RECIPIENT }}' \ - SMTP_FROM='${{ secrets.SMTP_FROM }}' \ - SMTP_HOST='${{ secrets.SMTP_HOST }}' \ - SMTP_PASS='${{ secrets.SMTP_PASS }}' \ - SMTP_PORT='${{ secrets.SMTP_PORT }}' \ - SMTP_SECURE='${{ secrets.SMTP_SECURE }}' \ - SMTP_USER='${{ secrets.SMTP_USER }}' \ - NEXT_PUBLIC_BASE_URL='${{ secrets.NEXT_PUBLIC_BASE_URL }}' \ - NEXT_PUBLIC_UMAMI_WEBSITE_ID='${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}' \ - NEXT_PUBLIC_UMAMI_SCRIPT_URL='${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}' \ - SENTRY_DSN='${{ secrets.SENTRY_DSN }}' \ - SITE_NAME='mb-grid-solutions.com' \ - sudo -u deploy -E HOME=/home/deploy /home/deploy/deploy.sh --zero-downtime" - - DEPLOY_EXIT_CODE=$? - echo "" - - 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 " • Ensure /home/deploy/deploy.sh exists and is executable" - exit $DEPLOY_EXIT_CODE - fi - echo "" - - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Workflow Summary - # ═══════════════════════════════════════════════════════════════════════════════ - - 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/mb-grid-solutions:latest" - echo " • Server: alpha.mintel.me" - echo " • Service: mb-grid-solutions.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 "" - echo "╔══════════════════════════════════════════════════════════════════════════════╗" - if [ "${{ job.status }}" == "success" ]; then - echo "║ ✅ DEPLOYMENT SUCCESSFUL ║" - else - echo "║ ❌ DEPLOYMENT FAILED ║" - fi - echo "╚══════════════════════════════════════════════════════════════════════════════╝" - - # ═══════════════════════════════════════════════════════════════════════════════ - # NOTIFICATION: Gotify - # ═══════════════════════════════════════════════════════════════════════════════ - - name: 🔔 Gotify Notification (Success) - if: success() - run: | - echo "Sending success notification to Gotify..." - RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -F "title=✅ Deployment Success: ${{ github.repository }}" \ - -F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) was successful. + deploy: + name: 🚀 Deploy + needs: [prepare, build, qa] + if: needs.prepare.outputs.target != 'skip' + runs-on: docker + steps: + - name: 🚀 Deploy via SSH + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: root + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + APP_DIR="/home/deploy/sites/mb-grid-solutions.com" + mkdir -p $APP_DIR + cd $APP_DIR - 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 + # Update Environment + cat > ${{ needs.prepare.outputs.env_file }} << EOF + IMAGE_TAG=${{ needs.prepare.outputs.image_tag }} + TRAEFIK_HOST=${{ needs.prepare.outputs.traefik_host }} + PROJECT_NAME=${{ needs.prepare.outputs.project_name }} + NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} + + # Directus + DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} + DIRECTUS_HOST=${{ needs.prepare.outputs.directus_host }} + DIRECTUS_API_TOKEN=${{ secrets.DIRECTUS_API_TOKEN }} + DIRECTUS_ADMIN_EMAIL=${{ secrets.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }} + DIRECTUS_ADMIN_PASSWORD=${{ secrets.DIRECTUS_ADMIN_PASSWORD }} + DIRECTUS_DB_NAME=${{ secrets.DIRECTUS_DB_NAME || 'directus' }} + DIRECTUS_DB_USER=${{ secrets.DIRECTUS_DB_USER || 'directus' }} + DIRECTUS_DB_PASSWORD=${{ secrets.DIRECTUS_DB_PASSWORD }} + DIRECTUS_KEY=${{ secrets.DIRECTUS_KEY }} + DIRECTUS_SECRET=${{ secrets.DIRECTUS_SECRET }} + EOF + + # Sync docker-compose + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + docker compose -p "${{ needs.prepare.outputs.project_name }}" --env-file ${{ needs.prepare.outputs.env_file }} pull + docker compose -p "${{ needs.prepare.outputs.project_name }}" --env-file ${{ needs.prepare.outputs.env_file }} up -d --remove-orphans + docker system prune -f --filter "until=24h" - - name: 🔔 Gotify Notification (Failure) - if: failure() + notifications: + name: 🔔 Notifications + needs: [prepare, deploy] + if: always() + runs-on: docker + steps: + - name: Notify Gotify run: | - echo "Sending failure notification to Gotify..." - RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -F "title=❌ Deployment Failed: ${{ github.repository }}" \ - -F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) failed! - - Commit: ${{ github.sha }} - Actor: ${{ github.actor }} - Run ID: ${{ github.run_id }} - - Please check the logs for details." \ - -F "priority=8") + STATUS="${{ needs.deploy.result }}" + COLOR="info" + [[ "$STATUS" == "success" ]] && PRIORITY=5 || 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 + curl -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ + -F "title=mb-grid-solutions Deployment" \ + -F "message=Status: $STATUS for ${{ needs.prepare.outputs.target }} (${{ needs.prepare.outputs.image_tag }})" \ + -F "priority=$PRIORITY" diff --git a/.gitignore b/.gitignore index d51bbfe..95f2969 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +.pnpm-store + node_modules dist dist-ssr diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..9ef41ae --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +pnpm commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..cb2c84d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 0000000..47e593e --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,11 @@ +const path = require('path'); + +const buildEslintCommand = (filenames) => + `next lint --fix --file ${filenames + .map((f) => path.relative(process.cwd(), f)) + .join(' --file ')}`; + +module.exports = { + '*.{js,jsx,ts,tsx}': [buildEslintCommand, 'prettier --write'], + '*.{json,md,css,scss}': ['prettier --write'], +}; diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..692c4b1 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +@mintel:registry=https://npm.infra.mintel.me/ +registry=https://npm.infra.mintel.me/ +//npm.infra.mintel.me/:_authToken=${NPM_TOKEN} +always-auth=true diff --git a/Dockerfile b/Dockerfile index 93e2b3a..bf5ff2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,36 @@ -# Build Stage -FROM node:20-slim AS build +# Start from the pre-built Nextjs Base image +FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder WORKDIR /app -COPY package*.json ./ -RUN npm ci +# Build-time environment variables for Next.js +ARG NEXT_PUBLIC_BASE_URL +ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID +ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL +ARG NEXT_PUBLIC_TARGET +ARG DIRECTUS_URL +ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL +ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID +ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL +ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET +ENV DIRECTUS_URL=$DIRECTUS_URL + +# Copy local files COPY . . -# Build Application -RUN npm run build - -# Runtime Stage -FROM node:20-slim +# Build the specific application +RUN pnpm build +# Production runner image +FROM registry.infra.mintel.me/mintel/runtime:latest AS runner WORKDIR /app -# Copy necessary files for production -COPY --from=build /app/package*.json ./ -COPY --from=build /app/.next ./.next -COPY --from=build /app/public ./public -COPY --from=build /app/node_modules ./node_modules +# Copy standalone output and static files +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -EXPOSE 3000 +USER nextjs -CMD ["npm", "start"] +CMD ["node", "server.js"] diff --git a/app/agb/page.tsx b/app/[locale]/agb/page.tsx similarity index 75% rename from app/agb/page.tsx rename to app/[locale]/agb/page.tsx index 9b8b361..4cf7be5 100644 --- a/app/agb/page.tsx +++ b/app/[locale]/agb/page.tsx @@ -1,44 +1,50 @@ -import { Download } from 'lucide-react'; -import fs from 'fs'; -import path from 'path'; +import { Download } from "lucide-react"; +import fs from "fs"; +import path from "path"; export default function AGB() { - const filePath = path.join(process.cwd(), 'context/agbs.md'); - const fileContent = fs.readFileSync(filePath, 'utf8'); + const filePath = path.join(process.cwd(), "context/agbs.md"); + const fileContent = fs.readFileSync(filePath, "utf8"); // Split by double newlines to get major blocks (headers + their first paragraphs, or subsequent paragraphs) - const blocks = fileContent.split(/\n\s*\n/).map(b => b.trim()).filter(b => b !== ''); - - const title = blocks[0] || 'Liefer- und Zahlungsbedingungen'; - const stand = blocks[1] || 'Stand Januar 2026'; - + const blocks = fileContent + .split(/\n\s*\n/) + .map((b) => b.trim()) + .filter((b) => b !== ""); + + const title = blocks[0] || "Liefer- und Zahlungsbedingungen"; + const stand = blocks[1] || "Stand Januar 2026"; + const sections: { title: string; content: string[] }[] = []; let currentSection: { title: string; content: string[] } | null = null; // Skip title and stand - blocks.slice(2).forEach(block => { - const lines = block.split('\n').map(l => l.trim()).filter(l => l !== ''); + blocks.slice(2).forEach((block) => { + const lines = block + .split("\n") + .map((l) => l.trim()) + .filter((l) => l !== ""); if (lines.length === 0) return; const firstLine = lines[0]; - + if (/^\d+\./.test(firstLine)) { // New section if (currentSection) sections.push(currentSection); - + currentSection = { title: firstLine, content: [] }; - + // If there are more lines in this block, they form the first paragraph(s) if (lines.length > 1) { // Join subsequent lines as they might be part of the same paragraph // In this MD, we'll assume lines in the same block belong together // unless they are clearly separate paragraphs (but we already split by double newline) - const remainingText = lines.slice(1).join(' '); + const remainingText = lines.slice(1).join(" "); if (remainingText) currentSection.content.push(remainingText); } } else if (currentSection) { // Continuation of current section - const blockText = lines.join(' '); + const blockText = lines.join(" "); if (blockText) currentSection.content.push(blockText); } }); @@ -49,7 +55,7 @@ export default function AGB() { if (sections.length > 0) { const lastSection = sections[sections.length - 1]; if (lastSection.content.includes(footer) || lastSection.title === footer) { - lastSection.content = lastSection.content.filter(c => c !== footer); + lastSection.content = lastSection.content.filter((c) => c !== footer); if (sections[sections.length - 1].title === footer) { sections.pop(); } @@ -57,12 +63,14 @@ export default function AGB() { } return ( -
+
-

{title}

+

+ {title} +

{stand}

- +
{sections.map((section, index) => (
-

{section.title}

+

+ {section.title} +

{section.content.map((paragraph, pIndex) => (

{paragraph}

diff --git a/app/[locale]/datenschutz/page.tsx b/app/[locale]/datenschutz/page.tsx new file mode 100644 index 0000000..42c7088 --- /dev/null +++ b/app/[locale]/datenschutz/page.tsx @@ -0,0 +1,66 @@ +export default function Privacy() { + return ( +
+
+
+

+ Datenschutzerklärung +

+ +
+
+

+ 1. Datenschutz auf einen Blick +

+

+ Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir + behandeln Ihre personenbezogenen Daten vertraulich und + entsprechend der gesetzlichen Datenschutzvorschriften sowie + dieser Datenschutzerklärung. +

+
+ +
+

+ 2. Hosting +

+

+ Unsere Website wird bei Hetzner Online GmbH gehostet. Der + Serverstandort ist Deutschland. Wir haben einen Vertrag über + Auftragsverarbeitung (AVV) mit Hetzner geschlossen. +

+
+ +
+

+ 3. Kontaktformular +

+

+ Wenn Sie uns per Kontaktformular Anfragen zukommen lassen, + werden Ihre Angaben aus dem Anfrageformular inklusive der von + Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der + Anfrage und für den Fall von Anschlussfragen bei uns + gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung + weiter. +

+
+ +
+

+ 4. Server-Log-Dateien +

+

+ Der Provider der Seiten erhebt und speichert automatisch + Informationen in sogenannten Server-Log-Dateien, die Ihr Browser + automatisch an uns übermittelt. Dies sind: Browsertyp und + Browserversion, verwendetes Betriebssystem, Referrer URL, + Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage, + IP-Adresse. +

+
+
+
+
+
+ ); +} diff --git a/app/error.tsx b/app/[locale]/error.tsx similarity index 75% rename from app/error.tsx rename to app/[locale]/error.tsx index a7c6cef..a53ac43 100644 --- a/app/error.tsx +++ b/app/[locale]/error.tsx @@ -1,9 +1,9 @@ -'use client'; +"use client"; -import { useEffect } from 'react'; -import { motion } from 'framer-motion'; -import { RefreshCcw, Home } from 'lucide-react'; -import Link from 'next/link'; +import { useEffect } from "react"; +import { motion } from "framer-motion"; +import { RefreshCcw, Home } from "lucide-react"; +import Link from "next/link"; export default function Error({ error, @@ -27,17 +27,19 @@ export default function Error({ > 500 - + -

Etwas ist schiefgelaufen

+

+ Etwas ist schiefgelaufen +

Es gab ein technisches Problem. Wir arbeiten bereits an der Lösung.

- +
- + Zur Startseite diff --git a/app/impressum/page.tsx b/app/[locale]/impressum/page.tsx similarity index 51% rename from app/impressum/page.tsx rename to app/[locale]/impressum/page.tsx index beb5428..4f59e05 100644 --- a/app/impressum/page.tsx +++ b/app/[locale]/impressum/page.tsx @@ -1,11 +1,11 @@ -'use client'; +"use client"; -import { motion } from 'framer-motion'; -import { TechBackground } from '@/components/TechBackground'; +import { motion } from "framer-motion"; +import { TechBackground } from "@/components/TechBackground"; export default function Legal() { return ( -
+
- -

Impressum

- + +

+ Impressum +

+
-

Angaben gemäß § 5 TMG

+

+ Angaben gemäß § 5 TMG +

- MB Grid Solutions & Services GmbH
- Raiffeisenstraße 22
+ MB Grid Solutions & Services GmbH +
+ Raiffeisenstraße 22 +
73630 Remshalden

-

Vertreten durch

+

+ Vertreten durch +

- Michael Bodemer
+ Michael Bodemer +
Klaus Mintel

@@ -40,24 +49,48 @@ export default function Legal() {
-

Registereintrag

+

+ Registereintrag +

- Eintragung im Handelsregister.
- Registergericht: Amtsgericht Stuttgart
+ Eintragung im Handelsregister. +
+ Registergericht: Amtsgericht Stuttgart +
Registernummer: HRB 803379

-

Urheberrecht

+

+ Urheberrecht +

- Alle auf der Website veröffentlichten Texte, Bilder und sonstigen Informationen unterliegen – sofern nicht anders gekennzeichnet – dem Urheberrecht. Jede Vervielfältigung, Verbreitung, Speicherung, Übermittlung, Wiedergabe bzw. Weitergabe der Inhalte ohne schriftliche Genehmigung ist ausdrücklich untersagt. + Alle auf der Website veröffentlichten Texte, Bilder und + sonstigen Informationen unterliegen – sofern nicht anders + gekennzeichnet – dem Urheberrecht. Jede Vervielfältigung, + Verbreitung, Speicherung, Übermittlung, Wiedergabe bzw. + Weitergabe der Inhalte ohne schriftliche Genehmigung ist + ausdrücklich untersagt.

diff --git a/app/kontakt/page.tsx b/app/[locale]/kontakt/page.tsx similarity index 62% rename from app/kontakt/page.tsx rename to app/[locale]/kontakt/page.tsx index 97bccac..73b9ead 100644 --- a/app/kontakt/page.tsx +++ b/app/[locale]/kontakt/page.tsx @@ -3,7 +3,8 @@ import ContactContent from "@/components/ContactContent"; export const metadata: Metadata = { title: "Kontakt", - description: "Haben Sie Fragen zu einem Projekt oder benötigen Sie technische Beratung? Wir freuen uns auf Ihre Nachricht.", + description: + "Haben Sie Fragen zu einem Projekt oder benötigen Sie technische Beratung? Wir freuen uns auf Ihre Nachricht.", }; export default function Page() { diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..e4afc97 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,123 @@ +import Layout from "@/components/Layout"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "../globals.css"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import { notFound } from "next/navigation"; + +const inter = Inter({ + subsets: ["latin"], + display: "swap", + variable: "--font-inter", +}); + +export const metadata: Metadata = { + metadataBase: new URL("https://www.mb-grid-solutions.com"), + title: { + default: "MB Grid Solutions | Energiekabelprojekte & Technische Beratung", + template: "%s | MB Grid Solutions", + }, + description: + "Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV. Expertise in Mittel- und Hochspannungsnetzen.", + keywords: [ + "Energiekabel", + "Hochspannung", + "Mittelspannung", + "Kabelprojekte", + "Technische Beratung", + "Engineering", + "Energiewende", + "110 kV", + ], + authors: [{ name: "MB Grid Solutions & Services GmbH" }], + creator: "MB Grid Solutions & Services GmbH", + publisher: "MB Grid Solutions & Services GmbH", + formatDetection: { + email: false, + address: false, + telephone: false, + }, + openGraph: { + type: "website", + locale: "de_DE", + url: "https://www.mb-grid-solutions.com", + siteName: "MB Grid Solutions", + title: "MB Grid Solutions | Energiekabelprojekte & Technische Beratung", + description: + "Spezialisierter Partner für Energiekabelprojekte bis 110 kV. Herstellerneutrale technische Beratung und Projektbegleitung.", + }, + twitter: { + card: "summary_large_image", + title: "MB Grid Solutions | Energiekabelprojekte & Technische Beratung", + description: "Spezialisierter Partner für Energiekabelprojekte bis 110 kV.", + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-video-preview": -1, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, +}; + +export default async function RootLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + + // Validate that the incoming `locale` is supported + if (locale !== "de") { + notFound(); + } + + // Providing all messages to the client + // side is the easiest way to get started + const messages = await getMessages(); + + const jsonLd = { + "@context": "https://schema.org", + "@type": "Organization", + name: "MB Grid Solutions & Services GmbH", + url: "https://www.mb-grid-solutions.com", + logo: "https://www.mb-grid-solutions.com/assets/logo.png", + description: + "Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV.", + address: { + "@type": "PostalAddress", + streetAddress: "Raiffeisenstraße 22", + addressLocality: "Remshalden", + postalCode: "73630", + addressCountry: "DE", + }, + contactPoint: { + "@type": "ContactPoint", + email: "info@mb-grid-solutions.com", + contactType: "customer service", + }, + }; + + return ( + + +