# W-Make Portfolio CMS - Umsetzungsplan **Erstellt:** 2026-08-22 **Ziel:** Admin-Login + Content-Management für Seiten, Links und News --- ## 🎯 Anforderungen ### Must-Have (Phase 1): 1. **Admin-Login** - Authelia-basiert (bereits vorhanden auf auth.w-make.com) 2. **News/Updates verwalten** - erstellen, bearbeiten, löschen, publizieren 3. **Links verwalten** - externe/interne Links zur Homepage hinzufügen 4. **Seiten bearbeiten** - existierende Content-Seiten (About, Notes) editieren ### Nice-to-Have (Phase 2): 5. Markdown-Editor mit Preview 6. Medien-Upload (Bilder für Notes) 7. Multi-Sprache (DE/EN synchron editieren) 8. Version History / Drafts --- ## 🏗️ Architektur-Entscheidungen ### ✅ Was bereits existiert: **Infrastructure:** - ✅ Authelia SSO auf `auth.w-make.com` (2FA-Login) - ✅ Traefik Reverse Proxy (schützt `/admin` Routes) - ✅ `updates-api` Service (Node.js + SQLite) - Admin-Panel unter `https://updates.w-make.com/admin` - Bearer-Token Auth für API - SQLite-Datenbank `/data/updates.sqlite` **Current Content Structure:** ```typescript // Aktuell: TypeScript-Dateien (kompiliert in Bundle) src/content/notes.ts // Blog-Posts src/content/projects.ts // Case Studies src/content/offers.ts // Services src/content/person.ts // About-Content ``` **Problem:** Content ist im Code → Deployment nötig bei Änderungen --- ## 🎨 Lösungsansatz: Hybrid CMS ### Strategie: "Code für Struktur, DB für Content" **Prinzip:** - Struktur (TypeScript-Types, Validierung) bleibt in Code - Dynamischer Content (News, Updates, optionale Seiten) in SQLite - Statische Inhalte (person.ts, offers.ts) bleiben vorerst im Code **Warum Hybrid?** - ✅ Schnelle Time-to-Market (updates-api erweitern statt neu bauen) - ✅ Keine komplexe CMS-Migration - ✅ SQLite = einfaches Backup, kein Postgres nötig - ✅ Authelia = Production-ready SSO, keine eigene User-DB --- ## 📋 Umsetzungsplan: 3 Phasen --- ## **Phase 1: Admin-Panel erweitern (5-7 Tage)** ### 1.1 Updates-API erweitern für Content-Management **Neue Endpoints:** ```typescript // News (bereits vorhanden, nur erweitern) POST /v1/admin/updates // News erstellen PUT /v1/admin/updates/:id // News bearbeiten DELETE /v1/admin/updates/:id // News löschen POST /v1/admin/updates/:id/publish // News publizieren // Links (neu) GET /v1/links // Öffentlich: alle Links POST /v1/admin/links // Link erstellen PUT /v1/admin/links/:id // Link bearbeiten DELETE /v1/admin/links/:id // Link löschen // Pages (neu, optional) GET /v1/pages/:slug // Öffentlich: dynamische Seiten POST /v1/admin/pages // Seite erstellen PUT /v1/admin/pages/:slug // Seite bearbeiten DELETE /v1/admin/pages/:slug // Seite löschen ``` **Datenbank-Schema (SQLite):** ```sql -- Bereits vorhanden CREATE TABLE updates ( id INTEGER PRIMARY KEY, product TEXT NOT NULL, title TEXT NOT NULL, summary TEXT, slug TEXT UNIQUE, published_at TEXT, created_at TEXT NOT NULL ); -- Neu hinzufügen CREATE TABLE links ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, category TEXT, -- 'external' | 'internal' | 'resource' description TEXT, icon TEXT, -- optional: FontAwesome-Icon sort_order INTEGER DEFAULT 0, visible BOOLEAN DEFAULT 1, created_at TEXT NOT NULL ); CREATE TABLE pages ( id INTEGER PRIMARY KEY, slug TEXT UNIQUE NOT NULL, title_de TEXT NOT NULL, title_en TEXT NOT NULL, content_de TEXT, content_en TEXT, meta_description_de TEXT, meta_description_en TEXT, published BOOLEAN DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); ``` **Implementierung:** ```bash services/updates-api/src/ routes/ admin/ updates.js # bereits vorhanden links.js # NEU pages.js # NEU db/ schema.sql # Migration hinzufügen migrations/ 001-add-links.sql 002-add-pages.sql ``` --- ### 1.2 Admin-Frontend erweitern **Aktuell:** `https://updates.w-make.com/admin` (vermutlich einfaches HTML/JS) **Erweitern:** ``` services/updates-api/src/admin/ index.html # Dashboard updates.html # News verwalten links.html # NEU: Links verwalten pages.html # NEU: Seiten verwalten css/ admin.css js/ api.js # Fetch-Wrapper mit Bearer-Token updates.js links.js # NEU pages.js # NEU ``` **UI-Components (Plain HTML + Alpine.js oder Vanilla JS):** ```html

