Files
w-make-portfolio/CMS-IMPLEMENTATION-PLAN.md
Jan Wagner 97944875ca 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)
2026-08-22 16:56:00 +02:00

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):

  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):

  1. Markdown-Editor mit Preview
  2. Medien-Upload (Bilder für Notes)
  3. Multi-Sprache (DE/EN synchron editieren)
  4. 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:

// 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:

  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?