
Unternehmenskatalog
Katalogseite und Verwaltungspanel für einen LED-Leuchtenhersteller mit Sitz in İzmir. Warenkorb, Zahlung und Konten liegen bewusst außerhalb des Umfangs: Die einzige Aufgabe der Website ist es, Besuchende in ein WhatsApp- oder Telefongespräch zu überführen. Produktseiten werden aus der Datenbank gelesen und zur Build-Zeit statisch erzeugt.
Der Betrieb ist reine B2B-Fertigung: Ein Elektrobetrieb oder ein Auftragnehmer liefert die Maße, es wird gefertigt, und das Produkt geht an dessen eigene Kundschaft. Direktverkauf an Endverbraucher gibt es nicht. In einem solchen Geschäft ist die Standardantwort — "fügen wir der Unternehmensseite E-Commerce hinzu" — nicht nur unnötig, sondern schädlich: Sie lädt Bestand, Zahlung, Rückgabe und eine Preisliste auf einen Ablauf, der nichts verkauft, und beschädigt durch öffentliche Preise zugleich die Verhandlungsgrundlage. Das eigentliche Problem ist nicht der Verkauf, sondern Vertrauen zu schaffen und ein Gespräch zu beginnen. Der zweite Punkt war das Timing: Die Domain war vorhanden, der Inhalt nicht fertig — und die Seite musste dennoch etwas zeigen.
Drei Besuchertypen, ein Konversionsziel. Elektrobetriebe und Elektro-Auftragnehmer: bestellen wiederholt, suchen im Katalog Produkttyp und technische Angaben, den Preis holen sie ohnehin im Gespräch. Architektinnen und Projektverantwortliche: schauen weniger auf Standardprodukte als auf die Kapazität für Sonderanfertigung. Die Endkundschaft des Elektrobetriebs: sieht die Seite als Vertrauensreferenz — der Betrieb zeigt sie ihr. Im Panel gibt es genau eine Person: die Inhaberseite, die Produkte und eingehende Anfragen pflegt.
Einzelner Entwickler. Die Umfangsentscheidung selbst, Datenmodell, Katalog und Struktur der statischen Erzeugung, Verwaltungspanel, Anfrageablauf und Bereitstellung.
Next.js 16 App Router, Neon Postgres und Drizzle. Die Anwendung ist zweigeteilt: die öffentliche Website und das Panel unter `/admin`. Produkt- und Kategorieseiten entstehen über `generateStaticParams` zur Build-Zeit, und die Parameterliste wird aus der Datenbank gelesen — die statische Ausgabe leitet sich also aus den Inhalten des Panels ab. In diesem Entwurf erreicht eine Besucheranfrage die Datenbank nie; nur das Kontaktformular schreibt auf den Server. Die Panel-Seite ist vollständig dynamisch: Produkt- und Variantendefinitionen, Kundendatensätze, Verkäufe und Verkaufspositionen, eingehende Anfragen. Bilder liegen im Objektspeicher; ihre Maße werden beim Upload gelesen und in den Datensatz geschrieben, sodass die Seitenverhältnisse beim Rendern bekannt sind. Die Sitzung wird in einem signierten Token geführt. Der Veröffentlichungsschalter ist eine einzige Umgebungsvariable: Ist er aktiv, reduziert sich die Anwendung auf einen Bildschirm, während der Rest der Struktur bestehen bleibt.
Die Begründung ist kaufmännisch, nicht technisch: In diesem Geschäft entsteht ein Auftrag über Maße und ein Gespräch, nicht über ein Produkt im Regal. Ein Warenkorb hätte Bestandsabgleich, Zahlungsanbindung, Rückgabeprozess und Preistransparenz auf einen Ablauf geladen, der zum Verkauf nichts beiträgt. Der engere Umfang hat die Architektur direkt vereinfacht: keine Sitzung auf Besucherseite, kein Warenkorbzustand, keine Bestandssperre.
KompromissDie Website ist für sich kein Vertriebskanal; Konversion ist nur bis zum Klick messbar, alles danach passiert im WhatsApp-Gespräch und hinterlässt im System keine Spur. Ob aus einer Anfrage ein Auftrag wurde, lässt sich im Panel nicht ablesen und wird von Hand nachgehalten.
Der Katalog ändert sich selten, und der Besucherverkehr besteht ausschließlich aus Lesevorgängen. Seiten zur Build-Zeit zu erzeugen, beseitigt die Datenbankverbindung zur Laufzeit: Eine Besucheranfrage geht nie an die Datenbank, also sind Verbindungspooling, Kaltstarts und Abfragelatenz auf Besucherseite kein Thema.
KompromissDie Aktualität des Katalogs hängt an der Bereitstellung: Ein im Panel korrigierter Tippfehler erreicht die Website erst mit dem nächsten Build. Das ist vertretbar, weil sich der Katalog selten ändert; bei einem Katalog, der sich mehrmals täglich ändert, wäre dieselbe Entscheidung falsch.
Die Domain sollte nicht leer stehen, solange der Inhalt unfertig war. Statt eine eigene "Demnächst"-Seite aufzusetzen, kam ein Schalter in die Anwendung: Ist er aktiv, reduziert sich die Seite auf einen Bildschirm — Marke, kurze Beschreibung, direkter Kontakt — ist er aus, öffnet sich die vollständige Website. Es gibt kein zweites Projekt zu pflegen.
KompromissDie Anwendung trägt zwei Erscheinungsbilder, und eines davon wird im Alltag nie geöffnet; ein defekter Demnächst-Modus fiele erst beim Einschalten auf. Da der Schalter eine clientseitig sichtbare Variable ist, ist er zudem keine Sicherheitsgrenze, sondern nur ein Anzeigeschalter.
Bei einem Leuchtenprodukt leistet das Bild mehr als der Text, und die Seite besteht überwiegend aus Bildern. Weil die Maßangabe im Datensatz steht, ist das Seitenverhältnis beim Rendern bekannt, und vor dem Laden verschiebt sich nichts. Die Alternative — das Bild bei jedem Seitenbau zu vermessen — hätte die Build-Zeit direkt proportional zur Bildanzahl wachsen lassen.
KompromissDie Maßangabe beruht auf einer einzigen Messung beim Upload; wird ein Bild im Objektspeicher extern ersetzt, bleibt das gespeicherte Maß still falsch. Einen Job, der neu misst, gibt es nicht.
Drizzle und Neon halten das vom Panel benötigte relationale Modell — Produkt, Kundschaft, Verkauf, Verkaufsposition — samt Migrationshistorie lesbar. Der Objektspeicher hält Bilder aus Repository und Datenbank heraus, ohne die Build-Ausgabe aufzublähen. Für Sitzungen genügte ein signiertes Token statt eines vollständigen Auth-Frameworks: Das Panel hat eine Nutzerklasse und braucht weder Registrierung noch Passwort-Reset noch Anbieter-Login — eine Abhängigkeit mitzuschleppen, die nicht genutzt wird, lohnt nicht. Turnstile schützt das Kontaktformular, weil es der einzige Konversionspunkt ist. Zod arbeitet sowohl bei Formulareingaben als auch bei der Prüfung der Umgebungsvariablen.
Das Panel ist durch ein signiertes Sitzungstoken geschützt, und die Panel-Routen sind vollständig von der Website getrennt. Das Kontaktformular liegt hinter Turnstile, geprüft wird serverseitig — ein vom Client geliefertes Prüfergebnis wird für sich nicht akzeptiert. Der Schreibpfad ist Server Action → Zod → Drizzle; einen anderen Schreibpfad gibt es auf Besucherseite nicht. Kunden- und Verkaufsdatensätze sind nur im Panel sichtbar; keine öffentliche Seite liest diese Tabellen, und die statische Erzeugung berührt sie überhaupt nicht. Umgebungsvariablen werden mit Zod geprüft.
Für dieses Projekt liegt keine gespeicherte, mit Datum versehene Lighthouse-Ausgabe vor, deshalb steht hier kein Seitengeschwindigkeits-Score. Das strukturelle Maß lautet: Auf Besucherseite gibt es zur Laufzeit keine Datenbankabfrage, die Seiten werden als Build-Ergebnis ausgeliefert. Da die Seitenverhältnisse aus dem Datensatz gelesen werden, entsteht beim Laden kein Layout-Sprung. Eine Bundle-Analyse liegt als fertiges Kommando im Repository.
In diesem Repository gibt es keine automatischen Tests, und am stärksten fehlt das an der Verbindung zwischen statischer Erzeugung und Panel: Nichts prüft, ob ein im Panel depubliziertes Produkt bis zum nächsten Build noch auf der Website steht. Heute würde ich zumindest die Funktion, die die Erzeugungsparameter liefert, rein machen und testen — genau das, was ich bei Ustura mit der Verfügbarkeitsberechnung getan habe, wo eine reine Funktion Sonderfälle mit einem einzigen Aufruf prüfbar machte. Der zweite Punkt ist der Umfang: Ich stehe dazu, Warenkorb und Zahlung wegzulassen, doch ob aus einer Anfrage ein Auftrag wurde, ist im System unsichtbar. Heute würde ich der Anfrage ein einfaches Statusfeld geben; das wäre kein CRM, sondern das Festhalten des Gesprächsergebnisses — die Statushistorie der Buchungen bei VAP Turizm leistet genau das.
Sagen Sie mir, was Sie bauen wollen; ich sage Ihnen vorab, wie lange es dauert und wo man anfängt.