Zum Inhalt springen
← Alle Projekte

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

Liste. Alle 400 Objekte, sortiert nach Bruttorendite, Filterleiste links, jede Karte ein Link.
Umkreissuche. 48 Objekte im Umkreis von 10 km um Olten, der aktive Filter als entfernbarer Chip, der ganze Zustand in der Adresse.
Detailseite. Sechs Kennzahlen, Eckdaten und der Renditerechner, statisch erzeugt und aus dem Zwischenspeicher ausgeliefert.
Vergleichsobjekte. Die fünf nächstgelegenen Objekte mit Distanz, gesucht über den räumlichen Index, die Liste unabhängig von der Karte.

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.