Backend (updates-api): - Add links table schema with categories, visibility, sorting - Implement full CRUD API for links (/v1/links, /v1/admin/links) - Create admin UI for links management (/admin/links) - Add navigation between Updates and Links admin pages - Add comprehensive tests for links API (7/7 passing) Frontend (Next.js): - Create API client for fetching links from updates-api - Add LinksSection component with category icons - Integrate links into homepage (between Notes and CTA) - Add i18n entries for links section (DE/EN) - ISR caching: 60s for links, 5min for updates Features: - Links organized by category (external/internal/resource/tool) - Visibility toggle (show/hide on homepage) - Sort order support - Optional icons and descriptions - Responsive grid layout (1/2/3 columns) Testing: - All existing tests pass - New links API tests cover CRUD + validation - Production build successful Phase 1 Complete: Links management ready for deployment Next: Phase 2 (Markdown editor + media upload)
14 KiB
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):
- Admin-Login - Authelia-basiert (bereits vorhanden auf auth.w-make.com)
- News/Updates verwalten - erstellen, bearbeiten, löschen, publizieren
- Links verwalten - externe/interne Links zur Homepage hinzufügen
- Seiten bearbeiten - existierende Content-Seiten (About, Notes) editieren
Nice-to-Have (Phase 2):
- Markdown-Editor mit Preview
- Medien-Upload (Bilder für Notes)
- Multi-Sprache (DE/EN synchron editieren)
- Version History / Drafts
🏗️ Architektur-Entscheidungen
✅ Was bereits existiert:
Infrastructure:
- ✅ Authelia SSO auf
auth.w-make.com(2FA-Login) - ✅ Traefik Reverse Proxy (schützt
/adminRoutes) - ✅
updates-apiService (Node.js + SQLite)- Admin-Panel unter
https://updates.w-make.com/admin - Bearer-Token Auth für API
- SQLite-Datenbank
/data/updates.sqlite
- Admin-Panel unter
Current Content Structure:
// 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:
// 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):
-- 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:
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):
<!-- Beispiel: Links verwalten -->
<div class="admin-panel">
<h1>Links verwalten</h1>
<button @click="createLink()">Neuer Link</button>
<table>
<thead>
<tr>
<th>Titel</th>
<th>URL</th>
<th>Kategorie</th>
<th>Sichtbar</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="link in links">
<td>{{ link.title }}</td>
<td>{{ link.url }}</td>
<td>{{ link.category }}</td>
<td>{{ link.visible ? '✓' : '✗' }}</td>
<td>
<button @click="editLink(link.id)">Edit</button>
<button @click="deleteLink(link.id)">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
Styling: Design-System von w-make.com wiederverwenden (Copper/Ink/Paper Tokens)
1.3 Frontend (Next.js) integrieren
Homepage: Links-Section hinzufügen
// src/app/[lang]/page.tsx
export default async function HomePage({ params }: PageProps<"/[lang]">) {
// ...
const links = await fetchLinks(); // NEU: von updates-api
return (
<main>
{/* ... existierende Sections ... */}
{/* NEU: Links-Section */}
<section className="border-t border-line">
<div className="mx-auto w-full max-w-6xl px-6 py-20 md:px-10">
<p className="kicker">Nützliche Links</p>
<h2 className="display mt-4 text-4xl md:text-5xl">
Ressourcen und Werkzeuge
</h2>
<div className="mt-12 grid gap-8 md:grid-cols-3">
{links.map((link) => (
<a
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="border-l border-line-strong pl-5 hover:border-copper"
>
<h3 className="font-serif text-xl">{link.title}</h3>
<p className="mt-2 text-sm text-mute-strong">
{link.description}
</p>
</a>
))}
</div>
</div>
</section>
</main>
);
}
API-Client hinzufügen:
// 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):
# 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
// 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
<!-- services/updates-api/src/admin/pages.html -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<textarea id="content-de"></textarea>
<textarea id="content-en"></textarea>
<script>
const editorDE = new SimpleMDE({ element: document.getElementById("content-de") });
const editorEN = new SimpleMDE({ element: document.getElementById("content-en") });
</script>
2.2 Bild-Upload
Backend (updates-api erweitern):
// 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
<div class="dropzone" id="image-upload">
Bild hierher ziehen oder klicken
<input type="file" accept="image/*" style="display:none">
</div>
<img id="preview" style="max-width: 400px">
Phase 3: Dynamische Seiten + Notes aus DB (3-4 Tage)
3.1 Notes von TypeScript → SQLite migrieren
Migration-Script:
// 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
// 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 <article>{/* Render DB-Content */}</article>;
}
// Fallback: statische notes.ts
const staticNote = getNote(slug as NoteSlug, locale);
return <article>{/* Render static content */}</article>;
}
3.2 Neue dynamische Seiten
Use-Case: Kundenwunsch "Changelog-Seite" oder "FAQ" hinzufügen ohne Deployment
// 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 (
<main>
<article className="prose prose-invert mx-auto max-w-4xl px-6 py-20">
<h1>{page[`title_${locale}`]}</h1>
<div dangerouslySetInnerHTML={{ __html: marked(content) }} />
</article>
</main>
);
}
// 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:
# 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):
// 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
- 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:
- Schnellste Time-to-Market (baut auf vorhandener updates-api auf)
- Minimaler Overhead (SQLite statt Postgres)
- Authelia bereits Production-ready
- Einfaches Backup/Restore
Nächster Schritt:
- Phase 1.1 umsetzen (Links-Endpoints + DB-Schema)
- Phase 1.2 umsetzen (Admin-UI erweitern)
- Testen auf Staging
- Phase 1.3 umsetzen (Next.js Integration)
- Production Deploy
Soll ich mit Phase 1.1 (API-Erweiterung) beginnen?