Seiten-Editor: Inhalte pflegen ohne Code-Änderung
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Antwort auf den CMS-Wunsch aus der Community — statt eines Forums, das
neben aktivem Discord erfahrungsgemäß verwaist, ein schlanker Editor für
eigene Seiten: Regeln, Über uns, Mitmachen, FAQ.

- neuer Setup-Tab "Seiten" (Scope content): Titel, Adress-Kürzel wird aus
  dem Titel abgeleitet, Markdown-Editor mit Live-Vorschau daneben
- pro Seite wählbar: veröffentlicht oder Entwurf, im Community-Menü oder
  nur per Direktlink, Reihenfolge
- Anzeige unter /<kürzel>, gerendert mit dem vorhandenen Markdown-Parser
- Entwürfe sind öffentlich nicht abrufbar — sie brauchen den content-Scope
- reservierte Kürzel (devlogs, settings, api, impressum …) werden
  abgelehnt, damit nichts die festen Routen überschreibt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:08:21 +02:00
co-authored by Claude Fable 5
parent 3b80334433
commit e8158ce90b
8 changed files with 337 additions and 2 deletions
+64
View File
@@ -17,6 +17,7 @@ import {
logAudit, listAudit, playerHistory, deleteUserData,
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
createService, updateService, listServices, deleteService,
savePage, getPage, listPages, listPublishedPages, deletePage,
} from '../db.js';
import crypto from 'node:crypto';
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
@@ -163,6 +164,69 @@ export function registerApiRoutes(app, client) {
}),
}));
// --- Frei angelegte Seiten (Regeln, Über uns, …) ---
// Adressen, die schon vom Frontend oder Server belegt sind
const RESERVED_SLUGS = new Set([
'devlogs', 'roadmap', 'server', 'galerie', 'level', 'events', 'changelog',
'profil', 'commits', 'settings', 'dashboard', 'impressum', 'datenschutz',
'api', 'auth', 'webhooks', 'sso', 'health', 'assets', 'features', 'commands', 'docs',
]);
// Menü-Einträge + veröffentlichte Seiten — öffentlich
app.get('/api/pages', async () => ({ pages: listPublishedPages() }));
app.get('/api/pages/:slug', async (request, reply) => {
const page = getPage(String(request.params.slug));
if (!page || !page.published) {
// Entwürfe darf nur sehen, wer sie auch bearbeiten dürfte
const scopes = scopesOf(getSessionUser(request));
if (!page || !(scopes.includes('*') || scopes.includes('content'))) {
return reply.code(404).send({ error: 'Seite nicht gefunden' });
}
}
return { page: { ...page, published: Boolean(page.published), in_menu: Boolean(page.in_menu) } };
});
// Verwaltung (Scope content) — inklusive Entwürfen
app.get('/api/pages/all/list', async (request, reply) => {
if (requireScope(request, reply, 'content')) return;
return {
pages: listPages().map((p) => ({
...p, published: Boolean(p.published), in_menu: Boolean(p.in_menu),
})),
};
});
app.put('/api/pages/:slug', async (request, reply) => {
if (requireScope(request, reply, 'content')) return;
const slug = String(request.params.slug).trim().toLowerCase()
.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
const title = String(request.body?.title ?? '').trim();
if (!slug || !title) return reply.code(400).send({ error: 'Kürzel und Titel nötig' });
if (RESERVED_SLUGS.has(slug)) {
return reply.code(400).send({ error: `${slug}" ist bereits vergeben — bitte anderes Kürzel` });
}
savePage({
slug,
title: title.slice(0, 80),
content: String(request.body?.content ?? '').slice(0, 50_000),
published: request.body?.published ? 1 : 0,
in_menu: request.body?.in_menu === false ? 0 : 1,
sort: Number(request.body?.sort) || 0,
});
logAudit(getSessionUser(request), 'seite gespeichert', `${title} (/${slug})`);
return { ok: true, slug, pages: listPages() };
});
app.delete('/api/pages/:slug', async (request, reply) => {
if (requireScope(request, reply, 'content')) return;
logAudit(getSessionUser(request), 'seite gelöscht', String(request.params.slug));
return { deleted: deletePage(String(request.params.slug)), pages: listPages() };
});
// Dienste-Kacheln fürs Portal — öffentlich
app.get('/api/services', async () => ({ services: listServices() }));