diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 837a1a0..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..7b4786a --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["dev"], + "port": 4321 + }, + { + "name": "preview", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["preview"], + "port": 4321 + } + ] +} diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..839e694 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,117 @@ +name: Deploy Marketing-Site + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-marketing + cancel-in-progress: false + +jobs: + test: + name: Lint + Smoke-Tests + runs-on: docker + container: + image: node:22-bookworm + steps: + - uses: actions/checkout@v4 + + - name: pnpm aktivieren + run: | + corepack enable + corepack prepare pnpm@latest --activate + + - name: Dependencies + run: pnpm install --frozen-lockfile + + - name: Production-Build + run: pnpm build + + - name: Playwright-Browser + run: pnpm exec playwright install --with-deps chromium + + - name: Smoke-Tests gegen Production-Build + run: pnpm exec playwright test + env: + CI: '1' + + - name: Build-Artefakt aufheben + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ + retention-days: 3 + + deploy: + name: Deploy auf Marketing-VPS + runs-on: docker + container: + image: alpine:3.20 + needs: test + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Tools installieren + run: | + apk add --no-cache rsync openssh-client + + - name: Build-Artefakt holen + uses: actions/download-artifact@v3 + with: + name: dist + path: dist/ + + - name: SSH-Key setzen + run: | + mkdir -p ~/.ssh + echo "${{ secrets.MARKETING_SSH_KEY }}" > ~/.ssh/marketing + chmod 600 ~/.ssh/marketing + ssh-keyscan -H "${{ secrets.MARKETING_HOST }}" >> ~/.ssh/known_hosts + + - name: Rsync zu Marketing-VPS + run: | + rsync -avz --delete \ + -e "ssh -i ~/.ssh/marketing -o StrictHostKeyChecking=yes" \ + dist/ \ + "${{ secrets.MARKETING_USER }}@${{ secrets.MARKETING_HOST }}:slimcore.io/" + + - name: Deploy-Verifikation + run: | + # Caddy braucht keine Reload — file_server liest live aus dem Verzeichnis + # Stattdessen: HTTPS-Check, dass die neue Version live ist + sleep 3 + STATUS=$(wget -qO- --server-response https://slimcore.io/ 2>&1 | awk '/HTTP\//{print $2}' | head -1) + if [ "$STATUS" != "200" ]; then + echo "Production-Site liefert HTTP $STATUS, erwartet 200" + exit 1 + fi + echo "✓ slimcore.io antwortet mit 200" + + notify: + name: Deploy-Notification + runs-on: docker + container: + image: alpine:3.20 + needs: deploy + if: always() + steps: + - name: Status-Mail an Pascal + if: ${{ secrets.BREVO_API_KEY != '' }} + run: | + apk add --no-cache curl + STATUS="${{ needs.deploy.result }}" + SUBJECT="[slimcore.io] Deploy ${STATUS}" + BODY="Deploy von ${{ github.sha }} auf slimcore.io: ${STATUS}\n\nCommit: ${{ github.event.head_commit.message }}\nWorkflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + curl -X POST https://api.brevo.com/v3/smtp/email \ + -H "api-key: ${{ secrets.BREVO_API_KEY }}" \ + -H "Content-Type: application/json" \ + -d "{ + \"sender\": {\"email\": \"deploy@digiformer.net\", \"name\": \"Forgejo Deploy\"}, + \"to\": [{\"email\": \"pascal.oelmann@digiformer.net\"}], + \"subject\": \"$SUBJECT\", + \"textContent\": \"$BODY\" + }" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc4cd81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules/ +dist/ +.astro/ +.DS_Store +*.local + +# Test artefacts +test-results/ +playwright-report/ +playwright/.cache/ + +# Editor / OS +.idea/ +.vscode/ +Thumbs.db diff --git a/.gitmodules b/.gitmodules index 3f0fd10..bfade13 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "docs/brand-system"] path = docs/brand-system - url = git@github.com:digiformer/brand-system.git + url = https://forge.digiformer.eu/digiformer/brand-system.git diff --git a/CLAUDE.md b/CLAUDE.md index 1538bbe..b4ab02d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ ## 0. TL;DR für Claude Code -- **Stack:** Astro 5 (Static Site, SEO-first) + Tailwind v4 + selektive React-Islands für interaktive Komponenten + shadcn/ui-Primitives + TypeScript strict. +- **Stack:** Astro 6 (Static Site, SEO-first) + Tailwind v4 + selektive React-Islands für interaktive Komponenten + shadcn/ui-Primitives + TypeScript strict. - **Domain:** `slimcore.io` = Marketing. `app.slimcore.io` = SaaS-App (separates Repo, nicht Teil dieses Briefs). - **Positionierung:** Primärgruppe sind Solo-Selbstständige und kleine Teams (1–10). Sekundär wachsende KMU bis ~30. Tertiär eCommerce-Händler. Hero-Linie: **„Schlank starten. Grenzenlos wachsen."** - **Familienzugehörigkeit:** SlimCore ist eine eigenständige Produktmarke unter dem digiFORMER-Dach (Adobe-Modell, nicht Atlassian-Modell). Familien-Anker sind Typografie + nummerierte Sektionen + Status-Glyphen — NICHT die Farbe. Jede Marke hat genau eine eigene Akzentfarbe. @@ -98,9 +98,10 @@ slimcore-marketing/ │ ├── favicon.svg │ ├── og-default.png ← 1200×630 OG-Bild, generiert │ └── fonts/ ← self-hosted (DSGVO!) -│ ├── source-serif-4-variable.woff2 -│ ├── inter-variable.woff2 -│ └── jetbrains-mono-variable.woff2 +│ ├── outfit-variable.woff2 +│ ├── outfit-variable-latin-ext.woff2 +│ ├── jetbrains-mono-variable.woff2 +│ └── jetbrains-mono-variable-latin-ext.woff2 ├── src/ │ ├── styles/ │ │ └── global.css ← Tailwind v4 @theme + Reset @@ -149,7 +150,7 @@ slimcore-marketing/ ### 4.1 Astro -- Astro 5.x, Output-Mode `static`. +- Astro 6.x, Output-Mode `static`. - `@astrojs/react` für React-Islands. - `@astrojs/sitemap` für `sitemap-index.xml`. - `@astrojs/mdx` für `/content/notizen` (Phase 2). @@ -189,13 +190,12 @@ Kein Card-Component, kein Accordion, kein Carousel. Marketing-Cards bauen wir se ### 4.6 Schriftarten — self-hosted (DSGVO!) -Keine Google Fonts CDN. Alle Fonts liegen in `public/fonts/` und werden über `@font-face` mit `font-display: swap` geladen. +Keine Google Fonts CDN. Alle Fonts liegen in `public/fonts/` und werden über `@font-face` mit `font-display: swap` geladen. Latin + Latin-Ext jeweils per `unicode-range` getrennt, damit Latin-Ext nur bei Bedarf geladen wird. -- **Serif:** Source Serif 4 Variable (SIL OFL, kostenlos) -- **Sans:** Inter Variable (SIL OFL, kostenlos) -- **Mono:** JetBrains Mono Variable (SIL OFL, kostenlos) +- **Headlines + Body:** Outfit Variable (SIL OFL, kostenlos) — geometrisch-modern, single-font für die ganze Marke +- **Mono:** JetBrains Mono Variable (SIL OFL, kostenlos) — Eyebrows, Stack-Strip, Wortmarke, Sektions-Nummern, Status-Labels -Alternativen falls Pascal anderen Charakter wünscht: Newsreader (Serif, leicht editorial-warm), Geist Sans/Mono (modern-technisch), GT Sectra (Paid, premium). +**Total:** 102 KB (32 KB Outfit Latin + 15 KB Outfit Latin-Ext + 40 KB JBM Latin + 15 KB JBM Latin-Ext). Brand-System §4.1 Mai-2026-Update: Outfit ersetzt Source Serif 4 + Inter familienweit. --- @@ -274,10 +274,10 @@ Alternativen falls Pascal anderen Charakter wünscht: Newsreader (Serif, leicht ### 5.3 Typografie ```css -/* Font-Stacks */ ---font-serif: "Source Serif 4", Georgia, serif; ---font-sans: "Inter", system-ui, sans-serif; +/* Font-Stacks — Outfit für Headlines + Body, JetBrains Mono für technische Marker */ +--font-sans: "Outfit", system-ui, sans-serif; --font-mono: "JetBrains Mono", ui-monospace, monospace; +--font-serif: var(--font-sans); /* Legacy-Alias, zeigt auf Outfit */ /* Type-Scale */ --text-eyebrow: 0.6875rem; /* 11px, mono, uppercase, letter-spacing 0.08em */ @@ -292,10 +292,10 @@ Alternativen falls Pascal anderen Charakter wünscht: Newsreader (Serif, leicht **Regeln:** -- Headlines (`h1`, `h2`): Serif, weight 500 (NICHT 600 oder 700), `letter-spacing: -0.015em` für Display-Größen, `line-height: 1.1–1.2`. -- Body: Sans, weight 400, `line-height: 1.65`. -- Eyebrows: Mono, 11px, uppercase, `letter-spacing: 0.08em`, Tertiärfarbe. -- Status-Dots, Section-Numbern (`01`–`04`), Tech-Stack-Marker: Mono. +- Headlines (`h1`, `h2`): Sans (Outfit), weight 500 (NICHT 600 oder 700), `letter-spacing: -0.01em` bei H1+, `line-height: 1.10–1.20`. Bei Hero-Highlight (Background-Stempel) darf `font-weight: 550` genutzt werden, um optische Verdünnung dunkler Schrift auf Persimmon auszugleichen. +- Body: Sans (Outfit), weight 400, `line-height: 1.65`. +- Eyebrows: Mono (JetBrains Mono), 11px, uppercase, `letter-spacing: 0.08em`, Tertiärfarbe. +- Status-Dots, Section-Numbern (`01`–`04`), Tech-Stack-Marker, Wortmarke: Mono. - Sentence case überall. Niemals Title Case. Niemals ALL CAPS außer Mono-Eyebrows. - Maximale Textbreite für Fließtext: 60–70 Zeichen (`max-w-[65ch]`). @@ -335,7 +335,7 @@ Custom-Komponenten (alle in `src/components/marketing/`): - **`RoadmapTimeline`** — vertikale Linie mit 4 Phasen (Heute / Q3-Q4 2026 / 2027 / Vision). Jede Phase = Card mit Liste der Items. - **`ObjectionAnswer`** — Frage als Serif-Quote, Antwort als Body-Text. 4 davon im Grid. - **`CTABlock`** — Schluss-Sektion mit Headline + zwei Buttons + E-Mail-Link. -- **`NavBar`** — sticky-on-scroll mit minimaler Mode-Reduktion (kein Background-Wechsel-Schauspiel). +- **`NavBar`** — sticky-on-scroll. Standardmäßig auf hellem Body-Hintergrund. **Auf der Home-Seite bei Scroll-Position 0 läuft die NavBar visuell IM dunklen Hero mit** (gleicher `#0E0F14`-Hintergrund, Logo und Links in Off-White). Sobald der Tech-Strip vorbei ist und der helle Body-Bereich beginnt, wechselt die NavBar in den hellen Modus zurück. Implementierung: NavBar als `position: sticky` mit `top: 0`, der Hero-Hintergrund läuft hinter der transparenten NavBar durch — beim Scrollen wird die NavBar dann von der hellen Body-Sektion „ausgewaschen". Alternative-Implementierung: NavBar wechselt Hintergrund-Farbe per IntersectionObserver, sobald der Hero den Viewport verlässt. Beide Wege sind ok, Hauptsache: kein Hard-Cut zwischen NavBar und Hero-Hintergrund. - **`Footer`** — 4-Spalten, dezente Border-Top, Akzent nur bei Hover-Links. ### 5.7 Buttons @@ -393,7 +393,7 @@ Keine `rounded-full`-Mega-Pills. `rounded-md` (6–8px) reicht. Padding: `px-5 p **Mobile:** Hamburger → Drawer (Dialog von rechts). Link-Liste, gleiche Struktur. -**Sticky-Verhalten:** NavBar bleibt sticky on scroll, aber subtil — `border-b` erscheint nur wenn `scrollY > 8px`, kein Background-Wechsel. +**Sticky-Verhalten:** NavBar ist sticky on scroll. Auf der Home-Seite ist sie bei `scrollY=0` im dunklen Hero-Modus (siehe NavBar-Komponenten-Spec in §5.6); der Übergang zum hellen Modus passiert beim Verlassen des Hero-Bereichs. Der `border-b` erscheint nur im hellen Modus und nur bei `scrollY > 8px`. ### 6.3 Footer @@ -412,16 +412,69 @@ Bottom-Bar: `© 2026 SlimCore — ein Produkt der digiFORMER GmbH · Impressum ### 7.1 `/` (Home) — der Reihe nach -**1. Hero** -- Eyebrow (Mono): „GESCHÄFTSSOFTWARE FÜR SOLO-SELBSTSTÄNDIGE UND KLEINE TEAMS" -- H1 (Serif, Display): „Schlank starten.
Grenzenlos wachsen." - - „Grenzenlos" optional in Persimmon-Akzent als hervorgehobenes Wort (siehe §5.2 Akzent-Verwendungs-Regeln — sparsam!) - - Alternativ-Varianten für späteren A/B-Test: „Klein und schlank. Auch wenn Sie wachsen." / „Eine Plattform für alles, was nicht Buchhaltung ist." -- Lead (17px, Sans, secondary): „SlimCore ist die schlanke Geschäftssoftware für Solo-Selbstständige und kleine Teams. Sie aktivieren am Anfang nur, was Sie heute brauchen — meistens CRM, Belege und Aufgaben. Wenn aus Ihnen drei werden, dann fünfzehn, schalten Sie weitere Module zu, ohne zu wechseln." -- CTAs: `[Tester werden →]` (primary) `[Module ansehen →]` (secondary) +**1. Hero — DUNKLER HERO MIT PERSIMMON-HIGHLIGHT (verbindlich)** -**2. Tech-Strip** (direkt unter Hero, voll Container-Breite, `border-y`) -- Mono, 11px: `STACK · PostgreSQL · PostgREST · Docker · Traefik · Hetzner Falkenstein / Nürnberg · DSGVO · ZUGFeRD 2.0 · DATEV` +> **Diese Spezifikation ist verbindlich, nicht optional.** Frühere Brief-Versionen ließen die Hero-Variante offen, was zu einer hellen Standard-Implementierung geführt hat. Die finale Entscheidung ist die dunkle Variante mit Background-Highlight auf „Grenzenlos". Bitte nicht zur hellen Variante zurückrationalisieren — die dunkle Variante ist Teil der Marken-Persönlichkeit „seriös, aber Revoluzer", und der Bruch zwischen dunklem Hero und hellen Folge-Sektionen ist gewolltes Lese-Architektur-Element. + +**Hintergrund und Farben:** +- Hero-Sektion: Vollflächiger dunkler Hintergrund `#0E0F14` (fast-schwarz, leicht kühler Stich — nicht reines Schwarz) +- Headline-Text: `#F5F5F0` (warmes Off-White, kein reines Weiß — passt zum cremefarbenen Body-Hintergrund der restlichen Site) +- NavBar in der Hero-Region läuft mit dunklem Hintergrund mit; Logo, Nav-Links und Sprachwahl in Off-White mit reduzierter Opacity (0.65–0.85). Übergang zum hellen Body unterhalb des Tech-Strips. +- Hero hat KEIN Border, KEIN Frame — nahtlos voll-bleed in die Container-Breite + +**Eyebrow:** +- Text: `▸ GESCHÄFTSSOFTWARE FÜR SOLO-SELBSTSTÄNDIGE UND KLEINE TEAMS` +- Schrift: JetBrains Mono, 11px, weight 500, letter-spacing 0.10em +- Farbe: `#FF6B2C` (Electric Persimmon — auf dunklem Hintergrund signalstark, nicht „dezent") +- Das vorangestellte `▸` ist verbindlich (nicht ◆, nicht •) — kleines visuelles Wiedererkennungs-Element + +**H1 (Headline):** +- Text auf zwei Zeilen: „Schlank starten." auf Zeile 1, „Grenzenlos wachsen." auf Zeile 2 +- Schrift: Outfit, weight 500, line-height 1.18, letter-spacing -0.015em +- Größe: clamp(38px, 5vw, 56px) — responsive, aber nicht kleiner als 38px im Hero +- Farbe: `#F5F5F0` für „Schlank starten." und „wachsen." +- **Das Wort „Grenzenlos" ist verbindlich als Background-Highlight gesetzt:** + - Background: `#FF6B2C` (Electric Persimmon) + - Vordergrund-Text auf dem Highlight: `#2A0F02` (dunkles Persimmon-Schwarz, nicht reines Schwarz) + - Padding: `0.04em 0.2em 0.08em` (em-relativ, skaliert mit der Schriftgröße) + - `display: inline-block`, `line-height: 0.95`, `font-weight: 550`, `vertical-align: baseline` — der Block hugt die Buchstaben eng, kompensiert die optische Verdünnung dunkler Schrift auf hellem Persimmon + - KEINE rounded corners (border-radius 0) — das Persimmon-Rechteck soll wie ein Marker-Stempel wirken, nicht wie ein Pill-Button + +**Lead-Paragraph:** +- Text: „Die schlanke Geschäftssoftware für Solo-Selbstständige und kleine Teams. Sie aktivieren am Anfang nur, was Sie heute brauchen — meistens CRM, Belege und Aufgaben. Wenn aus Ihnen drei werden, dann fünfzehn, schalten Sie weitere Module zu, ohne zu wechseln." +- Schrift: Outfit, 16–17px, weight 400, line-height 1.6 +- Farbe: `rgba(245, 245, 240, 0.75)` (75% Opacity des Off-Whites — sekundäre Lesbarkeit ohne Kontrast-Verlust) +- Maximale Breite: ~540px (verhindert dass der Lead über die volle Container-Breite läuft) + +**CTAs:** +- Primary „Tester werden →": Background `#FF6B2C`, Text `#2A0F02` (NICHT weiß — Kontrast-Grund, siehe Brand-System §3.7), border-radius `var(--border-radius-md)`, padding `11px 20px`, font-size 13px, weight 500 +- Secondary „Module ansehen →": Background transparent, border `0.5px solid rgba(245,245,240,0.5)`, Text `#F5F5F0`, sonst gleich + +**Vertikales Padding:** +- Hero-Sektion: `60px` oben, `48px` unten (Desktop). Auf Mobile 40px/32px. + +**Übergang zur nächsten Sektion (Tech-Strip):** +- Tech-Strip läuft VISUELL noch im dunklen Hintergrund weiter (gleiches `#0E0F14`), nicht im hellen +- Border-top und border-bottom des Tech-Strips: `0.5px solid rgba(245,245,240,0.08)` (sehr dezent) +- Mono-Text im Strip in `rgba(245,245,240,0.55)`, „STACK"-Label in `rgba(245,245,240,0.85)` +- Erst die Modul-Landschaft (Sektion 3) bricht in den hellen Hintergrund — dieser Bruch ist die gewollte Lese-Architektur + +**Was NICHT passieren darf:** +- Kein heller Hintergrund im Hero +- Kein Persimmon-Wort ohne Background-Highlight (nur Wort-Farbe wäre die helle-Variante-Lösung — hier ist Background-Highlight verbindlich) +- Keine rounded corners auf dem Persimmon-Rechteck +- Kein weißer Text auf dem Persimmon-Highlight (Kontrast unter WCAG-AA) +- Keine Drop-Shadows, keine Glow-Effekte, keine Gradients + +**2. Tech-Strip — dunkler Hintergrund, läuft visuell vom Hero weiter** +- Hintergrund: `#0E0F14` (gleicher dunkler Wert wie Hero, kein Bruch) +- Höhe: padding `12px 32px` (vertikal niedrig, optisch eine schmale Strip-Leiste) +- Border-top und border-bottom: `0.5px solid rgba(245,245,240,0.08)` +- Inhalt: Mono, 11px, letter-spacing 0.06em, uppercase + - `STACK` als Label in `rgba(245,245,240,0.85)` + - Folgende Stack-Komponenten in `rgba(245,245,240,0.55)`, getrennt durch ` · ` (mit Spaces): + `STACK · POSTGRESQL · POSTGREST · HETZNER DE · DSGVO · ZUGFERD 2.0 · DATEV` +- Mobile: gleicher Inhalt, aber mit `flex-wrap: wrap` damit die Items in mehreren Zeilen umbrechen können **3. Modul-Landschaft (Hero-Visual)** - Eyebrow: `VIER SÄULEN · 16 MODULE` diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..fbfe88a --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,27 @@ +import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; +import sitemap from '@astrojs/sitemap'; +import mdx from '@astrojs/mdx'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + site: 'https://slimcore.io', + i18n: { + locales: ['de', 'en'], + defaultLocale: 'de', + routing: { + prefixDefaultLocale: false, + }, + }, + integrations: [ + react(), + sitemap({ + i18n: { defaultLocale: 'de', locales: { de: 'de-DE', en: 'en-US' } }, + filter: (page) => !page.includes('/dev/'), + }), + mdx(), + ], + vite: { + plugins: [tailwindcss()], + }, +}); diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..37ee5d3 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,549 @@ +# Deployment — SlimCore Marketing Site + +> **Stand:** 2026-05-04 · **Zielgruppe:** Pascal Oelmann · **Adressat:** dieselbe Marketing-Site (`slimcore.io`) plus die anderen Marken-Sites (`digiformer.eu`, `slimsafe.io`, `fonboard.io`) als Folge-Schritte +> **Pfad:** Forgejo-First. GitHub wird übersprungen — kein Übergangs-Setup, das später migriert werden muss. + +Dieses Dokument beschreibt wie die Marketing-Site live geht. Es ist Teil eines familienweiten Standards für **Tier 2 — Static Marketing-Sites** (siehe `docs/handoff.md` für die Tier-Definition). + +--- + +## 0. Voraussetzungen — ein Mal pro Familie + +Diese Phasen sind nur beim ersten Mal nötig. Spätere Marken-Sites laufen direkt zu Phase B. + +### Phase A — Forgejo-Server + Runner (~1 h) + +#### A.1 VPS bestellen + +Hetzner Cloud → neuer Server in **Falkenstein**: + +| Feld | Wert | +|---|---| +| Typ | CX22 (4 GB RAM / 2 vCPU / 40 GB SSD) | +| Image | Ubuntu 24.04 LTS | +| SSH-Key | dein Schlüssel + ed25519 (kein RSA) | +| Cloud-Init | optional — das Setup unten ist manuell | +| Backups | aktivieren (~€0,80/mo extra) | +| Standort | Falkenstein (FSN1) | +| Hostname | `forge.digiformer.eu` | +| Private Network | `slimcore-infra` (10.0.0.0/24) — falls später shared mit SlimCore-Cluster | + +Kosten: ~€4/mo + €0,80 Backups. + +#### A.2 Server härten (~10 min) + +```bash +# SSH als root +ssh root@ + +# System aktualisieren +apt update && apt upgrade -y && apt autoremove -y + +# Deploy-User +adduser --disabled-password --gecos "" deploy +usermod -aG sudo,docker deploy +mkdir -p /home/deploy/.ssh +cp ~/.ssh/authorized_keys /home/deploy/.ssh/ +chown -R deploy:deploy /home/deploy/.ssh +chmod 600 /home/deploy/.ssh/authorized_keys + +# SSH härten — root-Login deaktivieren, nur Key-Auth +sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config +sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config +systemctl reload ssh + +# Firewall +apt install -y ufw fail2ban +ufw default deny incoming +ufw default allow outgoing +ufw allow OpenSSH +ufw allow http +ufw allow https +ufw --force enable +systemctl enable --now fail2ban + +# Docker installieren +curl -fsSL https://get.docker.com | sh +usermod -aG docker deploy +``` + +#### A.3 Forgejo + Caddy + Runner via Docker Compose + +`/home/deploy/forgejo/docker-compose.yml`: + +```yaml +services: + forgejo: + image: codeberg.org/forgejo/forgejo:9 + container_name: forgejo + restart: unless-stopped + environment: + USER_UID: '1000' + USER_GID: '1000' + FORGEJO__database__DB_TYPE: postgres + FORGEJO__database__HOST: db:5432 + FORGEJO__database__NAME: forgejo + FORGEJO__database__USER: forgejo + FORGEJO__database__PASSWD_FILE: /run/secrets/db_password + FORGEJO__server__DOMAIN: forge.digiformer.eu + FORGEJO__server__ROOT_URL: https://forge.digiformer.eu/ + FORGEJO__server__SSH_DOMAIN: forge.digiformer.eu + FORGEJO__service__DISABLE_REGISTRATION: 'true' + FORGEJO__webhook__ALLOWED_HOST_LIST: 'private,*.slimcore.io,*.digiformer.eu' + secrets: + - db_password + volumes: + - ./data:/var/lib/gitea + - ./config:/etc/gitea + ports: + - '127.0.0.1:3000:3000' + - '0.0.0.0:2222:22' + depends_on: + - db + + db: + image: postgres:16-alpine + container_name: forgejo-db + restart: unless-stopped + environment: + POSTGRES_DB: forgejo + POSTGRES_USER: forgejo + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + secrets: + - db_password + volumes: + - ./pg-data:/var/lib/postgresql/data + + runner: + image: code.forgejo.org/forgejo/runner:6 + container_name: forgejo-runner + restart: unless-stopped + user: '1000:1000' + volumes: + - ./runner-data:/data + - /var/run/docker.sock:/var/run/docker.sock + command: /bin/sh -c 'sleep 5; forgejo-runner daemon' + depends_on: + - forgejo + +secrets: + db_password: + file: ./db_password.txt +``` + +```bash +# Auf dem Server +sudo -u deploy bash +cd /home/deploy/forgejo +mkdir -p data config pg-data runner-data +openssl rand -hex 24 > db_password.txt +chmod 600 db_password.txt +docker compose up -d + +# Erste Schritte +docker compose logs -f forgejo +# Browser: http://:3000 (über SSH-Tunnel oder kurz UFW öffnen) +# Setup-Wizard durchklicken: Admin-User anlegen, kein Email für jetzt +``` + +#### A.4 Caddy als TLS-Proxy + +Auf demselben Server, separat: + +`/home/deploy/caddy/docker-compose.yml`: + +```yaml +services: + caddy: + image: caddy:2-alpine + container_name: caddy + restart: unless-stopped + ports: + - '80:80' + - '443:443' + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + network_mode: host # damit Caddy localhost:3000 erreicht + +volumes: + caddy-data: + caddy-config: +``` + +`/home/deploy/caddy/Caddyfile`: + +```caddy +forge.digiformer.eu { + reverse_proxy 127.0.0.1:3000 + encode zstd gzip + header Strict-Transport-Security "max-age=31536000" +} +``` + +```bash +docker compose up -d +# DNS A-Record forge.digiformer.eu → +# Caddy holt automatisch Let's-Encrypt-Cert, ~30 s +``` + +#### A.5 Runner registrieren + +In der Forgejo-UI (`https://forge.digiformer.eu`): +- **Site Administration → Actions → Runners → Create new Runner** +- Token kopieren + +```bash +docker compose -f /home/deploy/forgejo/docker-compose.yml exec runner forgejo-runner register \ + --no-interactive \ + --instance https://forge.digiformer.eu \ + --token \ + --name forge-runner-1 \ + --labels docker:docker://node:22-bookworm,ubuntu-latest:docker://node:22-bookworm +``` + +Verifikation: in Forgejo-UI sollte der Runner als **online** erscheinen. Damit ist die Forgejo-Plattform fertig. + +#### A.6 Backup einrichten + +Cron auf Forgejo-Server (User `deploy`): + +```bash +# /home/deploy/backup-forgejo.sh +#!/bin/bash +set -euo pipefail +DATE=$(date +%Y-%m-%d-%H%M) +DEST="/home/deploy/backups/forgejo-$DATE.tar.zst" +docker exec forgejo gitea dump --type tar.zst --file - > "$DEST" +# Push to Storage Box (siehe Storage-Box-Setup unten) +rsync -e "ssh -p 23 -i /home/deploy/.ssh/storagebox" "$DEST" \ + uXXXXXX@uXXXXXX.your-storagebox.de:/home/forgejo-backups/ +# Lokale Retention: 7 Tage +find /home/deploy/backups -name "forgejo-*.tar.zst" -mtime +7 -delete +``` + +```bash +chmod +x /home/deploy/backup-forgejo.sh +crontab -e +# 0 3 * * * /home/deploy/backup-forgejo.sh >> /var/log/forgejo-backup.log 2>&1 +``` + +--- + +## Phase B — Marketing-VPS einrichten (~30 min) + +### B.1 VPS bestellen + +Wieder Hetzner Cloud, gleiche Wahl wie A.1, aber Hostname `marketing.digiformer.eu`. Standort Falkenstein. + +### B.2 Server härten + +Identisch zu A.2 — Deploy-User, Firewall, Docker. + +### B.3 Caddy installieren + +Marketing-VPS hostet alle statischen Marken-Sites über einen einzigen Caddy. + +`/home/deploy/marketing/docker-compose.yml`: + +```yaml +services: + caddy: + image: caddy:2-alpine + container_name: marketing-caddy + restart: unless-stopped + ports: + - '80:80' + - '443:443' + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ./sites:/var/www:ro + - caddy-data:/data + - caddy-config:/config + +volumes: + caddy-data: + caddy-config: +``` + +`/home/deploy/marketing/Caddyfile`: + +```caddy +# — slimcore.io — +slimcore.io, www.slimcore.io { + root * /var/www/slimcore.io + try_files {path} {path}/ /index.html + file_server + encode zstd gzip + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options nosniff + Referrer-Policy strict-origin-when-cross-origin + -Server + } + # Astro generiert /index.html, /en/index.html, etc. + # Sitemap + robots gehen direkt + handle /sitemap-*.xml { + file_server + } + handle /robots.txt { + file_server + } + # SPA-Fallback nicht nötig — alle Routen haben echte HTML-Files +} + +# — digiformer.eu — (analog, sobald migriert) +digiformer.eu, www.digiformer.eu { + root * /var/www/digiformer.eu + file_server + encode zstd gzip + header Strict-Transport-Security "max-age=31536000" +} + +# — slimsafe.io — +slimsafe.io, www.slimsafe.io { + root * /var/www/slimsafe.io + file_server + encode zstd gzip + header Strict-Transport-Security "max-age=31536000" +} + +# — fonboard.io — +fonboard.io, www.fonboard.io { + root * /var/www/fonboard.io + file_server + encode zstd gzip + header Strict-Transport-Security "max-age=31536000" +} + +# Catch-all 404 für unbekannte Hostnames +:80 { + respond 404 +} +``` + +```bash +sudo -u deploy bash +cd /home/deploy/marketing +mkdir -p sites/slimcore.io sites/digiformer.eu sites/slimsafe.io sites/fonboard.io +docker compose up -d +``` + +### B.4 SSH-Key für Forgejo-Runner → Marketing-VPS + +Forgejo's Self-hosted Runner muss per SSH auf den Marketing-VPS rsynce können. + +```bash +# Auf Forgejo-Server (deploy-user) +ssh-keygen -t ed25519 -f ~/.ssh/marketing_deploy_key -N "" +cat ~/.ssh/marketing_deploy_key.pub +``` + +```bash +# Auf Marketing-VPS (deploy-user) +adduser --disabled-password --gecos "" rsync-deploy +mkdir -p /home/rsync-deploy/.ssh +echo "" >> /home/rsync-deploy/.ssh/authorized_keys +# Restriktion: nur rsync, kein Shell-Login +echo 'command="rrsync -wo /var/www" ' > /home/rsync-deploy/.ssh/authorized_keys +chmod 700 /home/rsync-deploy/.ssh +chmod 600 /home/rsync-deploy/.ssh/authorized_keys +chown -R rsync-deploy:rsync-deploy /home/rsync-deploy/.ssh +# rsync-Schreibrechte auf /var/www +mkdir -p /var/www +chown -R rsync-deploy:rsync-deploy /var/www +``` + +In Forgejo-UI für das `slimcore-website`-Repo: +- **Settings → Secrets and Variables → Actions → New Secret**: + - `MARKETING_SSH_KEY`: privater Schlüsselinhalt von `~/.ssh/marketing_deploy_key` auf Forgejo-Server + - `MARKETING_HOST`: IP oder DNS des Marketing-VPS + - `MARKETING_USER`: `rsync-deploy` + +--- + +## Phase C — Slimcore-Website Repo deployen + +### C.1 Repo nach Forgejo migrieren + +```bash +# Lokal, im slimcore-website-Folder +git remote add forgejo git@forge.digiformer.eu:digiformer/slimcore-website.git +git push --mirror forgejo +# Damit ist alles drüben — Branches, Tags, History +``` + +In Forgejo-UI: +- Repo `digiformer/slimcore-website` ist nun gespiegelt +- **Repository → Settings → Mirror Settings**: aus, weil unsere Quelle jetzt Forgejo ist + +### C.2 Workflow-Datei + +In diesem Repo bereits vorhanden: `.forgejo/workflows/deploy.yml` (siehe diese Datei für Details). Der Workflow wird automatisch erkannt, sobald das Repo in Forgejo liegt. + +Trigger: +- `push` auf `main` → Test, Build, Deploy auf Marketing-VPS unter `slimcore.io` + +### C.3 DNS-Records setzen + +Beim Domain-Registrar (idealerweise INWX, Brand-System §7.3 Greylist): + +| Record | Typ | Ziel | +|---|---|---| +| `slimcore.io` | A | `` | +| `www.slimcore.io` | A | `` | +| `digiformer.eu` (später) | A | `` | +| `forge.digiformer.eu` | A | `` | +| `marketing.digiformer.eu` | A | `` (für SSH-Zugriff) | + +Caddy holt sich beim ersten Request das Let's-Encrypt-Cert automatisch. ~30 s nach DNS-Auflösung. + +### C.4 Erstes Deployment auslösen + +```bash +git checkout main +git push forgejo main +``` + +In Forgejo-UI: +- **Actions** Tab im Repo → laufender Workflow +- Bei Erfolg ist `https://slimcore.io/` live + +### C.5 Verifikation + +Auf einem beliebigen Rechner: + +```bash +curl -I https://slimcore.io/ +# → 200 OK, HSTS-Header, server: Caddy + +curl https://slimcore.io/sitemap-index.xml | head +# → Sitemap-XML + +curl https://slimcore.io/robots.txt +# → robots.txt + +curl -I https://slimcore.io/en/ +# → 200 OK +``` + +Plus: lokale Playwright-Tests gegen Production einmal laufen lassen: + +```bash +PLAYWRIGHT_BASE_URL=https://slimcore.io pnpm exec playwright test --grep "renders" +``` + +Falls alle 16 Render-Tests grün → Site ist produktionsreif. + +--- + +## Phase D — Spätere Marken-Sites + +Sobald Phase A + B abgeschlossen, ist jede weitere Marketing-Site: + +1. Repo in Forgejo anlegen / migrieren +2. `.forgejo/workflows/deploy.yml` aus diesem Repo kopieren, `slimcore.io` durch passende Domain ersetzen +3. DNS A-Record auf Marketing-VPS-IP zeigen +4. Caddyfile-Block auf Marketing-VPS ergänzen + `caddy reload` + +Das ist ~15 min pro Marke. Kein neues VPS, kein neues Forgejo-Setup. + +--- + +## Phase E — SlimCore-App-Repo (cockpit) später migrieren + +Cockpit folgt demselben Prinzip aber komplexer (3 Production-VPS, Branch-Protection, secrets-Repo, Patroni-Migrations). Eigener Migrations-Plan in `cockpit/docs/infrastructure/forgejo-migration.md` — wird erst angegangen, wenn Marketing-Site stabil läuft. Forgejo + Runner sind dann schon da; nur noch Workflow-Portierung + Secrets-Re-Upload + Branch-Protection. + +--- + +## Backup-Strategie pro Komponente + +| Was | Wie | Wohin | Frequenz | +|---|---|---|---| +| Forgejo-Daten (Repos + DB + Config) | `gitea dump` → tar.zst | Hetzner Storage Box Nürnberg | Täglich 03:00 | +| Marketing-Site `/var/www/*` | Inhaltlich = Build-Output, immer aus Git wieder herstellbar | — | nicht nötig | +| Caddy-Daten (Let's-Encrypt-Certs) | Volume-Snapshot | Storage Box | Wöchentlich | +| Marketing-VPS-Config (`docker-compose.yml`, `Caddyfile`) | Im Forgejo-Repo `infra/marketing-vps/` | — | bei jedem Commit | + +**Storage Box:** Hetzner Storage Box BX21 (1 TB, Nürnberg, ~€10/mo). Ein gemeinsamer Account für alle Marken. SFTP/rsync-Zugriff. Sub-Pfade pro System: `/forgejo-backups/`, `/slimcore-app-backups/`, etc. + +--- + +## Monitoring (vorerst minimal) + +Phase 1 reicht **Uptime Kuma** auf demselben Marketing-VPS: + +```yaml +# /home/deploy/uptime-kuma/docker-compose.yml +services: + uptime: + image: louislam/uptime-kuma:1 + container_name: uptime + restart: unless-stopped + ports: + - '127.0.0.1:3001:3001' + volumes: + - ./data:/app/data +``` + +Caddy-Block dafür: + +```caddy +status.digiformer.eu { + reverse_proxy 127.0.0.1:3001 + basicauth { + pascal $2a$14$ # caddy hash-password generieren + } +} +``` + +Monitore einrichten: HTTPS-Check für jede Marken-Site + Forgejo. Bei Ausfall Mail an Pascal über Brevo SMTP. + +--- + +## Disaster Recovery — Wenn der Marketing-VPS ausfällt + +Marketing-Site ist statisch und reproduzierbar: + +1. **Neuer VPS bestellen** (Hetzner UI, ~5 min) +2. Server härten + Caddy installieren (Phase B.2 + B.3, ~20 min) +3. DNS A-Records auf neue IP umstellen (Hetzner DNS oder Registrar, ~5 min) +4. Forgejo-Workflow manuell triggern (Forgejo-UI → Actions → Run workflow) für jede Marken-Site (~5 min Build + Deploy pro Site) + +**Total RTO: ~45 min.** Kein RPO-Datenverlust, weil Build-Output reproduzierbar ist. + +Wenn der **Forgejo-VPS** ausfällt: + +1. Neuer VPS bestellen +2. Backup von Storage Box ziehen (`gitea dump`) +3. Forgejo-Container hochfahren mit dump-restore +4. Runner neu registrieren + +**Total RTO: ~1 h.** RPO: bis 24 h (letztes Backup) — aber lokale Git-Clones haben aktuelle Stände, falls nötig push-zurück. + +--- + +## Kostenbilanz Tier-2-Setup + +| Posten | €/mo | +|---|---| +| Forgejo-VPS CX22 + Backups | ~5 | +| Marketing-VPS CX22 + Backups | ~5 | +| Hetzner Storage Box BX21 (1 TB) — geteilt mit anderen Backups | ~10 | +| Domain-Registrar (INWX o.ä.) | ~1/Domain/Jahr | +| **Total Marketing-Infra** | **~20/mo** | + +Das ist fix für **alle** Marketing-Sites — egal ob 1 oder 10 Marken. + +--- + +## Was als Nächstes + +- [ ] Pascal: VPS bestellen (Forgejo + Marketing) +- [ ] Phase A durcharbeiten — Forgejo + Runner laufen, TLS aktiv +- [ ] Phase B durcharbeiten — Marketing-VPS bereit für rsync +- [ ] Phase C durcharbeiten — `slimcore-website` deployt unter `https://slimcore.io/` +- [ ] Smoke-Test, dann live +- [ ] Andere Marken-Sites einzeln nach Phase D nachziehen +- [ ] Cockpit-Migration als separater Plan später (Phase E) diff --git a/docs/handoff.md b/docs/handoff.md new file mode 100644 index 0000000..a373fd7 --- /dev/null +++ b/docs/handoff.md @@ -0,0 +1,285 @@ +# Phase 0 — Handoff + +> Stand: 2026-05-04 + +## Was funktioniert + +- **Astro 6.2.2** mit Static-Output, `@astrojs/react`, `@astrojs/sitemap`, `@astrojs/mdx` +- **Tailwind v4** via `@tailwindcss/vite` — alle Design-Tokens im `@theme inline`-Block in `src/styles/global.css` +- **Self-hosted Fonts** in `public/fonts/`: Source Serif 4 Variable, Inter Variable, JetBrains Mono Variable (latin + latin-ext). Kein Google Fonts CDN. +- **Design-Tokens** aus `digiformer-brand-system.md` korrekt umgesetzt: Electric Persimmon `oklch(0.71 0.22 38)`, Text-on-Accent `#2A0F02`, alle Surface-/Text-/Border-Token +- **shadcn/ui-Primitives**: Button (3 Varianten: default/secondary/ghost), Input, Textarea, Label, Dialog — alle mit Brand-Token statt hardcodierten Farben +- **BaseLayout** mit korrektem `` (OG-Tags, hreflang, canonical, font-preload), NavBar, Footer +- **NavBar**: sticky, Mono-Wortmarke, 4 Nav-Links, DE/EN-Switcher (EN ausgegraut), CTA-Button, Hamburger-Icon mobil, Border-on-scroll +- **Footer**: 4-Spalten (Marke, Produkt, Stack, Kontakt), Bottom-Bar mit Copyright + Impressum/Datenschutz +- **Home** (`index.astro`): Hero-Sektion mit Eyebrow, H1 (Persimmon-Akzent auf "Grenzenlos"), Lead-Text, zwei CTAs +- **`pnpm dev`** läuft auf `localhost:4321`, Typografie und Farben visuell verifiziert +- **Astro-Telemetrie** deaktiviert + +## Verzeichnisstruktur + +``` +src/ +├── components/ +│ ├── primitives/ ← Button, Input, Textarea, Label, Dialog (React) +│ ├── marketing/ ← NavBar.astro, Footer.astro +│ └── islands/ ← (leer, für Phase 3: TesterForm, ContactForm) +├── content/ ← (leer, für Phase 1: module.ts) +├── layouts/ ← BaseLayout.astro +├── pages/ ← index.astro +└── styles/ ← global.css +``` + +## Was als Nächstes (Phase 1 — Komponenten-Bibliothek) + +Laut CLAUDE.md §9, Phase 1: + +1. `Eyebrow`, `SectionHeading`, `NumberedItem` — primitive Layout-Bausteine +2. `StatusDot`, `ModuleCard`, `ModuleGrid` — Modul-Visualisierung +3. `TechStrip`, `SovereigntyBlock`, `RoadmapTimeline` — Sektions-Komponenten +4. `ObjectionAnswer`, `CTABlock` — Argumentations- und Abschluss-Komponenten +5. Dev-Seite: `/dev/components.astro` — alle Bausteine in allen Varianten + +## Update 2026-05-04 — Hero Dark-Variante (verbindlich) + +CLAUDE.md §7.1 wurde mit verbindlicher Spec für dunkle Hero-Variante erweitert. Umsetzung: + +- **`src/pages/index.astro`** — Hero-Sektion komplett umgebaut: `#0E0F14`-Hintergrund, Off-White-Text `#F5F5F0`, Persimmon-Eyebrow mit `▸`-Prefix in `#FF6B2C`. Wort „Grenzenlos" als Background-Highlight (`#FF6B2C` bg, `#2A0F02` fg, padding `0 12px`, `box-decoration-break: clone`, kein border-radius). Tech-Strip läuft im selben dunklen Hintergrund weiter (`border 0.5px solid rgba(245,245,240,0.08)`, STACK-Label bei `0.85`-Opacity, Items bei `0.55`). Sentinel `
` markiert das Ende der dunklen Zone für die NavBar. +- **`src/components/marketing/NavBar.astro`** — Dual-Mode-Implementierung. CSS-Klassen `.nav-bar--dark` und `.nav-bar--light` definieren je eigene Farb-Sätze (Logo, Links, Lang-Switch, CTA). Toggle per Scroll-Listener: `getBoundingClientRect()` des `data-hero-end`-Sentinels gegen NavBar-Höhe. Auf Nicht-Home-Seiten (Sentinel fehlt) immer Light-Mode. Transition 200ms. + +Visuell verifiziert via preview_inspect: `bg = rgb(14,15,20)`, Highlight-Farben korrekt, `box-decoration-break: clone` aktiv (mit -webkit-Prefix), NavBar-Dark-Mode bei scrollY=0, Light-Mode-Switch beim Sentinel-Crossing nachgewiesen. + +## Phase 1 abgeschlossen — Komponenten-Bibliothek + +Alle 11 Marketing-Komponenten plus Daten-Foundation und Showcase-Seite stehen. + +**Neue Dateien:** +- `src/content/module.ts` — Modul-Typen, 19 Module aus Appendix A, `statusLabel`/`pillarTitles`-Maps +- `src/components/marketing/Eyebrow.astro` — Mono-Caption mit Tone-Varianten (`tertiary` / `accent` / `inverse`), optionalem `prefix` (z.B. `▸`) und Status-Pille +- `src/components/marketing/StatusDot.astro` — vier Status-Glyphen rein per CSS (gefüllt / halbgefüllt / outline / dashed), monochrom, mit `inverse`-Variante für dunkle Hintergründe +- `src/components/marketing/SectionHeading.astro` — `h2` mit optionalem Eyebrow + Subtitle, `display`-Variante für „große Versprechen"-Sektionen, inverse-Modus +- `src/components/marketing/NumberedItem.astro` — Mono-Nummer + Serif-Title + Body-Slot +- `src/components/marketing/ModuleCard.astro` / `ModuleGrid.astro` — eine Säulen-Karte und das 4-Spalten-Grid mit optionaler Status-Legende +- `src/components/marketing/TechStrip.astro` — extrahiert aus `index.astro`, hat `dark`/`light`-Varianten +- `src/components/marketing/SovereigntyBlock.astro` — 2-Spalten-Layout mit Display-Headline links und 2×2-Border-Left-Items rechts +- `src/components/marketing/RoadmapTimeline.astro` — vertikale Linie mit Phase-Markern, current-Pille +- `src/components/marketing/ObjectionAnswer.astro` — Serif-Frage als Quote, Body-Slot +- `src/components/marketing/CTABlock.astro` — Eyebrow + Headline + Body + bis zu 3 CTAs (`primary`/`secondary`/`ghost`), `inverse`-Modus +- `src/pages/dev/components.astro` — interne Showcase-Seite mit jeder Komponente in jeder Variante (nicht im Sitemap) +- `public/og-default.png` — 1200×630 Platzhalter, gespiegeltes Hero-Layout, generiert via `node scripts/generate-og.mjs` +- `scripts/generate-og.mjs` — Skript zur Regeneration +- `sharp` zur Dev-Dependency-Liste ergänzt (für OG-Generierung; war ohnehin transitiv via Astro vorhanden) + +**Geänderte Dateien:** +- `src/pages/index.astro` — Tech-Strip-Inline-Code durch `` ersetzt, identisches visuelles Ergebnis +- `CLAUDE.md` §0 + §4.1 — „Astro 5" → „Astro 6" (stillschweigender Versions-Update wie abgesprochen) + +**Visuell verifiziert:** Komplettes Durchscrollen von `/dev/components` und Re-Check von `/`. Status-Glyphen monochrom korrekt, Persimmon-Akzent sparsam (nur Hero-Highlight, current-Pille, primary CTAs, Eyebrow-accent-Variante, Border-Left auf SovereigntyBlock-Items), keine Drop-Shadows oder Gradients. + +## Phase 2 abgeschlossen — Home + +Alle 9 Sektionen aus CLAUDE.md §7.1 sind in `src/pages/index.astro` komponiert: + +1. Hero (dark) — unverändert aus Phase 0/1 +2. Tech-Strip (dark) — unverändert +3. Modul-Landschaft — `` mit Legende, Eyebrow nutzt `modules.length` (aktuell 19), Footer-Link auf `/module` +4. Was uns trennt — 3 `` 01/02/03, Surface-White-Hintergrund +5. Souveränität — `` mit dreizeiliger Display-Headline, vier Border-Left-Items +6. Roadmap-Snapshot — `` mit 4 Phasen, „Heute" mit Aktuell-Pille (Persimmon) +7. Schnell-Argumentarium — 5 `` im 2-Spalten-Grid, fünfter wraps unten allein (per Spec) +8. Tester-Programm-Teaser — `` mit Primary + Ghost-Mail-Link +9. Final-CTA — `` mit Primary + Secondary, dunkler Hintergrund symmetrisch zum Hero + +**Visueller Rhythmus:** Dunkel (Hero+Strip+Sentinel) → hell → surface-white → hell → surface-white → hell → surface-white → dunkel (Final-CTA). Wechselnde Bg-Töne strukturieren ohne Bordüren. + +**NavBar Dual-Mode** verifiziert: bei `scrollY=0` über Hero `nav-bar--dark` aktiv, beim Body-Scroll wechselt korrekt zu `nav-bar--light`. Beim Final-CTA bleibt sie hell (Sentinel ist nach Sektion 2 platziert). + +**Verifiziert per DOM-Inspection:** Alle Sektionen rendern mit korrekter Bg, Eyebrow, Headline. Auto-Count im Modul-Eyebrow funktioniert (`VIER SÄULEN · 19 MODULE`). + +**Inhaltliche Hinweise an Pascal:** +- CLAUDE.md erwähnt mehrfach „16 Module", in `module.ts` (Appendix A) sind aber 19 hinterlegt. Der Eyebrow rendert dynamisch aus den Daten — `19 MODULE`. Wenn 16 die richtige Marketing-Aussage ist, kürze ich die Modul-Liste; wenn 19 stimmt, aktualisiere ich CLAUDE.md §0/§1. +- Die `Aktuell`-Pille auf der Roadmap nutzt Persimmon-Bg + dunklen Text — passt zum Brand-System §3.5 („Status-Pille aktuelle Phase"). +- Final-CTA-Headline ist bewusst eine zusammengefasste Version der zwei Sätze aus §7.1 §9 („30 Minuten, kostenlos, unverbindlich..." + „Sie schildern, wo Sie stehen..."). Wenn du beide Sätze willst, splitte ich es in body + headline. + +## Update — EN-Variante + Sprachumschaltung (vorgezogen aus Phase-2-Backlog) + +EN war ursprünglich Phase 2 (CLAUDE.md §9 Phase 6: „EN-Variante mit `astro-i18n` oder Manual-Routing"). Auf Pascal-Wunsch jetzt vorgezogen. + +**Routing:** Astro built-in i18n in `astro.config.mjs`, `defaultLocale: 'de'`, `prefixDefaultLocale: false`. → `/` bleibt DE, `/en/` ist EN. + +**Neue/Geänderte Dateien:** +- `astro.config.mjs` — i18n-Block, sitemap mit Locale-Map +- `src/content/module.ts` — `nameEn`, `pillarTitleEn`, `descriptionEn` pro Modul; `statusLabelEn`/`pillarTitlesEn`-Maps; Helper `getModuleName/getStatusLabel/getPillarTitle(item, lang)` +- `src/i18n/strings.ts` — zentrale UI-Strings DE/EN für NavBar, Footer, Roadmap-Pille; `getLangFromPath()` und `getLocalizedPath()` +- `src/layouts/BaseLayout.astro` — auto-detect Lang aus Pfad, ``, hreflang DE/EN/x-default, og:locale, lokalisierte Default-Description +- `src/components/marketing/NavBar.astro` — Sprachumschalter mit aktiv/inaktiv-States in beiden Modi (dark/light), Links via `getLocalizedPath()`, lang-spezifische Nav-Pfade (`/module` vs `/en/module` etc.) +- `src/components/marketing/Footer.astro` — alle Strings aus i18n-Dict +- `src/components/marketing/StatusDot.astro` / `ModuleCard.astro` / `ModuleGrid.astro` / `RoadmapTimeline.astro` — `lang`-Prop (Default `'de'`) +- `src/pages/en/index.astro` — komplette englische Home, gleiche 9 Sektionen mit übersetzten Inhalten + +**Verifiziert:** +- `/` → ``, deutsche Inhalte, DE aktiv im Switcher +- `/en/` → ``, englische Inhalte, EN aktiv im Switcher +- Switcher-Links: DE-Button auf `/en/` führt zu `/`, EN-Button auf `/` führt zu `/en/` +- hreflang reziprok auf beiden Seiten, `x-default` zeigt auf DE +- Modul-Namen: „Aufgaben" (DE) / „Tasks" (EN), „Belege" / „Documents", etc. Status-Labels und Säulen-Titel ebenfalls übersetzt +- NavBar-Dual-Mode funktioniert auf EN-Seite identisch (dark über Hero, light beim Body-Scroll) +- `/dev/components` rendert weiterhin fehlerfrei (alle Komponenten haben `lang`-Default `'de'`) + +**Nicht-übersetzte Pfade (Hinweis):** EN-NavBar verlinkt auf `/en/module`, `/en/sovereignty`, `/en/roadmap`, `/en/tester` — diese Seiten existieren noch nicht (auch nicht in DE), 404 bis Phase 3+4 sie liefern. Footer-Links analog. Sprachumschalter geht von `/en/foo` zurück nach `/foo` (oder umgekehrt) — wenn die DE-Variante nicht existiert, landet der User auf der DE-404 mit deutscher Sprache. + +**Inhaltliche Hinweise an Pascal:** +- Module-Eigennamen bleiben in beiden Sprachen identisch wo sinnvoll (CRM, Helpdesk, WhatsApp), übersetzte Konzepte sonst (Belege → Documents, Versand → Shipping, BuHa-Export → Accounting export, Personal → HR, Zeiterfassung → Time tracking) +- „Tester" im EN als „Testers" für die Nav, „Become a tester" als CTA. Wenn du „Beta tester" oder „Pilot user" stärker findest, ändere ich +- Hero-Headline EN: „Start lean. Grow without limits." mit Highlight nur auf „Grow" (statt „Grenzenlos"). Andere Variante denkbar: „Grow boundless." oder „Scale without limits." — sag wenn du eine bestimmte willst +- Tech-Strip EN: DSGVO → GDPR (selbe Sache, andere Schreibung), Rest unverändert +- Sektion 5 Souveränität: „Your data. Your country. Your control." spiegelt das DE 1:1 +- Imprint/Privacy: Deutsche Pflichtseiten haben deutsche URLs (`/impressum`, `/datenschutz`); EN-Footer linkt auf `/en/imprint` und `/en/privacy`. Diese müssen in Phase 4 angelegt werden — englische Pflichtseiten sind rechtlich zwar nicht erforderlich, aber Usability-relevant + +## Phase 3 abgeschlossen — Module + Tester (Scope C, ohne Form) + +Pascal-Entscheidung: Form-Anbindung auf Phase 3.5 verschieben, weil `app.slimcore.io` produktiv noch nicht live ist (nur `app.staging.slimcore.io`). Marketing-Site geht ohne Form-Submission live, Tester-Anmeldung läuft über Mail + Calendly bis Production-App + Form fertig sind. + +**Neue Dateien:** +- `src/pages/module.astro` — DE Modul-Seite, Hero-light + ModuleFilter-Island + `