Links verwalten

Titel URL Kategorie Sichtbar Aktionen
{{ link.title }} {{ link.url }} {{ link.category }} {{ link.visible ? '✓' : '✗' }}
``` **Styling:** Design-System von w-make.com wiederverwenden (Copper/Ink/Paper Tokens) --- ### 1.3 Frontend (Next.js) integrieren **Homepage: Links-Section hinzufügen** ```tsx // src/app/[lang]/page.tsx export default async function HomePage({ params }: PageProps<"/[lang]">) { // ... const links = await fetchLinks(); // NEU: von updates-api return (
{/* ... existierende Sections ... */} {/* NEU: Links-Section */}

Nützliche Links

Ressourcen und Werkzeuge

{links.map((link) => (

{link.title}

{link.description}

))}
); } ``` **API-Client hinzufügen:** ```typescript // src/lib/api-client.ts const UPDATES_API_URL = process.env.UPDATES_API_URL || 'http://localhost:8080'; export async function fetchLinks() { const res = await fetch(`${UPDATES_API_URL}/v1/links`, { next: { revalidate: 60 } // ISR: Cache 60s }); if (!res.ok) return []; return res.json(); } export async function fetchUpdates(limit = 5) { const res = await fetch( `${UPDATES_API_URL}/v1/updates?product=batchmaker&limit=${limit}`, { next: { revalidate: 300 } } ); if (!res.ok) return []; return res.json(); } ``` --- ### 1.4 Authelia-Integration absichern **Traefik-Config (auf VPS prüfen):** ```yaml # Bereits vorhanden in /opt/traefik/config/ http: routers: updates-admin: rule: "Host(`updates.w-make.com`) && PathPrefix(`/admin`)" middlewares: - authelia@docker # Redirect zu auth.w-make.com service: updates-api ``` **Admin-Panel: Session-Check** ```javascript // services/updates-api/src/admin/js/auth.js async function checkAuth() { const token = localStorage.getItem('admin_token'); if (!token) { // Authelia hat Remote-User-Header gesetzt // Oder redirect zu /login window.location.href = '/login'; } } // Bei API-Calls fetch('/v1/admin/updates', { headers: { 'Authorization': `Bearer ${token}` } }); ``` --- ## **Phase 2: Markdown-Editor + Media-Upload (3-5 Tage)** ### 2.1 Rich-Text-Editor einbauen **Library-Auswahl:** - **TipTap** (modern, headless, gute TypeScript-Unterstützung) - **SimpleMDE** (leichtgewichtig, Markdown-fokussiert) - **Quill** (etabliert, aber schwerer) **Empfehlung:** SimpleMDE für Notes/Pages ```html ``` --- ### 2.2 Bild-Upload **Backend (updates-api erweitern):** ```javascript // services/updates-api/src/routes/admin/media.js import multer from 'multer'; import path from 'path'; const storage = multer.diskStorage({ destination: '/data/uploads/', filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only images allowed')); } } }); router.post('/v1/admin/media', upload.single('image'), (req, res) => { res.json({ url: `/uploads/${req.file.filename}`, filename: req.file.filename }); }); ``` **Frontend: Drag & Drop Upload** ```html
Bild hierher ziehen oder klicken
``` --- ## **Phase 3: Dynamische Seiten + Notes aus DB (3-4 Tage)** ### 3.1 Notes von TypeScript → SQLite migrieren **Migration-Script:** ```typescript // scripts/migrate-notes-to-db.ts import { notes } from '../src/content/notes'; import { db } from '../services/updates-api/src/db'; for (const [slug, note] of Object.entries(notes)) { db.run(` INSERT INTO pages (slug, title_de, title_en, content_de, content_en, published) VALUES (?, ?, ?, ?, ?, 1) `, [ `notizen/${slug}`, note.de.title, note.en.title, note.de.body.join('\n\n'), note.en.body.join('\n\n') ]); } ``` **Frontend: Dynamic Notes Route** ```typescript // src/app/[lang]/notizen/[slug]/page.tsx export default async function NotePage({ params }: PageProps) { const locale = requireLocale((await params).lang); const slug = (await params).slug; // Zuerst DB prüfen const dbNote = await fetchPageBySlug(`notizen/${slug}`); if (dbNote) { return
{/* Render DB-Content */}
; } // Fallback: statische notes.ts const staticNote = getNote(slug as NoteSlug, locale); return
{/* Render static content */}
; } ``` --- ### 3.2 Neue dynamische Seiten **Use-Case:** Kundenwunsch "Changelog-Seite" oder "FAQ" hinzufügen ohne Deployment ```typescript // src/app/[lang]/[slug]/page.tsx (Catch-All Dynamic Route) export default async function DynamicPage({ params }: PageProps) { const locale = requireLocale((await params).lang); const slug = (await params).slug; const page = await fetchPageBySlug(slug); if (!page || !page.published) { notFound(); } const content = locale === 'de' ? page.content_de : page.content_en; return (

