Files
portfolio/DIRECTUS_MIGRATION.md
2026-01-22 20:56:35 +01:00

6.3 KiB

Directus Integration - Migration Guide

🎯 Was wurde geändert?

Das Portfolio nutzt jetzt Directus als CMS für alle Texte. Die Integration ist hybrid:

  • Directus (primär) → Texte werden aus Directus CMS geladen
  • JSON Fallback (sekundär) → Falls Directus nicht erreichbar, nutzen wir messages/*.json

📁 Neue Dateien

Core Infrastructure

  • lib/directus.ts - REST Client für Directus (nutzt de-DE, en-US)
  • lib/i18n-loader.ts - Lädt Texte mit Fallback-Chain
  • lib/translations-loader.ts - Batch-Loader für alle Sections
  • types/translations.ts - TypeScript Types für alle Translation Objects

Components

  • app/components/Header.server.tsx - Server Wrapper für Header
  • app/components/HeaderClient.tsx - Client Implementation mit Props
  • app/components/ClientWrappers.tsx - Wrapper für Hero, About, Projects, Contact, Footer
  • app/_ui/HomePageServer.tsx - Server Component lädt alle Translations

🔄 Architektur

Vorher (next-intl only)

Client Component → useTranslations("nav") → JSON File

Jetzt (Directus + Fallback)

Server Component → getNavTranslations(locale)
                → Directus API (de-DE/en-US)
                → Falls nicht gefunden: JSON File (de/en)
                → Props an Client Component
Client Component → Nutzt translations aus Props

🗄️ Directus Setup

1. Collection: messages

Felder:

  • id (Primary Key, UUID, auto)
  • key (String, required) - z.B. "nav.home"
  • locale (String, required) - WICHTIG: de-DE oder en-US (mit -)
  • value (Text, required) - Der übersetzte Text
  • translations (Translations) - Directus Native Translations Feature

WICHTIG: Du hast zwei Optionen:

Option A: Directus Native Translations (Empfohlen)

  1. Aktiviere "Translations" für messages Collection
  2. Definiere de-DE und en-US als Languages
  3. Felder: key (unique), value (translatable)
  4. Pro Key nur ein Eintrag, Directus managed Translations intern

Option B: Flat Structure (Einfacher)

  1. Keine Translations Feature
  2. Felder: key + locale + value
  3. Pro Key/Locale Kombination ein Eintrag
  4. Beispiel:
    • Row 1: key="nav.home", locale="de-DE", value="Startseite"
    • Row 2: key="nav.home", locale="en-US", value="Home"

2. Collection: content_pages (Optional)

Für längere Inhalte (z.B. Datenschutz, Impressum):

Felder:

  • id (Primary Key, UUID)
  • slug (String, unique) - z.B. "privacy-policy"
  • locale (String) - de-DE oder en-US
  • title (String)
  • content (Rich Text oder Long Text)

3. Permissions

Public Role:

  • messages: Read access (alle Felder)
  • content_pages: Read access (alle Felder)

📝 Keys eintragen

Alle Keys aus DIRECTUS_CHECKLIST.md müssen in Directus eingetragen werden.

Beispiel Keys:

nav.home
nav.about
nav.projects
nav.contact
home.hero.greeting
home.hero.name
home.hero.role
home.hero.description
...

Wichtig: Keys sind dot-separated (wie in JSON), aber Locale nutzt -:

  • key="nav.home", locale="de-DE"
  • key="nav_home", locale="de"

🔧 Environment Variables

In .env.local:

DIRECTUS_URL=https://cms.dk0.dev
DIRECTUS_STATIC_TOKEN=ogUMcHCa1CAYU1YifsoeJ_7V76o1atYG

🚀 Wie funktioniert's?

1. Seite wird geladen

// app/[locale]/page.tsx
export default async function Page({ params }) {
  const { locale } = await params;
  return <HomePageServer locale={locale} />;
}

2. Server Component lädt Translations

// app/_ui/HomePageServer.tsx
export default async function HomePageServer({ locale }) {
  const heroT = await getHeroTranslations(locale);
  // ...
  return <HeroClient locale={locale} translations={heroT} />;
}

3. Translation Loader fetcht von Directus

// lib/translations-loader.ts
export async function getHeroTranslations(locale: string) {
  // Batch-Load aus Directus
  // locale='de' wird zu 'de-DE' gemapped
  const values = await Promise.all([...]);
  return { greeting, name, role, ... };
}

4. Client Component nutzt Props

// app/components/ClientWrappers.tsx
export function HeroClient({ locale, translations }) {
  // Konvertiert zu next-intl Format
  return (
    <NextIntlClientProvider messages={messages}>
      <Hero />
    </NextIntlClientProvider>
  );
}

🔍 Fallback Chain

Für jeden Key wird gesucht:

  1. Directus (requested locale) - z.B. de-DE
  2. Directus (EN fallback) - Falls nicht gefunden: en-US
  3. JSON (normalized locale) - Falls Directus down: messages/de.json
  4. JSON (EN fallback) - Falls Key nicht existiert: messages/en.json
  5. Key selbst - Als letzter Fallback: return "nav.home"

🎨 Cache

  • In-Memory Cache mit 5 min TTL
  • Cache Key: msg:${key}:${locale}
  • Läuft im Server Memory (nicht persistent)
  • Bei Deploy/Restart wird Cache geleert

Testing

  1. Mit Directus: Trage einen Test-Key ein:

    • Key: test
    • Locale: de-DE
    • Value: Hallo von Directus!
    • Prüfe: await getLocalizedMessage('test', 'de') → "Hallo von Directus!"
  2. Ohne Directus: Stoppe Directus

    • Prüfe: Messages sollten aus JSON files kommen
    • Website sollte normal funktionieren (degraded mode)
  3. Build Test:

    npm run build
    
    • Sollte ohne Errors durchlaufen

🐛 Troubleshooting

"Key nicht gefunden"

  • Prüfe Directus GUI: Key exakt gleich? (nav.home nicht nav_home)
  • Prüfe Locale: de-DE oder en-US (mit -)?
  • Prüfe Permissions: Public role hat Read access?

"Directus nicht erreichbar"

  • Prüfe DIRECTUS_URL in .env
  • Prüfe Token: DIRECTUS_STATIC_TOKEN
  • Test: curl -H "Authorization: Bearer TOKEN" https://cms.dk0.dev/items/messages

"Texte ändern sich nicht"

  • Cache! Warte 5 Minuten oder restart Server
  • Oder: Clear Cache manuell (clearI18nCache() in lib/i18n-loader.ts)

📚 Next Steps

  1. Directus deployen (Docker auf IONOS)
  2. Collections erstellen (messages, content_pages)
  3. Keys eintragen (aus DIRECTUS_CHECKLIST.md)
  4. Testen (dev environment)
  5. Production (wenn alles funktioniert)

🎯 Benefits

  • Keine Rebuilds für Text-Änderungen
  • Non-Tech Editor kann Texte ändern (Directus GUI)
  • Graceful Degradation (JSON Fallback)
  • Type Safety (TypeScript Types für alle Translations)
  • Performance (Server-side caching, parallel loading)