static-portfolio-build — Statisches mehrseitiges Portfolio
Technische Leitlinie: Nur HTML5 + CSS3 + Vanilla JS. Keine UI-Frameworks wie Bootstrap/Tailwind, keine externen CDN-Schriftarten. Primärfarben ≤ 3, Schriftarten ≤ 2, jedes Bild muss einen
alt-Attributwert haben (SVGs verwendenrole="img"+aria-label). Erster Bildschirm bei 3G ≤ 3s (geschätzt bei 1,6 Mbit/s / RTT 400 ms).
1. Architektur: Datengesteuert, einmal pflegen
Schreibe Portfolio-Inhalte nicht von Hand in jedes HTML. Extrahiere sie in JSON-Daten + eine Rendering-Schicht:
works-data.json Datenquelle (skriptgeneriert, nicht manuell bearbeiten)
works-data.js Gleiche Daten als Inline-Version (window.__WORKS_DATA__) – für file://-Doppelklick um CORS zu umgehen
works.js Rendering-Schicht: ROLES / cardHTML() / tileHTML() / bindFilter() / bindExpand()
extract-works.mjs Extrahiere aus alter Einzelseite
patch-works.mjs Neu nummerieren / Module ergänzen / Motive zuweisen (muss idempotent sein!)
Warum eine Inline-JS-Datenversion notwendig ist: fetch('./works-data.json') wird unter file://-Protokoll von CORS blockiert. Benutzer sehen beim Doppelklick einen leeren Bildschirm.
Idempotentes Patch-Skript: patch-works.mjs muss wiederholbar ausgeführt werden können. Verwende EXTRA_TITLES, um bereits gepatchte Einträge zu filtern – sonst wirft die zweite Ausführung einen Fehler wegen fehlender Zuordnungstabelle.
Modulnummern mit realen Verzeichnissen abgleichen: Nummeriere Werke nicht nach Kategorie, sondern nach der tatsächlichen Zugehörigkeit zu lokalen Verzeichnissen/GitHub-Repos. Orientiere dich an vorhandenen Verzeichnissen für deren Nummern und fülle Lücken für verzeichnislose Werke, um eine durchgehende Nummerierung ohne Lücken zu gewährleisten. Beim Ändern von Nummern alle Verweise auf der gesamten Website aktualisieren (Einstiegskarten, Banner der Rollenseiten, Kategorielisten auf der Über-Seite) – am besten die Einstiegskarten automatisch aus den Daten generieren lassen, um manuelle Inkonsistenzen zu vermeiden:
document.querySelectorAll('[data-owns]').forEach(function (el) {
var r = WORKS.ROLES[el.getAttribute('data-owns')];
if (r) el.textContent = 'Enthält ' + r.owns.length + ' Module (' + r.owns.join(' / ') + ')';
});
2. Was tun ohne Screenshots: Inline-SVG-Diagramme
Die meisten Werke haben keine Screenshots. Weise jeder Kategorie ein geometrisches Motiv zu und generiere Inline-SVGs (keine externen Bilder, keine Bildanfragen beim ersten Laden):
| Kategorie | Motiv | Elemente |
|---|---|---|
| Planung | Roadmap | Knoten + gestrichelte Verbindungen + Meilensteine |
| Workflow | Pipeline | Eingabe → Verarbeitung → Prüfung → Ausgabe + Pfeil-Marker |
| Agent | Neural | Geschichtete Knoten + vollständig verbundene Kanten |
| Daten | Metriken | Balken + Liniendiagramm-Trend |
| Open Source | Terminal | Dunkles Fenster + farbige Codezeilen + Cursor |
| Branche | Grid | Blaupausen-Raster |
Jedes muss role="img" + semantischen aria-label haben.
Fallstricke:
<text>in SVGs wird von Kartenverläufen verdeckt → Text aus SVG auslagern und mit HTML-Overlay.w-cover-tag(position:absolute) darstellenpreserveAspectRatioverwendemeetnichtslice, um Beschnitt zu vermeiden- Array-Index-Verschiebung: Motivdaten als
[x,y,Text,Farbe]geschrieben, aber Rendering liestl[2]als[x,y,width,color]→ Rechtecke sind alle deaktiviert, nur Cursor wird angezeigt. Stelle vor dem Definieren von Motiven sicher, dass die Index-Semantik konsistent ist.
3. 3D und Effekte (schön, aber ohne Ruckeln)
Hintergrund: SpaceX-artiges Perspektiv-Gitter
.bg-grid-floor {
position: absolute; left: -30%; right: -30%; top: 16%; height: 90%;
background-image:
repeating-linear-gradient(90deg, rgba(10,10,10,.16) 0 1px, transparent 1px 74px),
repeating-linear-gradient(180deg, rgba(10,10,10,.15) 0 1px, transparent 1px 40px);
transform: perspective(520px) rotateX(72deg) translateZ(0);
transform-origin: 50% 0%;
mask-image: linear-gradient(to bottom, transparent 0%, #000 11%, #000 52%, transparent 90%);
animation: floorDrift 30s linear infinite; /* background-position ändern, nicht transform */
}
Darüber eine Horizont-Lichtlinie und drei driftende Lichtflecken (jeweils 24/29/34s Atmung).
Performance-Eisernes Gesetz (Entscheidend für Ruckelfreiheit)
Verwende niemals filter: blur() und backdrop-filter. Diese beiden rendern bei jedem Frame den gesamten Hintergrund neu – die Hauptursache für Ruckeln beim Scrollen:
- Sanfte Lichtflecken mit
radial-gradient(circle, rgba(...).10, transparent 68%)– der Verlauf selbst ist bereits weich, kein Blur nötig - Milchglas-Navigation durch fast undurchsichtigen Hintergrund
rgba(255,255,255,.96)ersetzen - Abschnitte mit halbtransparentem weißen Hintergrund polstern, kein backdrop-filter
Animationen nur auf transform / opacity (vom Compositor verarbeitet, lösen kein Reflow/Repaint aus). Scroll-Parallaxe mit requestAnimationFrame throttlen und translate3d.
3D-Kippen von Karten
pointermove schreibt CSS-Variablen, ohne das DOM zu berühren:
.tilt { transform: perspective(1000px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)); }
.tilt.is-tilting { transition: transform .06s linear; } /* Reduzierte Transition bei Berührung */
.tilt .w-body { transform: translateZ(18px); } erzeugt schwebenden Parallax-Effekt; ::before radialer Glanz folgt --mx/--my.
Hover-Verschiebung und Eintritts-Animation konkurrieren um transform → Hover ändert nur box-shadow/border-color, Verschiebung wird komplett von .tilt übernommen.
4. Eintritts-Animation: Niemals reines CSS opacity:0
Das ist die größte Falle. Reines CSS versteckt initial + JS fügt .is-revealed hinzu – wenn IntersectionObserver nicht zurückruft (Headless-Browser, JS-Fehler, anormaler Pfad), bleibt der gesamte Seiteninhalt dauerhaft unsichtbar.
Richtige Vorgehensweise: JS fügt bei Bedarf die versteckte Klasse hinzu, Elemente ohne Klasse sind standardmäßig sichtbar:
.w-card.reveal-armed { opacity: 0; transform: translateY(18px); }
.is-revealed { opacity: 1 !important; transform: none !important; }
list.forEach(function (el) {
var r = el.getBoundingClientRect();
var inView = r.top < innerHeight + 150 && r.bottom > -150;
if (!inView) el.classList.add('reveal-armed'); // Nur außerhalb des sichtbaren Bereichs verstecken
io.observe(el);
});
// Fallback: Nach 3 Sekunden noch nicht sichtbare Elemente erzwingen
setTimeout(function () { /* force reveal */ }, 3000);
Elemente im sichtbaren Bereich sind sofort zu sehen, außerhalb werden sie erst bewaffnet – der erste Bildschirm wird nicht durch Animationen verlangsamt, und anormale Pfade führen nicht zu einem leeren Bildschirm.
5. Skript-Injection: Prüfe <script>-Paarung
Beim Batch-Injizieren von Code in mehrere Seiten muss überprüft werden, dass die Anzahl von <script> und </script> gleich ist.
Realer Vorfall: Beim Injizieren ein öffnendes <script>-Tag zu viel geschrieben → der nachfolgende Inline-Skriptblock wurde vom JS-Parser verschluckt → der Rendering-Code aller vier Rollenseiten war komplett deaktiviert. Die Seite sah völlig normal aus (Titel, Navigation, Aufgabenliste vorhanden), nur der Werkbereich war leer – kaum erkennbar.
Beachte bei der Verwendung von regulären Ausdrücken: LastIndexOf('<script>') könnte <script src=...> treffen, wodurch die Definition außerhalb des Tags platziert und als sichtbarer Text angezeigt wird. Verwende stattdessen einen expliziten Anker-String.
6. Verifikationsmethoden (wiederverwendbar)
Screenshot-Verifikation (Wichtig: Gleichnamige Dateien werden gecached)
vision_html_screenshot cached bei gleichem Dateinamen. Nach CSS-Änderungen muss die Datei mit Copy-Item in ein temporäres Verzeichnis .verify/ kopiert und dann als Screenshot aufgenommen werden. Nach der Prüfung die Sandbox löschen.
Synchroner Probe (Nicht load + setTimeout verwenden)
In Headless-Umgebungen sind window.load + setTimeout unzuverlässig. Schreibe den Probe direkt in den Render-Callback (synchron), ausgegeben in ein <pre> mit position:fixed;bottom:0:
WORKS.loadData(function (data) {
root.innerHTML = WORKS.listByRole(data.items, 'fde');
var d = document.createElement('pre');
d.style.cssText='position:fixed;bottom:0;left:0;right:0;z-index:99999;background:#000;color:#0f0;font:11px monospace;padding:4px;margin:0';
d.textContent = 'CARDS=' + root.querySelectorAll('.w-card').length;
document.body.appendChild(d);
});
⚠️ Verwende für den Probe nicht top-Positionierung – das 3D-Kippen der Karten (transform) könnte das fixed-Element verschieben.
Batch-Prüfung (Schneller als seitenweises Lesen)
- Tote Links: Regex
./xx/yy.mdextrahieren →Test-Patheinzeln prüfen - Nummern-Kontinuität:
Compare-Objecterwartete Sequenz vs. tatsächliche - Verzeichnisabgleich: PowerShell vergleicht
'01'als int 1 – muss mit[string]umwandeln - Teure Features übrig:
[regex]::Matches($css,'filter:\s*blur|backdrop-filter'), dabei Kommentare unterscheiden
7. Deployment auf GitHub Pages
gh repo create <user>/<repo> --public --description "..."
git remote add origin https://github.com/<user>/<repo>.git
git push -u origin main
Vor dem Deployment prüfen:
git ls-filesüberprüfen, ob die Abhängigkeitsverzeichnisse wirklich getrackt werden (lokale Dateien ≠ git getrackt)- Bei bereits vorhandenem entfernten Inhalt zuerst
git ls-tree --name-only origin/mainvergleichen, nichts überschreiben - Lokale relative Pfade zu benachbarten Projektverzeichnissen: Entweder beide pushen oder in GitHub-Repo-Adressen ändern
- GitHub Pages benötigt Settings → Pages → Source muss Branch auswählen
- Bei Sub-Apps (z. B.
hitech-self-assessment/) darauf achten, dass sie nicht durch die Hauptseite überschrieben werden
Performance-Schätzung: Gesamtbytes / 8 / 1.6e6 + 4 * 0.4 (Sekunden)
8. Selbstcheck vor Auslieferung (Abgleich mit web-delivery-checklist)
- Alle vier/fünf Seiten per Screenshot verifiziert (nicht nur die Startseite)
- Tote Links: 0, fehlende
alt: 0 -
<script>-Paarung geschlossen, CSS-Klammern ausgeglichen - Tatsächliche Anzahl von
filter: blur/backdrop-filter= 0 - 3G-Schätzung < 3s
- Primärfarben ≤ 3, Schriftarten ≤ 2, null externe CDN-Ressourcen
- Auf mobilen Geräten (390px) Hamburger-Menü auf-/zuklappbar,
aria-expandedsynchronisiert - Bei
prefers-reduced-motionalle Animationen deaktiviert