{page[`title_${locale}`]}

); } // Statische Pfade für Build-Zeit export async function generateStaticParams() { const pages = await fetchAllPublishedPages(); return pages.map(page => ({ slug: page.slug })); } ``` --- ## 🚀 Deployment-Strategie ### Phase 1 Deployment: ```bash # 1. updates-api erweitern cd services/updates-api npm install multer marked npm run build # 2. Datenbank migrieren sqlite3 /data/updates.sqlite < src/db/migrations/001-add-links.sql # 3. Docker neu bauen docker compose build updates-api docker compose up -d updates-api # 4. Admin-Panel testen open https://updates.w-make.com/admin ``` ### Next.js ISR (Incremental Static Regeneration): ```typescript // Automatisches Rebuild bei neuen Inhalten ohne Deployment fetch(url, { next: { revalidate: 60 } // Cache 60s, dann re-fetch }); ``` --- ## 📊 Aufwandsschätzung | Phase | Tasks | Aufwand | Priorität | |-------|-------|---------|-----------| | **Phase 1** | Admin-Panel + Links + API | 5-7 Tage | ⭐⭐⭐ HOCH | | **Phase 2** | Editor + Upload | 3-5 Tage | ⭐⭐ MITTEL | | **Phase 3** | Dynamic Pages + Migration | 3-4 Tage | ⭐ NIEDRIG | | **Testing & Docs** | E2E-Tests, Anleitung | 2-3 Tage | ⭐⭐ MITTEL | **Total:** 13-19 Tage (2-3 Wochen bei Vollzeit) --- ## 🔒 Sicherheits-Checkliste - [x] Authelia SSO (2FA bereits aktiv) - [ ] CSRF-Protection für Admin-Endpoints - [ ] Rate-Limiting für Uploads (max 10 MB/Stunde) - [ ] Input-Sanitization (XSS-Schutz bei HTML-Content) - [ ] File-Type-Validation (nur Bilder erlauben) - [ ] SQL-Injection-Schutz (Prepared Statements) - [ ] Backup-Strategie für SQLite-DB - [ ] Rollback-Prozess dokumentieren --- ## 📚 Alternativen (falls Hybrid-Ansatz nicht passt) ### Option B: Headless CMS (Strapi/Payload) **Pro:** - ✅ Professionelles Admin-UI out-of-the-box - ✅ Media-Library, Rollen-Management, Webhooks **Contra:** - ❌ Overhead: PostgreSQL + Redis nötig - ❌ Mehr Operational Complexity - ❌ Höherer Server-Ressourcen-Bedarf ### Option C: Git-basiert (Decap CMS / Tina CMS) **Pro:** - ✅ Content bleibt in Git (Version Control) - ✅ Keine separate Datenbank **Contra:** - ❌ Deployment bei jedem Content-Edit - ❌ Langsamer für häufige Updates --- ## 🎯 Empfehlung: Start mit Phase 1 **Warum:** 1. Schnellste Time-to-Market (baut auf vorhandener updates-api auf) 2. Minimaler Overhead (SQLite statt Postgres) 3. Authelia bereits Production-ready 4. Einfaches Backup/Restore **Nächster Schritt:** 1. Phase 1.1 umsetzen (Links-Endpoints + DB-Schema) 2. Phase 1.2 umsetzen (Admin-UI erweitern) 3. Testen auf Staging 4. Phase 1.3 umsetzen (Next.js Integration) 5. Production Deploy Soll ich mit **Phase 1.1** (API-Erweiterung) beginnen?