Renditeradar
Aggregator für Renditeobjekte: von der unsauberen Rohzeile bis zur Umkreissuche über einen räumlichen Index.
- Rolle
- Alleinentwickler, Arbeitsprobe für ein Mandat
- Zeitraum
- September 2026, an einem Tag
- Next.js 16
- React 19
- TypeScript
- Tailwind CSS v4
- PostgreSQL
- PostGIS
- Row Level Security
- Supabase
- Python 3.13
- Leaflet
- Vercel
- GitHub Actions
Worum es ging
Ausgeschrieben war ein Immobilien-Aggregator mit Scraping, Kennzahlen, Filtern, Renditerechner und Abo-Modell. Zwei Anforderungen daraus waren im Portfolio nicht belegt: Next.js mit statischer Erzeugung für Suchmaschinen, und PostgreSQL mit schnellen Filter- und Standortabfragen. Alles Übrige war es bereits. Also nicht ein kleines Abbild der ganzen Plattform, sondern genau diese zwei Belege, dafür vollständig und nachgewiesen. Umgesetzt an einem Tag, vom ersten Commit um 00:54 bis zum letzten Meilenstein um 16:03.
Eine Python-Pipeline erzeugt absichtlich unsaubere Rohdaten, normalisiert sie, erkennt Duplikate über einen Hash und schreibt sie mit berechneten Kennzahlen nach PostgreSQL. Eine Next.js-Anwendung zeigt die Objekte als gefilterte Liste und als statisch erzeugte Detailseiten mit Renditerechner, Vergleichsobjekten und Karte. Der gesamte Filterzustand steht in der Adresse: die Filterleiste ist ein Formular mit GET-Methode, Chips und Blättern sind Links, die Liste funktioniert ohne JavaScript. Bis zum Renditerechner gab es keine einzige Client Component, danach genau zwei.
Der Kern in einem Satz: Jede Behauptung hat einen ausgeführten Nachweis. Die Idempotenz durch einen zweiten Pipeline-Lauf (400 neu und 0 aktualisiert, dann 0 neu und 400 aktualisiert), die Indexnutzung durch den Abfrageplan, das Schreibverbot durch einen abgewiesenen Schreibversuch mit dem öffentlichen Schlüssel, HTTP 401. Kein Scraping realer Portale, keine Nutzerverwaltung, keine Zahlung: der Umfang ist auf das begrenzt, was hier neu gezeigt werden soll, und das steht im Repository ausdrücklich statt stillschweigend weggelassen.
Zahlen
- Objekte, idempotent geschrieben: der zweite Lauf ändert nichts
- 400 Objekte, idempotent geschrieben: der zweite Lauf ändert nichts
- Lighthouse in allen vier Kategorien auf der Detailseite
- 100 Lighthouse in allen vier Kategorien auf der Detailseite
- Nachbarsuche über den räumlichen Index, im Abfrageplan belegt
- 0.16 ms Nachbarsuche über den räumlichen Index, im Abfrageplan belegt
Einblicke




Ein räumlicher Index, der nicht benutzt wurde
Die Suche nach den fünf nächstgelegenen Objekten war als Verknüpfung der Tabelle mit sich selbst geschrieben. Sie lieferte die richtigen Ergebnisse in 0.63 Millisekunden, und bei 400 Zeilen sieht das nach fertig aus. Der Abfrageplan zeigte aber einen sequenziellen Durchlauf über alle Zeilen mit anschliessendem Sortieren: der GiST-Index stand da und wurde nicht angefasst. Ein Index für die Suche nach dem nächsten Nachbarn setzt voraus, dass eine Seite des Vergleichs feststeht. Der Bezugspunkt steht jetzt als skalare Unterabfrage im Ausdruck statt als Partner der Verknüpfung. Postgres wertet ihn einmal vorab aus, für den Planer ist er eine Konstante, und erst dann kann der Index den Distanzoperator bedienen. Der Plan zeigt danach einen Index Scan, die Zeit fällt auf 0.16 Millisekunden. Die Lehre steckt nicht in der Zahl, sondern darin, wo sie stand: die eine Variante wächst linear mit der Tabelle, die andere nicht, und bei 400 Zeilen ist das auf der Uhr unsichtbar. Der Unterschied steht im Plan.
Stack
- Frontend
- Next.js 16 mit App Router
- React 19
- TypeScript strict
- Server Components als Vorgabe
- zwei Client Components im ganzen Projekt
- 400 statische Detailseiten mit Erneuerung im Hintergrund
- Metadaten, JSON-LD und Sitemap über Dateikonventionen
- Gestaltung und Karte
- Tailwind CSS v4
- IBM Plex Sans über next/font
- Design-Token aus dem Mockup als CSS-Variablen
- Leaflet mit OpenStreetMap, nur im Browser nachgeladen
- Datenbank
- PostgreSQL 15 auf Supabase, Region Zürich
- PostGIS 3.3
- eine Tabelle mit 23 Spalten und 6 Indizes
- zusammengesetzter Index für die Listenfilter
- GiST-Index auf einer erzeugten Geography-Spalte
- Row Level Security mit genau einer Policy für select
- Pipeline
- Python 3.13
- psycopg 3
- Generator mit festem Startwert
- Normalisierung und Duplikat-Hash
- idempotenter Upsert in einer Transaktion
- Prüfung und CI
- 15 Tests mit pytest
- 36 Tests mit Vitest
- ruff, ESLint, Prettier, tsc
- GitHub Actions mit drei Workflows
- Lighthouse gegen die laufende Seite
- Betrieb
- Vercel
- Deployment aus main
- Produktionsadresse aus der Umgebung abgeleitet
Status
Alle sechs Meilensteine abgeschlossen, jeder mit Git-Tag und Nachweis. Demo öffentlich auf Vercel, Quellcode öffentlich auf GitHub unter MIT-Lizenz. Die 400 Objekte sind generiert, echt sind nur die 62 Gemeinden mit Koordinaten und Preisniveau.