From 646562d47335d49c223a2d04f909648409f4d7af Mon Sep 17 00:00:00 2001 From: Jan Wagner Date: Mon, 31 Aug 2026 19:44:41 +0200 Subject: [PATCH] Fix: deploy target + Traefik-Labels + Next rewrites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy-vps.sh: free-warez.top → free-warez.win (eldov.win liegt auf .win, nicht .top). Vorheriger Deploy lief auf .top ohne Traefik-Routing. - docker-compose.yml: Traefik-Labels analog w-make-com (Router für eldov.win + www.eldov.win, websecure entrypoint, HTTP→HTTPS-Redirect). Service hängt am 'proxy'-Netzwerk. - next.config.ts: Rewrites als Backup zum proxy.ts (/ → /de, /projekte/[slug] → /de/projects/[slug]). In manchen Setups greift der proxy.ts nicht zuverlässig für statische Routen — doppelt hält besser. - src/app/page.tsx: Root-Route serviert DE-Inhalt direkt, damit Healthchecks und Bots eine echte Antwort bekommen (kein endloser Redirect). --- Dockerfile | 2 ++ deploy-vps.sh | 9 ++++-- docker-compose.yml | 46 +++++++++++++++++++++++++-- next.config.ts | 42 +++++++++++++++++++++++-- src/app/page.tsx | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 src/app/page.tsx diff --git a/Dockerfile b/Dockerfile index 731e9e7..923e61d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,8 @@ RUN npm ci FROM deps AS build WORKDIR /app COPY . . +ARG NEXT_PUBLIC_SITE_URL=https://eldov.win +ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL} ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build diff --git a/deploy-vps.sh b/deploy-vps.sh index 73e5d37..933ff4e 100755 --- a/deploy-vps.sh +++ b/deploy-vps.sh @@ -1,15 +1,18 @@ #!/usr/bin/env bash # deploy-vps.sh — Synchronisiert den eldov-win Workspace auf den VPS-Container. -# Host: free-warez.top · Container-Pfad: /opt/containers/eldov-portfolio +# Host: free-warez.win · Container-Pfad: /opt/containers/eldov-portfolio +# +# eldov.win läuft auf demselben VPS wie w-make.com und minecraft-portal. +# Traefik routet dort die Subdomains per Docker-Labels. # # Voraussetzungen: -# - ssh-Zugang zu free-warez.top ist konfiguriert +# - ssh-Zugang zu free-warez.win ist konfiguriert # - .env.local ist lokal vorhanden (wird NICHT übertragen) # - auf dem VPS liegt bereits ein Container namens 'eldov-portfolio' set -euo pipefail -VPS_HOST="${VPS_HOST:-free-warez.top}" +VPS_HOST="${VPS_HOST:-free-warez.win}" VPS_DIR="${VPS_DIR:-/opt/containers/eldov-portfolio}" echo "→ Prüfe SSH-Verbindung zu $VPS_HOST..." diff --git a/docker-compose.yml b/docker-compose.yml index b6a1b50..3699cdc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,22 @@ # Compose-Service für eldov.win. -# Host-übergreifend konsistent mit w-make-com: data-Volume für die SQLite-DB, -# .env für Secrets, kein externer Port. +# Host: free-warez.win (gleicher VPS-Cluster wie w-make.com). +# Traefik routet eldov.win per Docker-Labels zum Container. +# +# Auf dem VPS muss liegen: +# - Traefik-Netzwerk 'proxy' (vom traefik3-crowdsec-stack Container erstellt) +# - .env.local mit ADMIN_PASSWORD + SESSION_SECRET +# - Volume 'eldov_data' für die SQLite-DB services: app: build: context: . dockerfile: Dockerfile + args: + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://eldov.win} image: eldov-portfolio:latest container_name: eldov-portfolio + mem_limit: 512m restart: unless-stopped env_file: - .env.local @@ -16,15 +24,47 @@ services: - NODE_ENV=production - NEXT_TELEMETRY_DISABLED=1 - DB_PATH=/app/data/eldov.db + # Hinter Traefik: X-Forwarded-For für Rate-Limiting vertrauen + - TRUST_PROXY_HEADERS=true volumes: - eldov_data:/app/data + expose: + - "3000" + security_opt: + - no-new-privileges:true healthcheck: - test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok||r.status===307?0:1)).catch(()=>process.exit(1))"] interval: 30s timeout: 5s retries: 5 start_period: 30s + networks: + - default + - proxy + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.routers.eldov-win.rule=Host(`eldov.win`) || Host(`www.eldov.win`)" + - "traefik.http.routers.eldov-win.priority=100" + - "traefik.http.routers.eldov-win.entrypoints=websecure" + - "traefik.http.routers.eldov-win.tls=true" + - "traefik.http.routers.eldov-win.tls.certresolver=http_resolver" + - "traefik.http.routers.eldov-win.middlewares=default@file,crowdsec-bouncer-plugin@file" + - "traefik.http.routers.eldov-win.service=eldov-win" + - "traefik.http.services.eldov-win.loadbalancer.server.port=3000" + # HTTP→HTTPS-Redirect für die unverschlüsselte Variante (Traefik + # bringt dafür eine eingebaute redirectScheme-Middleware mit). + - "traefik.http.routers.eldov-win-http.rule=Host(`eldov.win`) || Host(`www.eldov.win`)" + - "traefik.http.routers.eldov-win-http.entrypoints=web" + - "traefik.http.routers.eldov-win-http.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" volumes: eldov_data: name: eldov_data + +networks: + proxy: + name: proxy + external: true diff --git a/next.config.ts b/next.config.ts index 7706d42..752d6ac 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,28 @@ import type { NextConfig } from "next"; +import { defaultLocale, locales } from "./src/i18n/routes"; + +const PUBLIC_TO_INTERNAL: Record = { + "/": "/de", + "/ueber-mich": "/de/about", + "/projekte": "/de/projects", + "/kontakt": "/de/contact", + "/impressum": "/de/legal", + "/datenschutz": "/de/privacy", + "/en": "/en", + "/en/about": "/en/about", + "/en/projects": "/en/projects", + "/en/contact": "/en/contact", + "/en/legal": "/en/legal", + "/en/privacy": "/en/privacy", +}; + +// Rewrites für Projekt-Detail: /projekte/[slug] -> /de/projects/[slug] (dynamisch). +function projectItemRewrite(srcPrefix: string, destLocale: "de" | "en") { + return { + source: `${srcPrefix}/:slug`, + destination: `/${destLocale}/projects/:slug`, + }; +} const config: NextConfig = { // better-sqlite3 ist ein nativer Server-only Import. Wir halten ihn aus dem @@ -6,10 +30,22 @@ const config: NextConfig = { // Komponenten verwenden — keine externen Imports in `src/app/**/page.tsx` // ohne expliziten server boundary. serverExternalPackages: ["better-sqlite3"], - // Statische Assets lokal ausliefern, kein CDN. - images: { - unoptimized: true, + images: { unoptimized: true }, + // Rewrites als Backup zum proxy.ts (manche Next.js 16 Setups greifen den + // proxy nicht konsistent für statische Routen). Der proxy.ts macht + // zusätzlich den Locale-Header-Set und Auth — wir behalten beides. + async rewrites() { + return [ + ...Object.entries(PUBLIC_TO_INTERNAL).map(([from, to]) => ({ + source: from, + destination: to, + })), + projectItemRewrite("/projekte", "de"), + projectItemRewrite("/en/projects", "en"), + ]; }, + // Traefik vertraut uns — wir akzeptieren X-Forwarded-For. + trustHostHeader: true, }; export default config; diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..01b8a49 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,77 @@ +// Root-`page.tsx` — fängt `/` ab und serviert den DE-Home-Inhalt direkt. +// Wir duplizieren hier die Logik aus [lang]/page.tsx, weil Next.js 16 ohne +// explizite Root-Route 404 für `/` zurückgibt (der [lang]-Proxy-Rewrite greift +// bei Direktzugriffen ohne Hostname-Rewrite wie z.B. Healthchecks nicht). +import { headers } from "next/headers"; +import { getDb } from "@/lib/db"; +import { seedIfEmpty } from "@/lib/seed"; +import { getProfile, listFeaturedProjects, listProjects } from "@/lib/models"; +import { BioSection } from "@/components/bio-section"; +import { LinksSection } from "@/components/links-section"; +import { ProjectGrid } from "@/components/project-grid"; +import { JsonLd } from "@/components/json-ld"; +import { getDict } from "@/i18n/dictionaries"; +import Link from "next/link"; +import { publicPath } from "@/i18n/routes"; + +export const dynamic = "force-dynamic"; + +export default async function RootIndex() { + const db = getDb(); + seedIfEmpty(db); + const profile = getProfile(db, "de"); + const featured = listFeaturedProjects(db, "de"); + const all = listProjects(db, "de"); + const dict = getDict("de"); + + return ( + <> + + +
+

{dict.home.kicker}

+

+ {dict.home.title} +

+

{dict.home.lede}

+
+ +
+
+ {featured.length > 0 ? ( + <> +
+ ~/featured +

{dict.home.featuredLabel}

+
+ + + ) : null} +
+ ~/all +

{dict.home.allProjectsLabel}

+ + {dict.actions.viewProjects} → + +
+ +
+ +
+ +
+

{dict.home.cta.title}

+

{dict.home.cta.body}

+
+ + ); +}