Node Version für Coolify/Nixpacks festlegen
This commit is contained in:
+292
@@ -0,0 +1,292 @@
|
||||
# Code-Review: webkulisse-saas (TanStack Start)
|
||||
|
||||
**Stack:** TanStack Start + Router (file-based), React 19 + Compiler, better-auth 1.5 (admin-Plugin), Drizzle + Postgres, Tailwind 4, shadcn (radix-vega), Nitro, Biome
|
||||
**Stand:** Landingpage + `/termin` + Auth + Admin-Userverwaltung + Dashboard-Platzhalter
|
||||
|
||||
**Gesamteindruck:** Die Grundstruktur ist gut. Route-Groups (`_authenticated`, `_admin`) sind richtig gewählt, Guards sitzen am Layout statt an jeder Seite, better-auth macht die Schwerarbeit statt selbstgebautem Auth, und die Kommentare in `config.ts` / `booking-dialog.tsx` zeigen, dass du über Entscheidungen nachdenkst. Das Lazy-Loading des cal.com-Bundles ist ein sauberer Move.
|
||||
|
||||
Die Probleme liegen weniger im Auth-Code selbst als in drei Bereichen: **Deploy-Reife** (Devtools, Env-Vars, Rechtstexte), **Datenfetching-Muster** (das ist dein Spaghetti-Risiko für Rechnungen/Abnahmen), und **Frontend-Details**, die dem eigenen Marketing-Versprechen widersprechen.
|
||||
|
||||
---
|
||||
|
||||
## KRITISCH — vor dem Livegang
|
||||
|
||||
### K1. Impressum und Datenschutzerklärung sind tote Links
|
||||
`src/components/footer.tsx:111,116` — beide `href="#"`.
|
||||
|
||||
§5 DDG (ehem. TMG) Impressumspflicht und DSGVO Art. 13 sind bei einer gewerblichen Seite nicht optional. Abmahnrisiko besteht real. Zusätzlich brisant: Du verkaufst im Klassik-Paket „Rechtstexte-Assistenz" — eine Agenturseite ohne eigenes Impressum untergräbt genau dieses Verkaufsargument.
|
||||
|
||||
Die Datenschutzerklärung braucht außerdem einen Absatz zum cal.com-Embed (`termin.hurler-webdesign.de`). Dass du selbst hostest, ist gut — das erspart dir den US-Drittlandtransfer, aber die iframe-Einbindung und die dort erhobenen Daten gehören trotzdem beschrieben.
|
||||
|
||||
**Fix:** Zwei echte Routen `/impressum` und `/datenschutz` anlegen, im Footer verlinken, beide mit `robots: noindex` ist *nicht* nötig — die sollen indexiert werden.
|
||||
|
||||
### K2. TanStack Devtools laufen in Produktion mit
|
||||
`src/routes/__root.tsx:94-104` — `<TanStackDevtools>` wird unbedingt gerendert. Zusätzlich stehen `@tanstack/react-devtools`, `@tanstack/react-router-devtools` und `drizzle-kit` in `dependencies` statt `devDependencies` (`package.json`).
|
||||
|
||||
Folge: Jeder Besucher lädt das Devtools-Bundle, und der komplette Route-Tree inklusive aller Admin-Pfade ist im Browser inspizierbar. Kein direktes Auth-Leck, aber unnötige Angriffsfläche und deutlich mehr JS auf einer Seite, die mit „unter 1 Sekunde Ladezeit" wirbt.
|
||||
|
||||
**Fix:**
|
||||
```tsx
|
||||
{import.meta.env.DEV && <TanStackDevtools ... />}
|
||||
```
|
||||
Plus die drei Pakete nach `devDependencies` verschieben.
|
||||
|
||||
### K3. Keine Fail-Fast-Prüfung der Environment-Variablen
|
||||
`src/lib/auth.ts:11-15`, `src/db/index.ts:5`
|
||||
|
||||
```ts
|
||||
baseURL: process.env.BETTER_AUTH_URL, // undefined möglich
|
||||
secret: process.env.BETTER_AUTH_SECRET, // undefined möglich
|
||||
trustedOrigins: process.env.BETTER_AUTH_URL ? [...] : undefined,
|
||||
db = drizzle(process.env.DATABASE_URL!, ...) // Non-Null-Assertion kaschiert den Fehler
|
||||
```
|
||||
|
||||
Der `trustedOrigins`-Fallback ist der gefährlichste Teil: Fehlt `BETTER_AUTH_URL` im Deployment, fällt der Origin-Check weg bzw. leitet sich aus Request-Headern ab. Das ist die Basis des CSRF-Schutzes bei better-auth. Ein vergessenes Env-Var in Coolify und die App startet klaglos mit deaktiviertem Schutz.
|
||||
|
||||
**Fix:** Ein `src/lib/env.ts`, das beim Boot validiert und bei fehlenden Werten wirft — kein `!`, kein stiller Fallback:
|
||||
```ts
|
||||
function required(name: string): string {
|
||||
const v = process.env[name]
|
||||
if (!v) throw new Error(`Fehlende Umgebungsvariable: ${name}`)
|
||||
return v
|
||||
}
|
||||
export const env = {
|
||||
DATABASE_URL: required('DATABASE_URL'),
|
||||
BETTER_AUTH_URL: required('BETTER_AUTH_URL'),
|
||||
BETTER_AUTH_SECRET: required('BETTER_AUTH_SECRET'),
|
||||
}
|
||||
```
|
||||
Dazu eine `.env.example` ins Repo (fehlt aktuell komplett).
|
||||
|
||||
---
|
||||
|
||||
## HOCH
|
||||
|
||||
### H1. Passwort-Reset beendet keine Sessions — die UI behauptet das Gegenteil
|
||||
`src/routes/_admin/admin.users.$userId.tsx:246-256, 270-273`
|
||||
|
||||
Der Text sagt: *„Überschreibt das aktuelle Passwort sofort. Der Kunde muss sich neu anmelden."* — `authClient.admin.setUserPassword()` invalidiert bestehende Sessions aber nicht. Wer schon eingeloggt ist, bleibt es.
|
||||
|
||||
Das ist genau der Fall, der zählt: Kunde meldet „mein Zugang wurde kompromittiert", du setzt ein neues Passwort, und die Session des Angreifers läuft weiter (bis zu 7 Tage, `expiresIn`).
|
||||
|
||||
**Fix:** `revokeUserSessions()` direkt nach `setUserPassword()` mitlaufen lassen — oder die UI-Aussage korrigieren. Ersteres.
|
||||
|
||||
### H2. Guards nur in `beforeLoad` — heute ok, ab Rechnungen nicht mehr
|
||||
`src/routes/_authenticated.tsx`, `src/routes/_admin.tsx`
|
||||
|
||||
Aktuell sicher, weil *alle* schreibenden Aktionen über das better-auth-Admin-Plugin laufen, das serverseitig selbst prüft. Die `beforeLoad`-Guards sind reines UX-Routing.
|
||||
|
||||
Das kippt in dem Moment, wo du deine erste eigene Server-Function für Rechnungen schreibst. `beforeLoad` läuft bei Client-Navigation im Browser — es ist keine Autorisierung. Wenn `getInvoicesFn()` sich darauf verlässt, dass der Nutzer „ja über `_authenticated` gekommen ist", kann jeder die Server-Function direkt aufrufen.
|
||||
|
||||
**Fix jetzt, nicht später** — das ist die wichtigste Architekturentscheidung im ganzen Review. Leg eine Middleware an, bevor die erste Rechnungs-Function existiert:
|
||||
|
||||
```ts
|
||||
// src/lib/middleware.ts
|
||||
import { createMiddleware } from '@tanstack/react-start'
|
||||
import { getRequest } from '@tanstack/react-start/server'
|
||||
import { auth } from '#/lib/auth.ts'
|
||||
|
||||
export const authMiddleware = createMiddleware({ type: 'function' })
|
||||
.server(async ({ next }) => {
|
||||
const session = await auth.api.getSession({ headers: getRequest().headers })
|
||||
if (!session) throw new Error('UNAUTHORIZED')
|
||||
return next({ context: { session } })
|
||||
})
|
||||
|
||||
export const adminMiddleware = createMiddleware({ type: 'function' })
|
||||
.middleware([authMiddleware])
|
||||
.server(async ({ next, context }) => {
|
||||
if (context.session.user.role !== 'admin') throw new Error('FORBIDDEN')
|
||||
return next({ context })
|
||||
})
|
||||
```
|
||||
|
||||
Regel fürs Team-of-one: **Jede** `createServerFn` bekommt eine dieser beiden Middlewares. Keine Ausnahme. Und jede Rechnungsabfrage filtert zusätzlich auf `context.session.user.id` — nicht auf eine ID aus den Function-Parametern, sonst hast du direkt ein IDOR.
|
||||
|
||||
### H3. Kein Passwort-Reset für Kunden, Klartext-Initialpasswörter
|
||||
`src/routes/_admin/admin.users.new.tsx:89-100`, `src/lib/auth.ts:25-30`
|
||||
|
||||
- Initialpasswort als `type="text"` — steht im Klartext auf dem Bildschirm. Bewusste Entscheidung (du musst es ja weitergeben), aber es landet auch im Browser-Autofill und ggf. in Screenshots.
|
||||
- Kein Zwang, es nach dem ersten Login zu ändern. Es gilt unbegrenzt.
|
||||
- Kein `sendResetPassword` konfiguriert → Kunde, der sein Passwort vergisst, muss dich anrufen.
|
||||
- Gleichzeitig existieren Rate-Limit-Regeln für `/forget-password` und `/reset-password` (`auth.ts:47-48`) — toter Code für Endpunkte, die nicht funktionieren.
|
||||
|
||||
**Fix, in der Reihenfolge:** Kurzfristig einen Passwortgenerator statt Freitextfeld (16 Zeichen, Copy-Button). Mittelfristig `sendResetPassword` mit Mailer konfigurieren und den Einladungs-Flow auf Setup-Link statt Klartext-Passwort umstellen. Dann wird auch das Rate-Limiting oben wieder sinnvoll.
|
||||
|
||||
### H4. Rate-Limiting im Speicher
|
||||
`src/lib/auth.ts:43` — `storage: 'memory'`
|
||||
|
||||
Setzt sich bei jedem Deploy zurück und wirkt pro Prozess. Bei einem Container auf Coolify ist das mager, aber tolerabel; sobald du skalierst oder häufig deployst, ist der Brute-Force-Schutz (`max: 5` auf `/sign-in/email`) faktisch offen.
|
||||
|
||||
**Fix:** `storage: 'database'` — die Tabelle hast du eh schon, und du brauchst kein Redis dafür.
|
||||
|
||||
### H5. `redirect`-Search-Param wird gesetzt, aber nie ausgewertet
|
||||
`src/routes/_authenticated.tsx:8`, `src/routes/_admin.tsx:8` schreiben `search: { redirect: location.href }`. `src/routes/login.tsx` liest ihn nie und navigiert immer nach `/dashboard` bzw. `/admin`.
|
||||
|
||||
Zwei Probleme: Deep-Links nach dem Login sind kaputt (Kunde klickt Link zu einer Rechnung, loggt sich ein, landet auf dem Dashboard). Und `/login` hat kein `validateSearch` — der Param ist untypisiert.
|
||||
|
||||
Wenn du ihn später auswertest, unbedingt validieren, sonst hast du einen Open Redirect:
|
||||
```ts
|
||||
validateSearch: (search) => ({
|
||||
redirect: typeof search.redirect === 'string' && search.redirect.startsWith('/')
|
||||
? search.redirect
|
||||
: undefined,
|
||||
})
|
||||
```
|
||||
Nur relative Pfade. `//evil.com` fängt `startsWith('/')` nicht ab — also zusätzlich `!redirect.startsWith('//')`.
|
||||
|
||||
### H6. Login und Dashboard sind für Suchmaschinen freigegeben
|
||||
`src/routes/__root.tsx:43` setzt global `robots: index,follow`, kein Override in `login.tsx`, `dashboard.tsx` oder den Admin-Routen.
|
||||
|
||||
Der Kundenlogin landet im Google-Index. Zusätzlich erbt jede Route den harten Canonical `https://webkulisse.de/` (`__root.tsx:79`) — `/login` zeigt kanonisch auf die Startseite, was SEO-technisch Unsinn ist.
|
||||
|
||||
**Fix:** In allen Routen unter `_authenticated` / `_admin` sowie auf `/login` ein `head: () => ({ meta: [{ name: 'robots', content: 'noindex,nofollow' }] })`. Canonical aus dem Root entfernen und pro Route setzen — `termin.tsx` macht das schon richtig, das ist das Muster.
|
||||
|
||||
---
|
||||
|
||||
## MITTEL — Code-Qualität, hier entsteht der Spaghetti-Code
|
||||
|
||||
### M1. Datenfetching per `useEffect` statt Router-Loader
|
||||
`admin.index.tsx:25-39`, `admin.users.$userId.tsx:35-61`
|
||||
|
||||
Beide Seiten haben handgeschriebenes `useState` für Daten, Loading und Error plus `useEffect` zum Laden. Das ist bei zwei Seiten überschaubar. Bei Rechnungen, Abnahmen, Dokumenten und Nachrichten sind es acht Seiten mit acht Kopien derselben fünf Zeilen — und genau das wolltest du vermeiden.
|
||||
|
||||
TanStack Router hat dafür Loader. Damit bekommst du Prefetching bei Hover (`defaultPreload: 'intent'` hast du schon aktiv), automatische Pending-States, `router.invalidate()` statt manuellem `load()`, und die Daten sind typisiert über `Route.useLoaderData()`.
|
||||
|
||||
```tsx
|
||||
export const Route = createFileRoute('/_admin/admin/')({
|
||||
loader: () => listUsersFn(),
|
||||
component: AdminUsersPage,
|
||||
pendingComponent: () => <p>Lade Kunden …</p>,
|
||||
errorComponent: ({ error }) => <ErrorBox error={error} />,
|
||||
})
|
||||
```
|
||||
|
||||
Für Mutationen mit Server-State (Rechnungsstatus, Abnahmen) würde ich zusätzlich TanStack Query dazunehmen — steht noch nicht im `package.json`, passt aber nahtlos zum Router.
|
||||
|
||||
### M2. Race Condition beim Laden des Users
|
||||
`admin.users.$userId.tsx:59-61` — `useEffect(() => { void load() }, [userId])` ohne Abbruch. Wechselt `userId` schnell, kann die ältere Response die neuere überschreiben. Löst sich mit M1 automatisch auf.
|
||||
|
||||
### M3. Fehler beim Sperren werden verschluckt
|
||||
`admin.index.tsx:41-50` — `toggleBan()` ignoriert das Ergebnis von `banUser`/`unbanUser` komplett. Schlägt der Call fehl, sieht der Admin nichts; erst `load()` zeigt, dass sich nichts geändert hat. Bei einer Sicherheitsfunktion („Kunde sperren") ist stiller Fehlschlag die falsche Wahl.
|
||||
|
||||
### M4. Zwei Quellen der Wahrheit für die Session
|
||||
`src/components/header.tsx:20` nutzt `authClient.useSession()` (Client-Fetch), während der Router die Session bereits über `__root.tsx:25-28` im Context hat.
|
||||
|
||||
Folge: zusätzlicher Request auf der Landingpage für jeden anonymen Besucher, plus ein Flackern des Buttons von „Kundenlogin" zu „Zum Dashboard" nach der Hydration.
|
||||
|
||||
**Fix:** `useRouteContext({ from: '__root__' })` und die vorhandene Session nutzen.
|
||||
|
||||
### M5. Zwei Import-Aliase parallel
|
||||
`#/` (29 Stellen, definiert in `package.json` imports) und `@/` (14 Stellen, definiert in `tsconfig.json` paths). Teils in derselben Datei-Ebene: `login.tsx` nutzt `#/`, `termin.tsx` nutzt `@/`. `components.json` ist auf `#/` konfiguriert, neue shadcn-Komponenten kommen also mit `#/`.
|
||||
|
||||
**Fix:** Auf `#/` vereinheitlichen (das ist der Node-Standard und passt zu shadcn), `@/*` aus der tsconfig entfernen, dann fängt TypeScript Rückfälle ab.
|
||||
|
||||
### M6. Schema-Drift: `todos`-Tabelle
|
||||
`drizzle/0000_majestic_hammerhead.sql:38-42` legt eine `todos`-Tabelle an, die in `src/db/schema.ts` nicht existiert. Überbleibsel aus dem CTA-Template. Ein `db:generate` erzeugt jetzt eine Drop-Migration — oder du hast eine Waisen-Tabelle in Produktion.
|
||||
|
||||
Nebenbei: `todos.json` steht im `.gitignore`, gehört zum selben Rest.
|
||||
|
||||
### M7. Seed-Script ist gitignored
|
||||
`.gitignore` enthält `scripts`, `package.json` referenziert `tsx scripts/seed-admin.ts` als `db:seed`. Auf einem frischen Clone existiert das Script nicht — du kannst keinen initialen Admin anlegen, und es gibt keinen anderen Weg (`disableSignUp: true`).
|
||||
|
||||
**Fix:** `scripts` aus `.gitignore` raus (falls das Secrets enthält: Secrets über Env-Vars, nicht über gitignorierte Scripts).
|
||||
|
||||
### M8. Keine Error- und NotFound-Boundaries
|
||||
Nirgends `errorComponent`, `notFoundComponent` oder `defaultNotFoundComponent`. Ein Fehler in einem Loader zeigt aktuell den nackten TanStack-Default. Mindestens im Root definieren.
|
||||
|
||||
### M9. `account.issuer` ist NOT NULL
|
||||
`schema.ts:35`. Bitte prüfen, ob better-auth 1.5 dieses Feld bei reinen Credential-Accounts (E-Mail/Passwort) immer befüllt — falls nicht, schlägt `createUser` fehl. Da du aktuell keine OAuth-Provider hast, ist das der einzige Pfad, der Accounts anlegt. Falls es funktioniert: gut, dann nur zur Kenntnis. Falls nicht: `.notNull()` entfernen.
|
||||
|
||||
### M10. Fehlende Indizes
|
||||
`session.user_id` und `account.user_id` haben Foreign Keys, aber keine Indizes. Bei jedem `revokeUserSessions` und jedem Login-Lookup ein Full Scan. Aktuell bei einer Handvoll Kunden irrelevant, aber ein Zweizeiler:
|
||||
```ts
|
||||
index('session_user_id_idx').on(table.userId)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DESIGN & FRONTEND
|
||||
|
||||
### D1. Deine Hausschrift lädt nicht
|
||||
`src/styles.css:9-14`:
|
||||
```css
|
||||
src: url('/public/fonts/VendSans-VariableFont_wght.woff2');
|
||||
```
|
||||
Der `public/`-Ordner *ist* der Web-Root. Der korrekte Pfad ist `/fonts/VendSans-VariableFont_wght.woff2`. Aktuell: 404, `--font-sans` fällt auf System-Sans zurück. Die gesamte Seite rendert also in einer anderen Schrift als du designt hast.
|
||||
|
||||
Dazu fehlt `font-display: swap` — ohne das gibt es einen unsichtbaren Text-Blitz beim Laden.
|
||||
|
||||
### D2. Ungenutzte Schriften und Bilder — 6 MB im `public/`
|
||||
- `nunito-sans` wird in `styles.css:5` importiert, aber nirgends verwendet (`--font-heading` nutzt Public Sans).
|
||||
- `AlegreyaSC-Bold.woff2` (104 KB), `SourceCodePro` (88 KB), `SourceSerif4` (416 KB) liegen ungenutzt im `public/fonts/`.
|
||||
- Vier von fünf Bildern sind ungenutzt: `desk-light.webp` (**2,0 MB**), `wood-desk-clean-dark.webp` (1,3 MB), `desk-red-orange.webp` (792 KB), `desk-clean-dark.webp` (488 KB).
|
||||
|
||||
Nitro kopiert `public/` unverändert ins Deployment. Du schleppst also ~5 MB toten Ballast mit.
|
||||
|
||||
### D3. Placeholder-Bilder in Produktion, während echte Assets ungenutzt danebenliegen
|
||||
`hero.tsx:58` und `footer.tsx:50` laden von `https://placehold.co`. Das ist im Hero-Bereich — das Erste, was ein Besucher sieht — und obendrein eine externe Abhängigkeit, die dir die Ladezeit versaut und datenschutzrechtlich in die Erklärung müsste.
|
||||
|
||||
Gleichzeitig hast du fünf passende Schreibtisch-Fotos im Projekt liegen, die keiner nutzt. Das wirkt wie ein reiner Vergessens-Fehler.
|
||||
|
||||
### D4. Bilder ohne responsive Auslieferung
|
||||
`features.tsx` lädt `laptop-planning.webp` (684 KB) mit `width="5184" height="3456"`, angezeigt wird es maximal 1152 px breit. Kein `srcset`, kein `sizes`. Auf dem Smartphone lädt jemand ein 5K-Bild für eine 400px-Anzeige.
|
||||
|
||||
Außerdem: `logo.png` ist 228 KB für eine 40×40-Darstellung — und dient gleichzeitig als Favicon *und* als Open-Graph-Bild.
|
||||
|
||||
Das alles steht direkt gegen dein zentrales Verkaufsversprechen („Unter 1 Sekunde Ladezeit", `hero.tsx:6`). Die eigene Seite ist das Portfolio-Stück — hier sollte ein Lighthouse-Run bei 100 landen.
|
||||
|
||||
### D5. Header bricht außerhalb der Startseite
|
||||
`src/components/header.tsx`:
|
||||
- Zeile 53: Logo ist `<a href="#">` statt `<Link to="/">`. Auf `/termin` führt ein Klick aufs Logo nirgendwohin.
|
||||
- Zeilen 9-14: Alle Nav-Links sind Anker (`#zielgruppen`, `#pricing` …). Auf `/termin` existieren diese Anker nicht — die komplette Hauptnavigation ist dort tot.
|
||||
|
||||
**Fix:** `<Link to="/" hash="pricing">` statt roher Anker, dann funktioniert es von jeder Route aus.
|
||||
|
||||
### D6. Open Graph unbrauchbar
|
||||
`__root.tsx:60,69` — `og:image` ist `/img/logo.png`, ein relativer Pfad. Open Graph verlangt absolute URLs; Facebook, LinkedIn und WhatsApp zeigen also gar kein Bild. Zusätzlich ist ein Logo im falschen Format — 1200×630 wäre richtig.
|
||||
|
||||
### D7. Kleinigkeiten
|
||||
- `styles.css:2-3`: `tw-animate-css` wird zweimal importiert (einmal mit einfachen, einmal mit doppelten Anführungszeichen).
|
||||
- `__root.tsx:13`: Variable heißt `themeItnitScript` (Tippfehler „Itnit").
|
||||
- `header.tsx:100`: `aria-label="Menü öffnen"` bleibt statisch, auch wenn das Menü offen ist. Screenreader-Nutzer bekommen die falsche Ansage.
|
||||
- `.rise-in` (`styles.css:336`) hat keinen `prefers-reduced-motion`-Fallback.
|
||||
- Kein `robots.txt`, keine `sitemap.xml`.
|
||||
- `admin.index.tsx:82`: Tabelle ohne `<caption>` und ohne `scope="col"` an den Headern.
|
||||
- `admin.users.$userId.tsx:429`: natives `confirm()` fürs Löschen. Funktioniert, passt aber stilistisch nicht zum Rest — du hast bereits einen Dialog in `components/ui/dialog.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## PLAUSIBILITÄT / INHALT
|
||||
|
||||
### P1. Platzhalter-Telefonnummer
|
||||
`footer.tsx:32,92` — `+49 176 22222222`. Steht zweimal auf der Seite als Kontaktweg.
|
||||
|
||||
### P2. Uneinheitliche Marke
|
||||
Die Seite heißt „webkulisse", die Domain ist `webkulisse.de`, die Anbieterangabe im Footer lautet „Mathias Hurler Webdesign", und der Buchungskalender liegt auf `termin.hurler-webdesign.de`. Der Besucher sieht beim Klick auf „Erstgespräch buchen" also plötzlich eine fremde Domain. Rechtlich in Ordnung (Anbieterangabe muss der echte Name sein), aber für das Vertrauen im Buchungsmoment ungünstig. Ein CNAME auf `termin.webkulisse.de` löst das.
|
||||
|
||||
### P3. Werbeaussagen gegen den eigenen Code prüfen
|
||||
`features.tsx:11-13`: *„Kein Content-Management-System, keine Plugins, keine Datenbank."* — Das gilt fürs Kompakt-Paket, nicht für „Ihre neue Webapp" (das hat laut `pricing.tsx:54-56` Login und Kundenportal, also zwangsläufig eine Datenbank). Deine eigene Seite ist das beste Gegenbeispiel. Ich würde die Formulierung an den Paket-Kontext binden, sonst widerspricht sich die Seite selbst.
|
||||
|
||||
Ebenso `pricing.tsx:24`: Das Kompakt-Paket verspricht ein Kontaktformular. Bei einer Seite „ohne Datenbank" braucht das einen Mail-Service — nur zur Sicherheit, dass der Aufwand eingepreist ist.
|
||||
|
||||
### P4. Showcase ist leer
|
||||
`showcase.tsx` zeigt drei „In Kürze"-Kacheln. Auf einer Agenturseite ist der Referenz-Bereich das stärkste Verkaufsargument; leer wirkt er schwächer als gar nicht vorhanden. Solange nichts da ist, würde ich den Abschnitt ausblenden statt Platzhalter zu zeigen.
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
|
||||
**Vor dem Livegang:** K1 (Rechtstexte), K2 (Devtools), K3 (Env-Validierung), H6 (noindex), D1 (Font-Pfad), D3 (Placeholder-Bilder), P1 (Telefonnummer)
|
||||
|
||||
**Direkt danach:** H1 (Session-Revoke), H4 (Rate-Limit in DB), H5 (Redirect), M6/M7 (Schema-Drift, Seed-Script), D2/D4 (Assets)
|
||||
|
||||
**Bevor die erste Rechnungs-Funktion entsteht:** H2 (Auth-Middleware), M1 (Loader-Muster), M5 (Alias vereinheitlichen), M8 (Error-Boundaries)
|
||||
|
||||
H2 und M1 sind die beiden, die über Spaghetti-Code entscheiden. Alles andere ist reparierbar; ein Datenzugriffs-Muster, das sich über zwanzig Dateien verteilt hat, nicht mehr ohne größeren Umbau.
|
||||
|
||||
---
|
||||
|
||||
## Wenn du das an Junie gibst
|
||||
|
||||
Der Teil, den Junie am besten übernehmen kann, ist der mechanische: M5 (Alias-Vereinheitlichung über alle Dateien), M1 (Umbau auf Loader), D7 (Kleinigkeiten), M3/M2 (Fehlerbehandlung). Bei H2 würde ich die Middleware selbst schreiben und Junie nur die Anwendung auf neue Server-Functions überlassen — das ist die Stelle, an der du verstehen willst, was passiert.
|
||||
Reference in New Issue
Block a user