6.3 KiB
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 (nutztde-DE,en-US)lib/i18n-loader.ts- Lädt Texte mit Fallback-Chainlib/translations-loader.ts- Batch-Loader für alle Sectionstypes/translations.ts- TypeScript Types für alle Translation Objects
Components
app/components/Header.server.tsx- Server Wrapper für Headerapp/components/HeaderClient.tsx- Client Implementation mit Propsapp/components/ClientWrappers.tsx- Wrapper für Hero, About, Projects, Contact, Footerapp/_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-DEoderen-US(mit-)value(Text, required) - Der übersetzte Texttranslations(Translations) - Directus Native Translations Feature
WICHTIG: Du hast zwei Optionen:
Option A: Directus Native Translations (Empfohlen)
- Aktiviere "Translations" für
messagesCollection - Definiere
de-DEunden-USals Languages - Felder:
key(unique),value(translatable) - Pro Key nur ein Eintrag, Directus managed Translations intern
Option B: Flat Structure (Einfacher)
- Keine Translations Feature
- Felder:
key+locale+value - Pro Key/Locale Kombination ein Eintrag
- 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-DEoderen-UStitle(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:
- Directus (requested locale) - z.B.
de-DE - Directus (EN fallback) - Falls nicht gefunden:
en-US - JSON (normalized locale) - Falls Directus down:
messages/de.json - JSON (EN fallback) - Falls Key nicht existiert:
messages/en.json - 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
-
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!"
- Key:
-
Ohne Directus: Stoppe Directus
- Prüfe: Messages sollten aus JSON files kommen
- Website sollte normal funktionieren (degraded mode)
-
Build Test:
npm run build- Sollte ohne Errors durchlaufen
🐛 Troubleshooting
"Key nicht gefunden"
- Prüfe Directus GUI: Key exakt gleich? (
nav.homenichtnav_home) - Prüfe Locale:
de-DEoderen-US(mit-)? - Prüfe Permissions: Public role hat Read access?
"Directus nicht erreichbar"
- Prüfe
DIRECTUS_URLin .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
- Directus deployen (Docker auf IONOS)
- Collections erstellen (messages, content_pages)
- Keys eintragen (aus DIRECTUS_CHECKLIST.md)
- Testen (dev environment)
- 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)