feat: Phase 1 - Admin CMS with Links Management

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)
This commit is contained in:
Jan Wagner
2026-08-22 16:56:00 +02:00
parent 1f37edfa46
commit 97944875ca
12 changed files with 1533 additions and 1 deletions
+566
View File
@@ -0,0 +1,566 @@
# 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
<!-- 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**
```tsx
// 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:**
```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
<!-- 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):**
```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
<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:**
```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 <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
```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 (
<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:
```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?