- Vorwort – Warum überhaupt noch ein Blog?
- Features
- Motivation – Das Problem hinter dem Projekt
- Produktvision – Schreiben wie Coden
- Technologieauswahl – Node.js & Next.js als Fundament
- Eine kleine Chronik der ersten Woche
- Architektur im Überblick
- Der Editor – Baustein statt Monolith
- Rollen & Moderation – Verantwortung sichtbar machen
- Sicherheit & Datenschutz – Schutz als Produktmerkmal
- Performance & Skalierung – schnell ist die Voreinstellung
- KI-Unterstützung – Helfer, nicht Entscheider
- Publishing-Workflow – vom ersten Satz bis zum "Jetzt live!"
- Status-Modell in der Datenbank
- Beispiele aus dem Maschinenraum – Code plus Erklärung
- Roadmap

Vorwort – Warum überhaupt noch ein Blog?
Es begann an einem verregneten Dienstagabend. Drei Tabs mit unterschiedlichen Blogsystemen, zwei kalte Espressi, ein Kopf voller Notizen – und das dringende Gefühl, dass keines der vorhandenen Werkzeuge wirklich „meins“ war. Ich wollte keinen sterilen Editor, sondern etwas, das sich wie eine Werkbank anfühlt: ein Tisch, auf dem Text, Code und Ideen durcheinander liegen dürfen, während man an ihnen feilt. Ein Platz, an dem man den Lötkolben neben der Kaffeetasse stehen lassen kann. Viele Tools sind großartig, wenn man linear erzählt. Doch sobald Code ins Spiel kommt, oder mehrere Autor:innen gleichzeitig arbeiten, oder Moderation nötig wird, beginnen selbst die elegantesten Systeme zu knirschen.
Also bauten wir unsere eigene Plattform – mit allen Freuden und Frustrationen, die so ein Projekt mit sich bringt. Dieser Beitrag ist deshalb kein trockenes Feature-Sheet, sondern ein Reisebericht. Er enthält Entscheidungen, Rückschritte, kleine Siege, Anekdoten aus dem Debugging-Alltag und die Freude an jenen Momenten, in denen Software plötzlich „klick“ macht.
Features
- ✍️ Beitragserstellung mit Block-Editor (Editor.js)
- 🔐 Login mit NextAuth, E-Mail & optional 2FA (TOTP)
- 🛠 Admin-Panel mit Benutzerrollen, Moderation & Ticket-System
- 💬 Audit-Log für System-Transparenz
- 🤖 KI-Unterstützung (OpenAI, Text- und Bild-Generierung)
- 📌 Blog-Kategorien, Tags, Slugs & Filter
- 🧠 Statusverwaltung: Entwurf, Veröffentlicht, Geplant, Nicht öffentlich
- 📊 Statistiken (Besucher, Beiträge, Trends – geplant)
- 📬 Newsletter-System
- 📨 Benachrichtigungen für Nutzer
Motivation – Das Problem hinter dem Projekt
Technische Texte haben eine Eigenheit: Sie müssen nicht nur verstanden, sondern oft nachvollzogen werden. Code ist nicht Dekoration, er ist Beweis. Er braucht Syntax-Highlighting, klare Typografie, Kopierbarkeit, manchmal sogar Begleittext Zeile für Zeile. Gleichzeitig geschieht Schreiben selten allein. Man entwirft, teilt, holt Feedback und verbessert. Und weil das Netz ein öffentlicher Ort ist, brauchen wir Moderation, Rollen und Nachvollziehbarkeit – ohne dass es nach Behördenflur riecht. Aus dieser Mischung entstand die Idee: Schreiben wie Coden.
Produktvision – Schreiben wie Coden
Im Zentrum steht ein Editor, der sich wie ein Baukasten anfühlt: Text, Überschrift, Code, Zitat, Bild, Diagramm, Hinweis, später beliebige Community-Blöcke. Codeblöcke sind erstklassige Bürger – mit Sprachen, Zeilennummern, Copy-Button, sauberem Rendering auf dem Server und flüssigem Scrollen im Client. Moderationsentscheidungen landen im Audit-Log und sind nachvollziehbar, ohne personenbezogene Daten offenzulegen. Rollen sind fein genug, um Wachstum zu erlauben und grob genug, um noch verstanden zu werden. Sicherheit -> von 2FA bis DSGVO -> ist kein Add-on, sondern Zutatenliste.
Technologieauswahl – Node.js & Next.js als Fundament
Wir wollen Geschwindigkeit im Kopf und im Build. Node.js liefert ein homogenes Ökosystem, in dem sich Backend-APIs, Worker und Tools in TypeScript treffen. Next.js bringt SSR/SSG, ein reifes Routing und gute DX mit. Zusammen ergibt das einen Rahmen, der Experimentierfreude zulässt und sich dennoch in Produktion ruhig verhält.
Eine kleine Chronik der ersten Woche
Am Anfang stand der Editor. Wir bauten ihn dreimal. Erst monolithisch (zu starr) dann zu granular (zu viele Renderzyklen), schließlich blockbasiert, mit einem klaren State-Modell. Das Syntax-Highlighten war zunächst „quick & dirty“ – schön, aber langsam. Nach einer Nacht mit Lighthouse-Messungen wechselten wir auf on-demand geladene Highlighter und SSR-sichere Einbindung. Die erste Integration eines KI-Dienstes erinnerte an das Kaffeeküchen-Telefon: mach mal frei, mach mal besetzt. Also lernten wir, mir Rate-Limits zu leben und Antworten robust zu verarbeiten. Und ganz nebenbei stellten wir fest: Rollen sollte man früh definieren, sonst wachsen sie wie Hecken in Einfahrten.
Architektur im Überblick
Die Anwendung besteht aus drei Schichten:
- Außen:
- Next.js – Oberfläche für Lesen, Schreiben, Moderieren
- Dazwischen:
- eine API-Schicht, die Eingabe validiert, Rechte prüft, Raten begrenzt und mit der Datenbank spricht
- Innen:
- eine rationale Datenbank, die Beiträge, Kommentare, Rollen und Logs verwaltet
Diese Trennung erlaubt uns, die Fassade zu renovieren, ohne statische Wände zu versetzen.
Der Editor – Baustein statt Monolith
Ein Beitrag ist eine Sequenz aus Blöcken. Jeder Block bringt seine eigene Regel mit: Ein Codeblock kennt seine Sprache, ein Bild seinen Alternativtext, ein Hinweis seine Semantik. Wir speichern nicht nur den Text, sondern auch Metadaten, damit spätere Migrationen, Volltextsuche oder das gezielte Rendern (z.B. „zeige mir alle Python-Snippsets“) möglich werden.
Rendering passiert serverseitig, Interaktion clientseitig – beides greift wie ein Zahnrad.
Rollen & Moderation – Verantwortung sichtbar machen
Moderation ist kein Orakel, sondern Handwerk. Wenn ein Kommentar gesperrt wird, entsteht ein Audit-Eintrag: Wer hat gehandelt, welche Ziel, welche Begründung, welcher Kontext? Nicht um zu strafen, sondern um später erklären zu können. Rollen bleiben lesbar: Autor:in, Moderator:in, Admin. Feingranular dort, wo es nötig ist (z.B. „darf veröffentlichen, aber keine Rolle ändern“), grob, wo es besser ist (z.B. „Gast darf vorschlagen“),
Sicherheit & Datenschutz – Schutz als Produktmerkmal
Sicherheit beginnt beim Login und hört bei Logs nicht auf. TOTP-basierte 2FA, Rate-Limits auf API-Routen, Schema-Validation beim Eingeben, sauberes Fehlerhandling. In den Logs vermeiden wir Klartext und speichern Kennungen statt Inhalt. Daten bekommen eine Lebensdauer: Entwürfe, die nicht veröffentlicht wurden, können automatisch bereinigt werden; Anonymisierung ersetzt Löschung, wenn Statistiken weiterleben sollen.
Performance & Skalierung – schnell ist die Voreinstellung
Die Plattform ist auf kurzen Wege gebaut; SSR, Caching am Rand, möglichst wenig Client-JavaScript. Lange Texte werden gestreamt, Bilder vorab skaliert. Die Datenbank erhält Indizes, die aus echten Abfragen entstehen. Und wenn ein Feature zu teuer wird, schreiben wir es um – nicht später, sondern jetzt.
KI-Unterstützung – Helfer, nicht Entscheider
KI ist unser Lektor, nicht unser Chefredakteur. Sie schlägt Tags vor, fasst Absätze zusammen, markiert potenziell problematische Kommentare – und lieg manchmal daneben. Deshalb bleiben alle Entscheidungen reversible, erklärbar und menschlich überprüfbar.
Use-Cases aus der Praxis
1) „Vom Gedanken zur Veröffentlichung“ – ein Team-Beitrag
Eine Autorin skizziert eine Idee im Editor: Einleitung, zwei Codeblöcke, ein Diagramm. Sie setze den Status auf DRAFT und markiert in der Seitenleiste „Review erbeten“. Ein Reviewer kommentiert direkt in Blöcken (kein globales Text-Chaos mehr) und schlägt eine alternative Lösung für ein Snippet vor. Ein Moderator prüft den Ton in den Kommentar und ändert nichts – aber hinterlässt die Spur im Audit-Log. Nach zwei Runden Review wechselt der Status auf READY_FOR_PUBLISH. Der Beitrag bekommt einen Veröffentlichungstermin, ein Social-Preview-Bild und eine kurze Zusammenfassung. Um 09:00 Uhr geht er live, automatisch.
2) „Kommentar-Sturm“ – Moderation in Echtzeit
Ein Beitrag trendet, die Kommentarzahl steigt. Ein Nutzer postet wiederholt aggressive Links. Die Moderation klick nicht wild herum, sondern setzt eine Regel: „Neue Kommentare mit diesem Muster in Warteschlange“. Die Plattform markiert nur auffällige Einträge und informiert die Moderator:innen in einem Slack-Kanal. Später werden die Regeln rückgebaut, die Audit-Einträge bleiben – als Lehrstück für die nächste Welle.
3) „Die Migration“ – vom Alt-Blog ins neue Zuhause
Wir importieren alte Beiträge, die HTML, wie Geröll tragen. Der Importer übersetzt bekannte Muster in Blöcke <pre><code> wird zum Codeblock, <h2> zur Überschrift, Bilder bekommen fehlende Alt-Texte aus Dateinamen (und Warnhinweise, wo das nicht genügt). Der alte Permalink bleibt als Weiterleitung bestehen. Nach dem Import erscheint ein Bericht: fehlende Metadaten, tote Links, zu lange Title. Was sich automatisch heilen lässt, wird geheilt; der Rest bekommt Aufgaben-Tickets.
4) „Der Block aus der Community“ – Erweiterbarkeit in der Praxis
Ein Contributorin baut einen Diagramm-Block. Sie veröffentlicht ihn als Paket, dokumentiert Props und Sicherheitsgrenzen. In unserem System wird der Block mit einer Freigabe versehen: „Neue Blöcke dürfen Text/Code, aber kein Inline-Script“. Nach der Freigabe erscheint er im Editor – rechts neben Bild, Code und Zitat.
Publishing-Workflow – vom ersten Satz bis zum „Jetzt live!“
Veröffentlichen ist mehr als ein Button. Es ist eine Abfolge von Zuständen, die Klarheit schaffen – für Menschen und Maschinen:
- Entwurf (DRAFT)
- frei, verspielt, ohne Pflichtfelder
- Review (IN_REVIEW)
- Kommentare, Vorschläge, kleine Korrekturen
- Ready (READY_TO_PUBLISHED)
- inhaltlich fertig, technisch geprüft
- Geplant (SCHEDULE)
- hat Datum/Zeit
- Veröffentlicht (PUBLISHED)
- sichtbar, versioniert
- Zurückgezogen (RETRACTED)
- öffentlich entfernt, intern nachvollziehbar
Dazu kommen Checks: Hat der Beitrag eine Einleitung? Sind Bilder mit Alt-Text versehen? Sind Codeblöcke einer Sprache zugeordnet? Stimmen Links, stimmen Lizenzen, stimmt der Ton? Manche Prüfungen sind hart (z.B. keine Veröffentlichung ohne Title), andere weich (Warnung, aber nicht blockierend).
Status-Modell in der Datenbank
model Post {
id String @id @default(cuid())
title String
content Json // Blockstruktur
status PostStatus @default(DRAFT)
language String @default("en")
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
}
enum PostStatus {
DRAFT
IN_REVIEW
READY_FOR_PUBLISH
SCHEDULED
PUBLISHED
RETRACTED
}
Übergang mit Guard-Regeln
type Transition = { from: PostStatus; to: PostStatus; guard?: (post: Post) => Promise<boolean> };
const transitions: Transition[] = [
{ from: 'DRAFT', to: 'IN_REVIEW' },
{ from: 'IN_REVIEW', to: 'READY_FOR_PUBLISH', guard: prepublishCheck },
{ from: 'READY_FOR_PUBLISH', to: 'SCHEDULED', guard: hasSchedule },
{ from: 'SCHEDULED', to: 'PUBLISHED' },
{ from: 'PUBLISHED', to: 'RETRACTED' },
];
async function canTransition(post: Post, to: PostStatus) {
const rule = transitions.find(t => t.from === post.status && t.to === to);
return rule ? (rule.guard ? rule.guard(post) : true) : false;
}
Pre-Publish-Check: kleine Maschine, große Wirkung
async function prepublishCheck(post: Post) {
const blocks = post.content as Array<any>;
const hasIntro = blocks.some(b => b.type === 'paragraph' && b.text.length > 60);
const imagesNeedAlt = blocks.filter(b => b.type === 'image').some(img => !img.alt || img.alt.length < 3);
const codeWithoutLang = blocks.filter(b => b.type === 'code').some(c => !c.language);
if (!post.title || post.title.length < 5) return false;
if (!hasIntro) return false;
if (imagesNeedAlt) return false;
if (codeWithoutLang) return false;
return true;
}
Geplante Veröffentlichung: pünklich ohne Drama
// Pseudocode eines einfachen Schedulers
import { prisma } from '@/lib/prisma';
export async function runPublishTick(now = new Date()) {
const due = await prisma.post.findMany({
where: { status: 'SCHEDULED', publishedAt: { lte: now } },
});
for (const post of due) {
await prisma.$transaction(async (tx) => {
// Doppelpullover gegen Race Conditions
const fresh = await tx.post.findUnique({ where: { id: post.id } });
if (fresh?.status !== 'SCHEDULED') return;
await tx.post.update({ where: { id: post.id }, data: { status: 'PUBLISHED' } });
await tx.event.create({ data: { type: 'POST_PUBLISHED', postId: post.id } });
});
}
}
Post-Publish – Messen, reden, lernen
Nach der Veröffentlichung sprechen wir mit zwei Systemen: Menschen und Maschinen. Maschinen bekommen Events (z.B. POST_PUBLISHED), die Webhooks auslösen – an Slack, Discord, Suchmaschinen-Ping, Newsletter-Queue. Menschen bekommen verständliche Benachrichtungen: „Dein Beitrag ist live. Hier sind die ersten Reaktionen“
// Minimaler Slack‑Webhook
async function notifySlack(text: string) {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
}
Beispiele aus dem Maschinenraum – Code plus Erklärung
Einen Blog-Beitrag per API erstellen
Die API ist das Herzstück der Plattform. Sie prüft Methoden, Rolle, Eingabe, drosselt notfalls und schreibt erst dann. Fehlerpfade sind explizit, damit das Frontend sinnvoll reagieren kann.
// pages/api/blog/create.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const BodySchema = z.object({
title: z.string().min(3).max(200),
content: z.string().min(1),
language: z.string().min(2).max(5).default('en'),
});
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).json({ error: 'Method Not Allowed' });
const session = await getServerSession(req, res, authOptions);
if (!session || !['ADMIN', 'BLOGGER'].includes((session as any).role)) {
return res.status(403).json({ error: 'Unauthorized' });
}
const parsed = BodySchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid body', details: parsed.error.flatten() });
}
const key = `createPost:${session.user.id}`;
const { success } = await ratelimit.limit(key);
if (!success) return res.status(429).json({ error: 'Too Many Requests' });
try {
const post = await prisma.blogPost.create({
data: {
title: parsed.data.title,
content: parsed.data.content,
authorId: session.user.id,
status: 'DRAFT',
language: parsed.data.language,
},
});
return res.status(201).json(post);
} catch (err) {
return res.status(500).json({ error: 'Failed to create post' });
}
}
Codeblock mit Syntax-Highlighting – SSR-sicher und mit Copy-Button
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const SyntaxHighlighter = dynamic(() => import('react-syntax-highlighter').then(m => m.Prism), { ssr: false });
import { tomorrow } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export default function CodeBlock({ code, language = 'tsx' }: { code: string; language?: string }) {
const [copied, setCopied] = useState(false);
const onCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {}
};
return (
<figure className="relative">
<button onClick={onCopy} className="absolute right-2 top-2 rounded px-2 py-1 text-xs bg-gray-200">
{copied ? 'Kopiert' : 'Copy'}
</button>
<SyntaxHighlighter language={language} style={tomorrow} showLineNumbers>
{code}
</SyntaxHighlighter>
</figure>
);
}
Moderationsentscheidung protokollieren – Audit-Log
await prisma.auditLog.create({
data: {
action: 'COMMENT_BLOCKED',
actorId: moderatorId,
targetId: commentId,
details: { reason },
ip: req.headers['x-forwarded-for'] as string | undefined,
userAgent: req.headers['user-agent'],
},
});
2FA mit TOTP – tolerant, aber nicht leichtsinnig
import { authenticator } from 'otplib';
export async function validate2FA(token: string, secret: string, key: string) {
const isValid = authenticator.verify({ token, secret, window: 1 });
if (!isValid) return false;
// throttle(key, 5, '1 m'); // max. fünf Versuche pro Minute (Implementierung projektabhängig)
return true;
}
Roadmap
🟢 Höchste Priorität
- Plugin-System für Blockeditor
- Erweiterte Rollen- und Rechteverwaltung
- KI-gestütze Moderation
🟠 Mittlere Priorität
- Live-Collaboration für den Editor
- Automatische Tag / Kategorie-Vorschläge
- API für externe Clients
- CDN-Integration für Medien & Assets
🟡 Niedrige Priorität
- Voting-System für Feature-Wünsche
- WebAssembly-Integration
- Edge-Computing für Moderation
Entdecke mehr von DaonWare
Melde dich für ein Abonnement an, um die neuesten Beiträge per E-Mail zu erhalten.