Das Problem mit Portfolio-Seiten
Die meisten Entwickler-Portfolios haben ein Problem: Sie sind statisch. Drei Projekte, jeweils ein Absatz, ein Link — fertig. Wenn ein neues Projekt dazukommt, muss man die gesamte Seite umbauen. Wenn man filtern will, wird es kompliziert. Und wenn man Screenshots zeigen will, klebt man ein paar <img>-Tags rein und hofft auf das Beste.
Fuer macip.de wollte ich ein System, das skaliert. Neue Projekte hinzufuegen sollte bedeuten: ein Objekt in ein Array pushen, Screenshots in einen Ordner legen — fertig. Alles andere generiert sich automatisch.
Die zentrale Datenquelle: projects.ts
Das Herzstueck des gesamten Projektsystems ist eine einzige TypeScript-Datei: src/data/projects.ts. Hier leben alle Projektdaten, Typen, Farb-Mappings und Helper-Funktionen.
Das Type-System
type AccentColor = "accent-teal" | "accent-blue" | "accent-purple";
interface Project {
slug: string;
tag: string;
title: string;
desc: string;
longDesc: string;
accent: AccentColor;
href: string;
stack: string[];
features: string[];
status: "live" | "in-entwicklung" | "intern";
category: "saas" | "lab" | "infra";
screenshots: string[];
}Jedes Projekt hat einen klaren Typ, eine Kategorie und einen Status. Die AccentColor ist ein Union-Type aus genau drei Farben — das verhindert, dass jemand versehentlich eine nicht existierende Farbe zuweist.
Das Farb-Mapping
const ACCENT_HEX_MAP: Record<AccentColor, string> = {
"accent-teal": "#00d4aa",
"accent-blue": "#5b8fff",
"accent-purple": "#aa77ff",
};Dieses Mapping ist die einzige Quelle fuer Akzent-Hex-Werte im gesamten Projekt. Tag, ProjectCard, Detail-Seiten — alle importieren von hier. Keine Hex-Duplikate, keine Inkonsistenzen.
Die Farbzuweisung folgt einer semantischen Logik:
- Teal (#00d4aa) → SaaS-Produkte (produktiv, kommerziell)
- Blue (#5b8fff) → Lab-Experimente (explorativ, technisch)
- Purple (#aa77ff) → Infrastruktur (intern, DevOps)
Helper-Funktionen
Zwei einfache Funktionen machen die Daten ueberall zugaenglich:
function getProjectBySlug(slug: string): Project | undefined {
return PROJECTS.find((p) => p.slug === slug);
}
function getAllSlugs(): string[] {
return PROJECTS.map((p) => p.slug);
}getAllSlugs() wird fuer generateStaticParams() verwendet — dazu gleich mehr.
Die Projekt-Uebersicht: /projects
Die Uebersichtsseite ist ein Server Component, das die gesamte Projekt-Liste rendert und einen Client-seitigen Filter bereitstellt.
// src/app/(marketing)/projects/page.tsx
export default function ProjectsPage() {
return (
<div className="mx-auto max-w-[960px] px-6 py-20 sm:px-12">
<h1 className="...">Projekte</h1>
<Suspense>
<ProjectFilter />
</Suspense>
</div>
);
}Wichtig ist der <Suspense>-Wrapper um <ProjectFilter />. Der Filter liest URL-Search-Params via useSearchParams() — ein Client-Hook, der in Next.js einen Suspense-Boundary benoetigt.
Der Kategorie-Filter
const CATEGORIES = [
{ key: "saas", label: "SaaS", accent: "accent-teal" },
{ key: "lab", label: "Lab", accent: "accent-blue" },
{ key: "infra", label: "Infra", accent: "accent-purple" },
];Drei Kategorien, jede mit ihrer eigenen Akzent-Farbe. Der Filter arbeitet ueber URL-Parameter (?filter=lab), sodass Links direkt auf gefilterte Ansichten zeigen koennen. Das nutzt die Navbar: Der "Lab"-Link zeigt auf /projects?filter=lab.
Der Filter hat ein Toggle-Verhalten: Klickt man auf die bereits aktive Kategorie, wird der Filter zurueckgesetzt. Das ist intuitiver als ein separater "Alle"-Button.
Die ProjectCard-Komponente
Jede Karte folgt einem konsistenten Pattern:
+------------------------------+
| ========== (Gradient) |
| |
| SaaS |
| YardWinner |
| SaaS-Plattform fuer die |
| maritime Industrie... |
| |
| Ansehen -> |
+------------------------------+
Von oben nach unten: Gradient-Linie (nur bei Hover), Tag mit Akzent-Farbe, Titel in font-sans, Beschreibung in font-mono und der Arrow-Link unten rechts
Die Karte entscheidet automatisch, welches Element sie rendert:
- Interner Link (
/...) →<Link>(client-side navigation) - Externer Link (
http...) →<a target="_blank"> - Platzhalter (
#) →<a>ohne target
Die Hover-Effekte sind subtil, aber durchdacht:
- Gradient-Linie oben — 2px Hoehe, von Akzent-Farbe nach Transparent
- Border-Glow — Border wechselt zur Akzent-Farbe mit 30% Opacity
- Hintergrund-Tint — Dezenter Akzent-Hintergrund
- Arrow-Text — "Ansehen →" erscheint unten rechts
Alles mit transition-all duration-300 fuer geschmeidige Uebergaenge.
Die Detail-Seite: /projects/[slug]
Hier passiert die eigentliche Magie: Jedes Projekt bekommt eine eigene, statisch generierte Seite mit voller SEO-Unterstuetzung.
Static Generation mit generateStaticParams
export async function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}Next.js generiert beim Build fuer jeden Slug eine statische HTML-Seite. Kein Server-Rendering zur Laufzeit, kein Datenbankzugriff — reine HTML-Dateien, die sofort ausgeliefert werden.
Dynamische Metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params; // Next.js 16: params ist ein Promise
const project = getProjectBySlug(slug);
if (!project) return {};
return {
title: project.title,
description: project.desc,
alternates: { canonical: `/projects/${slug}` },
openGraph: {
type: "article",
title: `${project.title} — macip.de`,
description: project.desc,
},
};
}Jede Projekt-Seite hat eigene OpenGraph-Tags, eine kanonische URL und eine saubere Title-Struktur. Google sieht fuer jedes Projekt eine vollstaendige, eindeutige Seite.
JSON-LD Structured Data
Jede Detail-Seite liefert zwei JSON-LD-Bloecke:
- SoftwareApplication — fuer Rich Results in der Google-Suche
- BreadcrumbList — fuer die Breadcrumb-Anzeige in den SERPs (Home → Projekte → Projektname)
Die Screenshot-Galerie
Projekte mit Screenshots bekommen eine interaktive Galerie mit Keyboard-Navigation:
const ScreenshotGallery = dynamic(
() => import("@/components/ui/ScreenshotGallery"),
{ ssr: false }
);Die Galerie wird mit next/dynamic und ssr: false geladen — sie braucht Browser-APIs fuer Keyboard-Events und hat keinen SEO-relevanten Content. Features:
- Haupt-Bild mit
next/image(automatische Optimierung) - Thumbnail-Strip mit aktivem Zustand (Akzent-Border + Bottom-Highlight)
- Pfeiltasten-Navigation (ArrowLeft/ArrowRight)
- Glassmorphism-Arrows: Halbtransparente Navigations-Pfeile mit Backdrop-Blur
Status-Badge
Jedes Projekt zeigt seinen aktuellen Status:
- Live → Gruener Pulse-Dot + "Live" (animierter Pulse-Effekt)
- In Entwicklung → Gelber Dot + "In Entwicklung"
- Intern → Grauer Dot + "Intern"
Das Lab-Konzept
"Lab" ist keine separate Sektion mit eigenen Seiten — es ist ein Filter auf die bestehende Projekt-Infrastruktur. Projekte mit category: "lab" werden als Experimente gekennzeichnet und bekommen die blaue Akzent-Farbe.
Dieses Design war eine bewusste Entscheidung:
- Kein Duplicate Content — Lab-Projekte und regulaere Projekte teilen sich dieselbe Infrastruktur
- Einfache Navigation — Der "Lab"-Link in der Navbar zeigt auf
/projects?filter=lab - Konsistente UX — Gleiche Karten, gleiche Detail-Seiten, gleiche Galerie
- Einfache Erweiterung — Ein neues Experiment ist ein weiterer Eintrag in
PROJECTS
Neues Projekt hinzufuegen: Der Workflow
Um ein neues Projekt zur Seite hinzuzufuegen, sind genau drei Schritte noetig:
1. Daten in projects.ts eintragen
{
slug: "neues-projekt",
tag: "SaaS",
title: "Neues Projekt",
desc: "Kurze Beschreibung fuer die Karte",
longDesc: "Ausfuehrliche Beschreibung fuer die Detail-Seite...",
accent: "accent-teal",
href: "/projects/neues-projekt",
stack: ["Next.js", "Supabase", "TypeScript"],
features: ["Feature 1", "Feature 2", "Feature 3"],
status: "in-entwicklung",
category: "saas",
screenshots: [
"/projects/neues-projekt/screenshot-1.webp",
],
}2. Screenshots ablegen
public/projects/neues-projekt/
+-- screenshot-1.webp
3. Push auf main
Der GitHub Actions Runner baut die Seite neu, generiert die statische Detail-Seite und deployed automatisch.
Kein CMS, kein Dashboard, keine Datenbank — fuer Projektdaten ist die TypeScript-Datei die perfekte Quelle. Sie ist typsicher, versioniert (Git) und benoetigt keinen Runtime-Zugriff.
Performance-Ergebnis
Durch die Kombination aus Static Generation, Server Components und optimierten Images erreichen die Projektseiten:
- First Contentful Paint: unter 0.8s
- Largest Contentful Paint: unter 1.5s
- Total Blocking Time: 0ms (kein Client-JS auf der Uebersichtsseite)
- Cumulative Layout Shift: 0 (alle Bilder haben feste Dimensionen)
Die Detail-Seiten laden die Screenshot-Galerie per Dynamic Import — sie blockiert weder den initialen Render noch das Hydration-Budget.
Fazit
Das Projektsystem von macip.de beweist, dass man fuer ein dynamisches Portfolio kein CMS braucht. Eine typisierte TypeScript-Datei, kombiniert mit Next.js Static Generation und durchdachten Komponenten, liefert:
- Type-Safety — Fehler werden beim Build erkannt, nicht zur Laufzeit
- Performance — Statische HTML-Seiten, kein Server-Rendering
- SEO — Individuelle Metadata, JSON-LD, kanonische URLs
- Konsistenz — Ein Farb-Mapping, ein Card-Pattern, eine Detail-Struktur
- Einfache Erweiterung — Ein Objekt hinzufuegen, Screenshots ablegen, pushen
Im naechsten Beitrag geht es um das Blogsystem — und warum dort eine Datenbank sinnvoller ist als statische Dateien.