Kommentare & Diskussionen
Regeln
- Verschachtelte Kommentare:
parent_id Fremdschlüssel in der Kommentartabelle — null für oberste Ebene, verweist auf das übergeordnete Element bei Antworten
- Optimistische UI: Kommentar sofort zur UI hinzufügen, mit Serverantwort bestätigen, bei Fehler rückgängig machen
- @Erwähnungen:
@benutzername im Kommentartext parsen — Dropdown zur Benutzersuche bei @-Tastendruck auslösen, erwähnte Benutzer benachrichtigen
- Reaktionen: Separates
reactions-Table (commentId, userId, emoji) — Unique Constraint auf (commentId, userId, emoji)
- Moderation:
status-Feld (published, flagged, removed) — Moderationswarteschlange für markierte Inhalte, automatische Markierung mit Wortliste
- Soft Delete:
deletedAt-Zeitstempel setzen — „Gelöscht“-Platzhalter für Kommentare mit Antworten anzeigen, verwaiste gelöschte Kommentare vollständig ausblenden
- Cursor-Paginierung:
cursor (commentId) + limit — sortiert nach createdAt, ältere Kommentare beim Scrollen laden
- Bearbeitungsverlauf: Änderungen in separatem Table oder JSON-Array speichern — „Bearbeitet“-Indikator mit letztem Bearbeitungszeitpunkt anzeigen
Patterns
const comments = await db.query.comments.findMany({
where: and(eq(comments.postId, postId), isNull(comments.parentId)),
with: {
author: { columns: { id: true, name: true, avatar: true } },
replies: {
with: { author: { columns: { id: true, name: true, avatar: true } } },
orderBy: [asc(comments.createdAt)],
},
reactions: true,
},
orderBy: [desc(comments.createdAt)],
limit: 20,
});
const addComment = useMutation({
mutationFn: (body: string) => api.post(`/posts/${postId}/comments`, { body }),
onMutate: async (body) => {
await queryClient.cancelQueries({ queryKey: ["comments", postId] });
const previous = queryClient.getQueryData(["comments", postId]);
queryClient.setQueryData(["comments", postId], (old: Comment[]) => [
{ id: `temp-${Date.now()}`, body, author: currentUser, createdAt: new Date() },
...old,
]);
return { previous };
},
onError: (_err, _body, context) => {
queryClient.setQueryData(["comments", postId], context?.previous);
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ["comments", postId] }),
});
Vermeiden
- Unbegrenzte Verschachtelungstiefe — auf 2-3 Ebenen begrenzen, tiefere Antworten dann abflachen
- Alle Kommentare auf einmal laden — Cursor-Paginierung verwenden, besonders bei aktiven Diskussionen
- Nur clientseitige Erwähnungsparsung — Erwähnungen serverseitig validieren und extrahieren für Benachrichtigungen
- Kommentare mit Antworten hart löschen — dadurch wird der Thread verwaist; stattdessen Soft Delete und Platzhalter anzeigen
- Reaktionen ohne Unique Constraints — ermöglicht doppelte Reaktionen vom selben Benutzer