← Zurueck zum Blog
next-jstailwindself-hostedproxmoxdesign-systeminfrastruktur

Wie www.macip.de entstand — Stack, Infrastruktur und Design-Entscheidungen

Marco Carstensen·2. März 2026·6 Min. Lesezeit

Die Ausgangslage

Jeder Freelancer braucht irgendwann eine eigene Website. Aber als jemand, der taeglich mit modernen Web-Stacks arbeitet, wollte ich mehr als eine statische Visitenkarte. macip.de sollte gleichzeitig Portfolio, technisches Playground und Beweis dafuer sein, dass ich die Technologien, die ich anbiete, selbst produktiv einsetze.

Die Anforderungen waren klar:

  • Performance: Lighthouse-Score ueber 95, keine unnoetige Client-Side-JS-Last
  • Self-Hosted: Volle Kontrolle ueber Daten, kein Vendor-Lock-in, DSGVO-konform ohne Kompromisse
  • Erweiterbar: Von der Landing Page zum Mini-CRM, Blog und Projekt-Showroom — alles aus einem Monorepo
  • Dark-Only Design: Ein konsistentes, dunkles Design-System mit klarer Farbhierarchie

Der Stack im Ueberblick

BereichTechnologie
FrameworkNext.js 16 (App Router, Turbopack)
SpracheTypeScript (strict mode)
StylingTailwind CSS v4 + shadcn/ui v4
FontsInter + JetBrains Mono (lokal via next/font)
BackendSupabase (Self-Hosted)
DeploymentStandalone Output, PM2, GitHub Actions
HostingProxmox-Cluster, deutsches Rechenzentrum

Die Wahl fiel bewusst auf Next.js 16 mit dem App Router. React Server Components erlauben es, den Grossteil der Seite serverseitig zu rendern — ohne dass der Browser ein riesiges JavaScript-Bundle laden muss. Nur Komponenten, die tatsaechlich Interaktivitaet brauchen (Typewriter-Animation, Mobile Menu, Formulare), bekommen ein "use client".


Warum Self-Hosted?

Die einfache Antwort: Kontrolle. Vercel oder Netlify haetten das Deployment vereinfacht, aber ich betreibe bereits einen Proxmox-Cluster mit mehreren Nodes und knapp 100 Services. Die Infrastruktur war da — warum sie nicht nutzen?

Die Server-Architektur

Die Infrastruktur besteht aus mehreren isolierten Containern auf einem Proxmox-Cluster, jeder mit einer klar definierten Aufgabe:

FunktionAufgabe
Web-ServerNext.js-App via PM2 auf Node 22
DatenbankSupabase als Docker-Compose-Stack (PostgreSQL, GoTrue, PostgREST, Studio)
CI/CD RunnerGitHub Actions Self-Hosted Runner fuer automatisierte Deployments
Reverse ProxyNginx mit TLS-Terminierung — alle Services nur ueber HTTPS erreichbar

Das Design-System

Bevor eine einzige Komponente entstand, habe ich das Farbsystem definiert. Alle Farben leben als Design Tokens in globals.css unter @theme inline und sind direkt als Tailwind-Klassen verfuegbar.

Die Farbhierarchie

/* Surfaces */
--color-surface:      #080810;   /* Seiten-Hintergrund */
--color-surface-card: #0f0f1a;   /* Karten */
 
/* Akzente */
--color-accent-teal:   #00d4aa;  /* Primary: CTAs, Links */
--color-accent-blue:   #5b8fff;  /* Sekundaer: Tags, Blog */
--color-accent-purple: #aa77ff;  /* Tertiaer: Infra */
 
/* Text-Hierarchie (7 Stufen) */
--color-text-heading:  #eeeef8;  /* Hoechster Kontrast */
--color-text-body:     #c0c0d4;  /* Fliesstext */
--color-text-subtle:   #8888a8;  /* Beschreibungen */
--color-text-nav:      #6a6a88;  /* Navigation */
--color-text-muted:    #5c5c7a;  /* Karten-Beschreibungen */
--color-text-dim:      #4e4e6c;  /* Timestamps */
--color-text-faint:    #3a3a55;  /* Labels */

Die sieben Text-Stufen klingen nach Overkill, sind aber entscheidend. Auf dunklem Hintergrund muss man praezise steuern, welche Information zuerst wahrgenommen wird. Eine Ueberschrift (text-heading) sticht sofort ins Auge, waehrend ein Timestamp (text-dim) bewusst zuruecktritt.

Typografie-Regeln

Zwei Fonts, klare Zustaendigkeit:

  • Inter (font-sans): Ausschliesslich fuer Ueberschriften, Logo, Card-Titles
  • JetBrains Mono (font-mono): Body-Text, Navigation, Labels, Tags, Buttons, Code

Die Entscheidung, JetBrains Mono als Body-Font zu verwenden, war bewusst. Die Monospace-Aesthetik verstaerkt den technischen Charakter der Seite und unterscheidet sie von typischen Portfolio-Designs.

Beide Fonts werden lokal ueber next/font geladen — kein Google-CDN-Request, keine DSGVO-Probleme, optimiertes Font-Loading mit automatischem font-display: swap.


Next.js 16: App Router in der Praxis

Route Groups

Die App nutzt zwei Route Groups, um oeffentliche und geschuetzte Bereiche sauber zu trennen:

