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
| Bereich | Technologie |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Sprache | TypeScript (strict mode) |
| Styling | Tailwind CSS v4 + shadcn/ui v4 |
| Fonts | Inter + JetBrains Mono (lokal via next/font) |
| Backend | Supabase (Self-Hosted) |
| Deployment | Standalone Output, PM2, GitHub Actions |
| Hosting | Proxmox-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:
| Funktion | Aufgabe |
|---|---|
| Web-Server | Next.js-App via PM2 auf Node 22 |
| Datenbank | Supabase als Docker-Compose-Stack (PostgreSQL, GoTrue, PostgREST, Studio) |
| CI/CD Runner | GitHub Actions Self-Hosted Runner fuer automatisierte Deployments |
| Reverse Proxy | Nginx 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-LockHero.tsx— Typewriter-Animation, Live-Metriken-PollingContactForm.tsx— Formular-State, API-CallsProjectCard.tsx— Hover-State-ManagementBlogEditorForm.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:
- SSH-Verbindung zum Web-Server
- Code pullen (
git fetch && git reset) - Dependencies installieren und App bauen (
npm install && npm run build) - Static Assets in den Standalone-Output kopieren
- 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:
- Server Components als Standard — minimaler JS-Footprint
- Design Tokens statt Hex-Werte — konsistente Farben ueberall
- Self-Hosted auf eigenem Proxmox-Cluster — volle Kontrolle, keine Cloud-Abhaengigkeit
- Standalone Output + PM2 — schlankes Deployment ohne Container-Overhead
- 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.