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)
567 lines
14 KiB
Markdown
567 lines
14 KiB
Markdown
# 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?
|