src/app/
├── (marketing)/          ← Oeffentlich: Homepage, Blog, Projekte
│   ├── layout.tsx        ← PageLayout (Navbar + Footer + Glow)
│   ├── page.tsx           ← Homepage
│   ├── blog/
│   ├── projects/
│   └── contact/
│
└── (app)/                ← Geschuetzt: Dashboard, Admin
    ├── layout.tsx        ← Auth-Guard + DashboardLayout
    └── dashboard/

Jede Route Group hat ihr eigenes Layout. Die (marketing)-Seiten bekommen automatisch Navbar, Footer und die dezenten Glow-Effekte. Die (app)-Seiten haben stattdessen die Sidebar-Navigation und pruefen auf jedem Request die Admin-Berechtigung.

Server Components als Standard

Eine zentrale Konvention: Jede Komponente ist standardmaessig ein Server Component. "use client" wird nur dort eingesetzt, wo es wirklich noetig ist:

  • Navbar.tsx — Hamburger-Menu-State, Scroll-Lock
  • Hero.tsx — Typewriter-Animation, Live-Metriken-Polling
  • ContactForm.tsx — Formular-State, API-Calls
  • ProjectCard.tsx — Hover-State-Management
  • BlogEditorForm.tsx — Editor-Interaktionen

Alles andere — Footer, PageLayout, Tag, Blog-Listing, Projekt-Detail — wird komplett auf dem Server gerendert. Das Ergebnis: minimaler JavaScript-Footprint im Browser.

Params als Promise (Next.js 16)

Ein Detail, das bei der Migration zu Next.js 16 auffaellt: params in dynamischen Routes ist jetzt ein Promise und muss mit await aufgeloest werden:

interface Props {
  params: Promise<{ slug: string }>;
}
 
export default async function BlogDetailPage({ params }: Props) {
  const { slug } = await params;
  // ...
}

Das ist eine Breaking Change gegenueber Next.js 14/15, wo params ein synchrones Objekt war.


CI/CD: GitHub Actions mit Self-Hosted Runner

Das Deployment ist vollstaendig automatisiert. Bei jedem Push auf main laeuft eine Pipeline auf dem Self-Hosted Runner:

  1. SSH-Verbindung zum Web-Server
  2. Code pullen (git fetch && git reset)
  3. Dependencies installieren und App bauen (npm install && npm run build)
  4. Static Assets in den Standalone-Output kopieren
  5. PM2-Prozess neu starten mit den aktuellen Environment Variables

Der gesamte Prozess dauert unter 60 Sekunden.

Standalone Output

Next.js wird mit output: "standalone" gebaut. Das erzeugt ein minimales Node.js-Bundle in .next/standalone/, das nur die tatsaechlich genutzten Dependencies enthaelt — kein vollstaendiges node_modules. Fuer ein Self-Hosted Setup ist das ideal: kleiner Footprint, schneller Start.


Security Headers

Sicherheit ist kein Nachgedanke. In next.config.ts werden fuer alle Routen strenge HTTP-Headers gesetzt:

{
  key: "Strict-Transport-Security",
  value: "max-age=63072000; includeSubDomains; preload",
},
{
  key: "X-Content-Type-Options",
  value: "nosniff",
},
{
  key: "X-Frame-Options",
  value: "SAMEORIGIN",
},
{
  key: "Permissions-Policy",
  value: "camera=(), microphone=(), geolocation=()",
}

Kein Framing, keine Sniffing-Angriffe, HSTS mit 2-Jahres-TTL und Preload. Die Permissions-Policy blockt zusaetzlich Kamera, Mikrofon und Geolocation.


Das Ambient-Glow-Design

Ein subtiles, aber wirkungsvolles Detail: Die Marketing-Seiten haben zwei animierte Glow-Effekte im Hintergrund. Ein tuerkiser Kreis oben rechts und ein blauer unten links — beide als fixierte Radial-Gradients mit Blur.

Die glow-Animation laesst den Effekt sanft zwischen 4% und 7% Opacity pulsieren — subtil genug, um nicht abzulenken, aber praesent genug, um Tiefe zu erzeugen.


Live-Infrastruktur-Metriken

Ein Feature, das die Seite von typischen Portfolios abhebt: Die Hero-Section zeigt Echtzeit-Metriken des Proxmox-Clusters. Nodes, Services, CPU-Auslastung, RAM und Storage werden ueber eine interne API-Route abgefragt.

// Hero.tsx — Polling alle 30 Sekunden
useEffect(() => {
  fetchPulse();
  const id = setInterval(fetchPulse, 30_000);
  return () => clearInterval(id);
}, []);

Die Daten werden mit ISR (revalidate: 30) gecached und mit stale-while-revalidate ausgeliefert — ein guter Kompromiss zwischen Aktualitaet und Performance.


Fazit

macip.de ist kein Ergebnis eines Wochenendes. Es ist ein gewachsenes System, das mit jedem Feature komplexer wird — aber durch klare Konventionen, ein durchdachtes Design-System und konsequente Self-Hosting-Strategie handhabbar bleibt.

Die wichtigsten Entscheidungen:

  1. Server Components als Standard — minimaler JS-Footprint
  2. Design Tokens statt Hex-Werte — konsistente Farben ueberall
  3. Self-Hosted auf eigenem Proxmox-Cluster — volle Kontrolle, keine Cloud-Abhaengigkeit
  4. Standalone Output + PM2 — schlankes Deployment ohne Container-Overhead
  5. Strenge Security Headers — Sicherheit ab Tag 1

Im naechsten Beitrag zeige ich, wie die Projektseiten und das Lab aufgebaut sind — mit dynamischen Routes, zentraler Datenverwaltung und filterbaren Kategorien.