dphil.ru
Die Website, die Sie sich gerade ansehen
Rolle
Creator, Full-Stack Engineer, DevOps
Jahr
2026
Stack
Next.js 16React 19TypeScriptTailwind CSS 4TanStack QueryGSAP 3View TransitionsReact Hook FormStrapi 5.5PostgreSQL 17S3 StorageDockerDokploy

Die Aufgabe
Erstellung einer Portfolio-Website, die nicht nur als schnelle Visitenkarte dient, sondern als lebendiger Beweis für technische Reife. Das Ziel war die Implementierung von Production-Grade-Lösungen: fehlertolerante Infrastruktur, tiefgreifende serverseitige Lokalisierung, anspruchsvolle Animationen ohne UX-Einbußen und eine strikte Datenarchitektur.
Die Phasen
Architekturplanung und Datenmodellierung
- Datenschema: Entwicklung eines relationalen Modells in Strapi 5.5 und Postgres 17. Hybride Lokalisierung von Beginn an eingeplant: Technische Felder (Slug, Links, Stack) wurden geteilt, der Content übersetzbar gemacht, was Daten-Desynchronisation komplett ausschließt.
- Frontend-Architektur: Aufbau des Anwendungs-Skeletts nach der Feature-Sliced Design (FSD) Methodik. Strenge Grenzen für jeden Slice definiert:
index.tsfür sichere Client-Exporte undserver.tsfür Server-Code, um die Strapi-Transportschicht vom Client-Bundle zu isolieren.
DevOps-Fundament und Überwindung der Container-Flüchtigkeit
- Infrastruktur: Bereitstellung einer Docker-Umgebung, verwaltet durch den Self-Hosted-Orchestrator Dokploy.
- Medien-Schicht: Da Docker-Container flüchtig (ephemeral) sind und lokale Daten bei jedem Deploy löschen, habe ich vom ersten Tag an einen S3-kompatiblen Objektspeicher und ein CDN integriert. Für die Bequemlichkeit der lokalen Entwicklung wurde ein Graceful Fallback auf die Festplatte eingerichtet, falls Cloud-Variablen fehlen.
Kernentwicklung, Server-Rendering und sauberes i18n
- Rendering: Initialisierung von Next.js 16 und React 19. Anfragen an das CMS auf den Server verschoben (RSC + Route Handlers), um eine sofortige Auslieferung von fertigem HTML ohne aufblitzende Ladezustände zu gewährleisten.
- Lokalisierungs-Optimierung:
i18nextso konfiguriert, dass es strikt serverseitig mit einem per-Locale gecachten Instanz läuft. Der Client erhält fertige Strings als Props – das spart enorm viel JS im Browser und macht überflüssige Client-Provider obsolet.
Animations-Engineering und API-Synchronisation
- Integration: Implementierung von GSAP 3 für Mikro-Animationen und der experimentellen View Transitions API in Next.js für fließende Seitenübergänge.
- Engineering Challenge: Konfrontation mit einem "Content Flash" (FOUC) Bug, bei dem React-Commits beim Nachladen von Lazy-Chunks sich über das laufende GSAP-Intro legten. Lösung des Problems auf struktureller Ebene (durch einen
key-Remount der Komponente mitupdate="none"), wodurch die Animationen resistent gegen den React StrictMode gemacht wurden.
Cache-Automatisierung, UI-Feinschliff und Release
- On-Demand ISR: Strapi und Next.js über Webhooks verbunden. Bei Veröffentlichung von Inhalten sendet das Backend einen Request an eine Frontend-Route und invalidiert gezielt die Cache-Tags eines spezifischen Projekts – Daten aktualisieren sich on the fly, ohne dass die gesamte Website neu gebaut werden muss.
- UI-Finalisierung: Konfiguration von Design-Tokens in Tailwind 4 (
@theme inline), Einsatz eines Inline-Skripts für den Theme-Wechsel ohne visuelles Flackern. Einrichtung von CI/CD durch den Bau von Images auf Docker Hub.
Der Prozess
Technologie-Stack
| Schicht | Technologien |
|---|---|
| Frontend | Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4 |
| Client-seitige Daten | TanStack Query |
| Animationen | GSAP 3 + @gsap/react, View Transitions API (experimentelles Next 16 Feature) |
| Formulare | React Hook Form + Zod |
| Content | react-markdown + remark-gfm, yet-another-react-lightbox, react-photo-album |
| i18n | i18next / react-i18next, 3 Sprachen: RU / EN / DE |
| Backend | Strapi 5.5 (Headless CMS) auf Node, PostgreSQL 17 |
| Medienspeicher | S3-kompatibler Objektspeicher + CDN |
| Infrastruktur | Docker, VPS, Dokploy, separate Git-Repositories für Frontend/Backend |
Frontend: Unter der Haube
Architektur — Feature-Sliced Design
Der Code ist in Schichten strukturiert (app → views → widgets → features → entities → shared) und nicht einfach in einen components/-Ordner geworfen. Jeder Slice verfügt über einen eigenen Barrel (index.ts) für client-sichere Exporte und eine separate server.ts für Server-Code – so kann das Client-Bundle die Strapi-Transportschicht physisch gar nicht mitziehen. Die Geschäftslogik (Infinite Scroll, Kontaktformular, Abrufen von Seitendaten) ist in Hooks und View-API-Services ausgelagert und nicht über Komponenten verschmiert. Das ist eine Architektur, die sich im Team problemlos skalieren lässt – kein "Portfolio-Workaround".
// entities/project/index.ts — Client-safe Barrel
export { projectTags, type ProjectTag } from './config/tags';
export { fetchProjectsPage, PROJECTS_PAGE_SIZE } from './api/client';
export { useInfiniteProjects } from './api/use-infinite-projects';
export { ProjectCard, type ProjectCardProps } from './ui/ProjectCard';
// entities/project/server.ts — Server-only Barrel, wird nur
// von RSC/Route Handlers importiert, erreicht nie das Client-Bundle
export {
getProject,
getProjects,
getProjectsCount,
getProjectSlugs,
} from './api/server';
Server Components + Server Route Handlers
Daten aus Strapi werden serverseitig abgefragt (RSC + Route Handlers); der Client erhält sofort fertiges HTML und minimales JS – das sorgt für einen schnellen First Render, perfektes SEO out-of-the-box und keinerlei "blitzende" Ladezustände auf der Startseite.
Theming und Tokens ohne FOUC
Das gesamte Styling (Farben, Abstände, Schriftarten) ist in CSS-Tokens abgebildet (Tailwind 4 @theme inline). Der Theme-Wechsel funktioniert ohne FOUC (Flash of Unstyled Content): Ein Inline-Skript wendet die .dark-Klasse vor dem First Paint an (via useServerInsertedHTML), kombiniert mit einem <noscript>-Fallback und voller Berücksichtigung von prefers-reduced-motion.
Serverseitige Mehrsprachigkeit
i18next läuft ausschließlich auf dem Server – die Instanz wird pro Locale gecacht, und Client-Komponenten erhalten bereits übersetzte Strings als Props, ohne überflüssige Provider und unnötiges JS im Browser. Die Sprache wird nach Priorität ermittelt: NEXT_LOCALE-Cookie → Accept-Language-Header (via negotiator + intl-localematcher) → Standard en. Alles ist lokalisiert, einschließlich des PDF-Lebenslaufs: Je nach gewählter Sprache wird die russische oder englische Version des CVs ausgeliefert (für die deutsche Locale wird die englische Variante verwendet).
GSAP-Animationen und Vermeidung des Content-Flashs
Benutzerdefinierter animierter Cursor, Text-Intro-Animationen (SplitText), nahtlose Übergänge zwischen den Seiten über die experimentelle View Transitions API von Next.js.
Eine besondere Herausforderung war der Content-Flash-Bug beim ersten Laden (Überschrift erscheint → verschwindet → wird erneut animiert). Die Ursache war komplex: React ruft im experimentellen View Transition-Modus document.startViewTransition nicht nur bei der Navigation auf, sondern bei jedem Commit mit einer Transition (insbesondere beim Nachladen eines Lazy-Client-Chunks direkt nach der Hydratation). Dadurch legte sich die Snapshot-Animation der Seite über das noch laufende GSAP-Intro. Die finale Lösung war eine strikte Trennung der Verantwortlichkeiten: Das erste Laden der Seite gehört GSAP, Routing-Übergänge der View Transitions API, und alles andere wird gar nicht animiert. Dies wurde strukturell gelöst (über einen key-Remount von <ViewTransition> mit update="none" für interne Commits), anstatt durch einen Hack mit Klassen oder Timern – absolut resistent gegen den React StrictMode. Die Bewältigung solcher Edge-Cases zeigt, dass man experimentelle APIs nicht nur nutzt, sondern deren interne Mechanik versteht, wenn die Dokumentation noch unzureichend ist.
// shared/lib/vt/PageTransition.tsx
export function PageTransition({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
// key={pathname} → Route-Änderung unmountet/remountet diese Boundary,
// was das "vtpage" Transition-Paar aktiviert (vt-page-out/in im CSS).
// update="none" → jeder andere Commit (Lazy Chunks, Data Refresh)
// wird von React übersprungen — kein Snapshot stört das laufende GSAP-Intro.
<ViewTransition key="{pathname}" name="vtpage" update="none">
<div>{children}</div>
</ViewTransition>
);
}
Galerie und Medien
yet-another-react-lightbox (React 19 kompatibel) + react-photo-album für ein Masonry-Layout der Projekt-Screenshots – Hoch- und Querformate zerschießen nicht die Zeilenhöhe. Markdown-Content aus dem CMS wird über react-markdown + remark-gfm mit einem benutzerdefinierten <img>-Renderer gerendert, der dieselben Bilder in der globalen Lightbox öffnet.
Mockups ohne eine einzige überflüssige Datei
Projekt-Screenshots, für die es keine echten Assets gibt, sind in CSS gezeichnete Browser-/Smartphone-Mockups (deterministische Auswahl basierend auf dem Projekt-Slug) und keine Platzhalter-Bilder. Das spart Ladezeit und löst elegant das Problem fehlender Assets bei Kundenprojekten unter NDA.
Formulare
React Hook Form + Zod-Validierung im Kontaktformular – ein typsicheres Validierungsschema, das zwischen Client- und potenzieller Server-Überprüfung geteilt wird.
Backend: Strapi + Postgres
Striktes Datenmodell
Kollektionen: Projekte, Tags, "Über mich" (Singleton), Kontaktanfragen – mit Relationen untereinander, keine flachen Tabellen. Die Feldlokalisierung ist differenziert durchdacht: Texte (Beschreibung, Aufgabe, Prozess, Ergebnis) sind pro Locale, während Systemfelder (Slug, Jahr, Stack, Links, Cover-Bild, Tag-Reihenfolge) für alle Locales geteilt werden. Dies schließt eine Datendesynchronisation zwischen den Sprachversionen desselben Projekts völlig aus.
Medien — im S3, nicht auf dem VPS-Speicher
Der Datei-Upload ist über @strapi/provider-upload-aws-s3 auf einen S3-kompatiblen Speicher (Ceph RGW) mit Auslieferung über ein CDN konfiguriert. Der Grund ist nicht nur, weil es "modern" ist, sondern sehr praktisch: Das Backend läuft in einem Docker-Container, der bei jedem Deploy neu gebaut wird. Die lokale Festplatte des Containers ist flüchtig (ephemeral), was bedeutet, dass ohne externen Speicher alle hochgeladenen Medien bei jedem Release verschwinden würden. Eingerichtet mit einem Graceful Fallback auf die lokale Festplatte, falls S3-Umgebungsvariablen fehlen – für reibungslose lokale Entwicklung ohne Cloud-Bucket.
Publishing ohne Rebuild der Website — On-Demand ISR
Wenn Inhalte veröffentlicht/aktualisiert werden, sendet Strapi einen Webhook an /api/revalidate im Frontend, welcher gezielt Next.js Cache-Tags invalidiert (projects, project:<slug>, about) – ohne vollständigen Redeploy und ohne den gesamten Cache der Website zu leeren. Das Aktualisieren einer Case Study setzt nicht den Cache der anderen zurück.
// app/api/revalidate/route.ts
switch (body.model) {
case 'project':
tags.add('projects');
if (body.entry?.slug) tags.add(`project:${body.entry.slug}`);
break;
// ...
}
// 'max' = stale-while-revalidate: Der Besucher erhält sofort die gecachte
// Seite, während im Hintergrund eine neue Version gebaut wird. Sollte das CMS
// während des Publishings abstürzen, bleibt die alte Seite bestehen (kein 500-Fehler).
for (const tag of tags) revalidateTag(tag, 'max');
Die bewusste Entscheidung für "ein Reload später, aber niemals ein 500er-Fehler" anstelle einer blockierenden, sofortigen Invalidierung ist ein typischer Trade-off, der in einem echten High-Uptime-Projekt anders aussieht als in einem reinen Demo-Projekt.
SEO-Metadaten
Title/Description werden aus dem CMS verwaltet, unterstützen aber dynamische Tokens wie {years} und {projects}. Die Jahre der Berufserfahrung und die Anzahl der Projekte werden on the fly berechnet (Karrierestart + aktueller CMS-Zähler), sodass die Texte nicht veralten und nicht jeden Januar manuell aktualisiert werden müssen.
// shared/lib/seo.ts
export function applyMetaTokens(text: string, projectsTotal: number): string {
const years = new Date().getFullYear() - site.careerStartYear;
const projects = projectsTotal;
return text.replaceAll('{years}', String(years)).replaceAll('{projects}', String(projects));
}
Im CMS wird der Text einmalig als "{years}+ Jahre kommerzielle Entwicklung" geschrieben – und bleibt für immer aktuell.
Benachrichtigungen und Feedback
Das Absenden des Kontaktformulars löst eine E-Mail an den Administrator aus (SMTP via Env-Variablen, mit demselben Graceful Fallback-Muster – ohne konfigurierte Zugangsdaten loggt das Formular die Anfrage lediglich und stürzt nicht ab).
Pläne
- Blog — Nutzt dasselbe Inhaltsmodell wie die Projekte (Strapi Collection + On-Demand ISR), lokalisiert in RU/EN/DE. Dient der Positionierung als Experte und bringt organischen SEO-Traffic auf die Website, anstatt sich nur auf die "Über mich"-Karte zu verlassen.
- Tests — Aktuell wird die Korrektheit durch TypeScript + Zod-Schemas an den Datengrenzen gewährleistet, aber es gibt noch keine automatisierte Verhaltensabdeckung: E2E (Playwright) für Schlüsselszenarien – Theme-/Sprachwechsel, Kontaktformular, View Transitions Navigation – und Unit-Tests für die Geschäftslogik in
entities/features. Eine natürliche Fortsetzung der etablierten FSD-Architektur, bei der Logik aus den Komponenten ausgelagert und somit isoliert testbar ist.
Was das insgesamt zeigt
- Full-Stack-Beherrschung: Von der Infrastruktur (Docker, S3, CDN, Webhooks) bis zum pixelgenauen Animations-Polishing im Browser – ohne Lücken dazwischen.
- Production Mindset, kein Demo-Code: Cache-Invalidierung, Flüchtigkeit von Containern, Graceful Fallbacks für die lokale Entwicklung, React StrictMode-Resilienz – das sind Dinge, die nur in realen Projekten auftreten.
- Bereitschaft für neue/experimentelle Technologien (Next.js 16, React 19, View Transitions API) und die Fähigkeit, deren interne Mechanik zu durchdringen, wenn die offizielle Dokumentation noch unzureichend ist.
- Design-Disziplin: Tokenisiertes Styling, durchdachte Typografie (Unbounded + Exo 2, mit kyrillischer Unterstützung), Minimalismus ohne Detailverlust.
- Sinnvolle Lokalisierung: 3 Sprachen auf Ebene von Inhalten und Metadaten, einschließlich separater, zielgruppengerechter PDF-Lebensläufe.
Die Ergebnisse
Sprachen mit automatischer Browser-Erkennung
serverseitig gerenderte Seiten
JS-Overhead für Client-Übersetzungen
CLS (Cumulative Layout Shift)