ignis-cms
v0.2.3
Published
Leichtgewichtiges, GitHub-basiertes CMS für Next.js App Router Projekte
Readme
ignis-cms
Ein leichtgewichtiges, git-basiertes CMS als npm-Package für Next.js App Router Projekte.
Das Package liest YAML-Dateien aus einem /src/content-Verzeichnis, generiert automatisch eine CMS-Oberfläche unter /admin, und schreibt Änderungen per GitHub API direkt ins Repository zurück.
Kernprinzipien
- Git ist die Datenbank. Alle Inhalte leben als YAML-Dateien im Repo. Versionierung und Rollback kommen gratis über Git.
- Zero Config. Package installieren, Admin-Route einhängen, Env-Vars setzen — fertig. Kein Setup-Wizard, keine Config-Datei.
- Schema-Ableitung. Feldtypen werden automatisch aus dem YAML-Wert ermittelt. Kein separates Schema nötig.
- GitHub API statt lokalem Filesystem. Content- und Asset-Operationen funktionieren auch auf serverlosen Hosts.
Installation
npm install ignis-cmsKein transpilePackages nötig. ignis-cms wird als gebautes Package aus dist/ konsumiert.
Integrationsvertrag für AI-Agents
Dieser Abschnitt ist die verbindliche Kurzreferenz für eine automatische Integration. Ein Agent soll die genannten Dateien exakt in der Host-App anlegen und keine eigene CMS-Session, Upload-API oder Preview-Infrastruktur ergänzen.
Voraussetzungen
- Next.js 14 oder neuer mit App Router
- Node.js-Runtime für die CMS-API
- GitHub-Repository und Fine-grained Token mit
Contents: Read and write IGNIS_CMS_AUTH_SECRETmit einem zufälligen, nicht öffentlichen Wert- bestehendes oder automatisch initialisierbares
src/content-Verzeichnis
Minimale Dateistruktur der Host-App
app/
admin/
layout.tsx
[[...path]]/
page.tsx
api/
cms/
[...action]/
route.ts
src/
content/
public/
uploads/Erforderliche Dateien
// app/admin/layout.tsx
import { CmsClient } from "ignis-cms/ui";
import "ignis-cms/ui/styles.css";
export default function AdminLayout() {
return <CmsClient aiEnabled={!!process.env.OPENROUTER_API_KEY} />;
}// app/admin/[[...path]]/page.tsx
export default function AdminPage() {
return null;
}// app/api/cms/[...action]/route.ts
export const runtime = "nodejs";
export { GET, POST, PUT, DELETE, PATCH } from "ignis-cms/api";# .env.local — niemals ins Repository committen
IGNIS_CMS_AUTH_SECRET=<mindestens-32-zufällige-zeichen>
GITHUB_TOKEN=<fine-grained-token>
GITHUB_OWNER=<organisation-oder-benutzer>
GITHUB_REPO=<repository-name>
GITHUB_BRANCH=main
GITHUB_APP_BASE_PATH=
# Für serverseitige Draft-Requests
IGNIS_CMS_BASE_URL=https://www.example.comOhne IGNIS_CMS_AUTH_SECRET startet der CMS-Handler nicht. Es gibt keinen
Development- oder Standard-Fallback für dieses Secret.
Auch GITHUB_TOKEN, GITHUB_OWNER und GITHUB_REPO werden beim Start geprüft.
GITHUB_APP_BASE_PATH bleibt bei einer einzelnen Next-App leer. In einem Monorepo
enthält es den Pfad vom Repository-Root zur App, beispielsweise apps/website.
Optionale Preview-Dateien
Preview ist nur vollständig, wenn alle drei Punkte umgesetzt sind:
CmsClient.preview.routesbeschreibt die Frontend-Routen.app/api/preview/route.tsexportiertcreateIgnisPreviewHandler().- Die jeweilige Frontend-Seite lädt den zuletzt im GitHub-CMS gespeicherten Stand
mit
getIgnisPreviewDraft()oderuseIgnisPreviewDraft().
Server- und Client-Imports dürfen nicht vermischt werden:
| Umgebung | Erlaubter Import |
| --- | --- |
| Route Handler und Server Components | ignis-cms/next |
| Client Components | ignis-cms/next/client |
| Beides, ohne Next-Abhängigkeit | ignis-cms/preview |
Die vollständigen Copy-paste-Beispiele stehen unter
Optional: Next Draft Preview und in
docs/next-draft-preview.md.
Integrationsregeln
CmsClientgehört inapp/admin/layout.tsx, nicht in die Catch-all-Seite.- Die CMS-Catch-all-Route muss
GET,POST,PUT,DELETEundPATCHexportieren. - Für die API muss
runtime = "nodejs"gesetzt sein. - GitHub- und Auth-Secrets bleiben ausschließlich auf dem Server.
- Es sind weder Middleware, NextAuth,
transpilePackagesnoch eigene CMS-Routen nötig. - Die Host-App bleibt für Published-Content-Loader und das Frontend-Rendering zuständig.
- Preview-Bild-URLs dürfen nicht normalisiert und ihre Query-Parameter nicht entfernt werden.
- Dateien mit
_-Prefix insrc/contentsind intern und dürfen nicht als Seiten gerendert werden.
Prüfung nach der Integration
/adminzeigt Setup oder Login./api/cms/sessionantwortet mit JSON statt404oder500.- Nach dem Login werden Collections und Singletons angezeigt.
- Eine YAML-Änderung erzeugt einen GitHub-Commit.
- Ein Upload erscheint in
public/uploadsundsrc/content/_assets.yaml. - Bei gleichem Dateinamen wird der Upload erfolgreich umbenannt und als Warnung gemeldet.
- Falls Preview aktiv ist: Textänderungen und frisch hochgeladene Bilder erscheinen im iframe.
Setup
1. Admin-UI
CmsClient muss im Layout leben, damit der Zustand bei der Navigation erhalten bleibt. Die Styles werden einmal im Layout importiert, Fonts und optionale Feature-Flags bleiben in der Host-App.
// app/admin/layout.tsx
import { CmsClient } from "ignis-cms/ui";
import "ignis-cms/ui/styles.css";
export default function AdminLayout() {
const aiEnabled = !!process.env.OPENROUTER_API_KEY;
return <CmsClient aiEnabled={aiEnabled} />;
}// app/admin/[[...path]]/page.tsx
export default function AdminPage() {
return null; // Layout übernimmt alles
}Optional: Next Draft Preview
CmsClient kann im Editor rechts neben den Eingabefeldern eine Next.js-Vorschau anzeigen. Die Spalte erscheint nur, wenn preview konfiguriert ist und das aktuelle Dokument über routes auf eine Frontend-Route gemappt werden kann. Auf Viewports unter 1024px wird die Vorschau automatisch ausgeblendet.
Eine ausführliche End-to-End-Anleitung steht in docs/next-draft-preview.md.
// app/admin/layout.tsx
import { CmsClient } from "ignis-cms/ui";
import "ignis-cms/ui/styles.css";
export default function AdminLayout() {
return (
<CmsClient
aiEnabled={!!process.env.OPENROUTER_API_KEY}
preview={{
entrypoint: "/api/preview",
routes: {
blog: "/blog/{slug}",
pages: "/{slug}",
settings: "/",
},
}}
/>
);
}routes ist ein Mapping von Collection-Slug, Singleton-Slug oder Dokumentpfad auf eine URL-Vorlage. Unterstützte Platzhalter:
| Platzhalter | Wert |
| --- | --- |
| {slug} | Dateiname ohne .yaml |
| {collection} | Collection-Slug, z.B. blog |
| {path} | Content-Pfad ohne .yaml, z.B. blog/post-1 |
| {field.name} | Skalarwert aus dem aktuellen Formular, z.B. {field.slug} |
Der Preview-Einstieg besteht nur noch aus dem CMS-Handler:
// app/api/preview/route.ts
import { createIgnisPreviewHandler } from "ignis-cms/next";
export const GET = createIgnisPreviewHandler({
allowedPathPrefixes: ["/blog/", "/pages/"],
});In einer Server Component liest und lädt das CMS den Preview-Zustand. Bildfelder sowie
/uploads/... in Markdown und HTML werden dabei automatisch auf den geschützten
Asset-Proxy umgeschrieben:
import { getIgnisPreviewContext, getIgnisPreviewDraft } from "ignis-cms/next";
type Props = {
params: Promise<{ slug: string }> | { slug: string };
searchParams: Promise<Record<string, string | string[] | undefined>> |
Record<string, string | string[] | undefined>;
};
export default async function BlogPage({ params, searchParams }: Props) {
const [{ slug }, preview] = await Promise.all([
params,
getIgnisPreviewContext(searchParams),
]);
const documentPath = `blog/${slug}.yaml`;
const [published, draft] = await Promise.all([
loadBlogPost(slug),
getIgnisPreviewDraft(preview, documentPath),
]);
return <BlogPost data={draft?.document.data ?? published} />;
}getIgnisPreviewDraft lädt den gespeicherten CMS-Stand über die authentifizierte
Content-API. IGNIS_CMS_BASE_URL muss auf die Deployment-Domain zeigen. Draft Mode
umgeht dabei Next-/ISR-Caches; er speichert selbst keine Entwurfsdaten. Änderungen
im Formular müssen deshalb vor der Preview gespeichert werden. Mit
useIgnisPreviewDraft aus ignis-cms/next/client steht alternativ ein Client-Hook
bereit.
Die vollständige Integration, Fehlerbehandlung und Bild-Fehlersuche beschreibt
docs/next-draft-preview.md.
2. API-Route
Zero-Config
// app/api/cms/[...action]/route.ts
export const runtime = "nodejs";
export { GET, POST, PUT, DELETE, PATCH } from "ignis-cms/api";Konfigurierbar
// app/api/cms/[...action]/route.ts
import { cmsApiHandler } from "ignis-cms/api";
const github = {
token: process.env.GITHUB_TOKEN ?? "",
owner: process.env.GITHUB_OWNER ?? "",
repo: process.env.GITHUB_REPO ?? "",
branch: process.env.GITHUB_BRANCH ?? "main",
appBasePath: process.env.GITHUB_APP_BASE_PATH ?? "",
committerName: process.env.GITHUB_COMMITTER_NAME ?? "ignis CMS",
committerEmail: process.env.GITHUB_COMMITTER_EMAIL ?? "[email protected]",
};
export const runtime = "nodejs";
export const { GET, POST, PUT, DELETE, PATCH } = cmsApiHandler(
{ contentDir: "./src/content", uploadsDir: "./public/uploads" },
{ github }
);3. Umgebungsvariablen
# .env.local
# CMS Auth (Pflicht)
IGNIS_CMS_AUTH_SECRET=<zufälliger-geheimer-string>
# GitHub API — Content Reads/Writes (Pflicht)
GITHUB_TOKEN=<fine-grained-personal-access-token>
GITHUB_OWNER=<org-oder-username>
GITHUB_REPO=<repo-name>
GITHUB_BRANCH=main
# Monorepo: Pfad zur Next.js App relativ zum Repo-Root, z.B. "apps/demo"
GITHUB_APP_BASE_PATH=
GITHUB_COMMITTER_NAME=ignis CMS
[email protected]
# Passwort-Reset per Mail (optional)
MAILERSEND_API_KEY=
MAILERSEND_FROM_EMAIL=
MAILERSEND_FROM_NAME=
# AI YAML-Assistent (optional)
OPENROUTER_API_KEY=GitHub Token Berechtigungen: Contents (Read & Write), Metadata (Read).
4. Inhalt ins Repo legen
Minimal:
src/content/
_users.yamlBeispiel:
# src/content/_users.yaml
users:
- email: [email protected]
name: Admin
password: "scrypt:SALT:HASH"
role: adminBeim ersten Aufruf initialisiert ignis-cms fehlende Basisdateien selbst.
Erster Start — Auto-Init
Beim ersten Aufruf des CMS (kein /src/content-Ordner im Repo) wird automatisch ein Grundgerüst angelegt:
src/content/
settings.yaml ← Globale Einstellungen (Firmenname, SEO, Header, Footer)
_assets.yaml ← Asset-Index (wird automatisch verwaltet)
pages/
home.yaml ← Starter-SeiteKein manuelles Anlegen nötig — einfach aufrufen und loslegen.
Benutzer verwalten
Benutzer werden in src/content/_users.yaml verwaltet. Im CMS selbst ist die Benutzerverwaltung für Admins über die Seitenleiste erreichbar.
# src/content/_users.yaml
users:
- email: [email protected]
name: Admin
password: "scrypt:SALT:HASH"
role: admin
- email: [email protected]
name: Editor
password: "scrypt:SALT:HASH"
role: editorPasswort-Hash erzeugen:
import { hashPassword } from "ignis-cms";
console.log(hashPassword("mein-sicheres-passwort"));
// → scrypt:abc123...:def456...Zwei Rollen:
- admin — Neue Dokumente anlegen, löschen, Benutzer verwalten
- editor — Nur bestehende Dokumente bearbeiten
Was ignis-cms selbst mitbringt
- Session-Auth über signiertes
httpOnlyCookie - Login, Logout, Session-Check unter
/api/cms/login,/api/cms/logout,/api/cms/session - Setup-Flow für ersten Admin unter
/api/cms/setup - Passwort-Reset unter
/api/cms/password-resetwenn MailerSend konfiguriert ist - Content-, Asset- und Benutzer-API unter
/api/cms/*
Kein NextAuth, keine separate Auth-Route, keine lokale Git-Integration nötig.
Content-Struktur
src/content/
blog/ ← Collection (Ordner = mehrere Dokumente)
post-1.yaml
post-2.yaml
pages/ ← Collection
home.yaml
about.yaml
settings.yaml ← Singleton (direkt in /src/content, kein Ordner)
_assets.yaml ← Reserviert (Asset-Index, kein Content)
_users.yaml ← Reserviert (Benutzer, kein Content)Dateien mit _-Prefix sind reserviert und erscheinen nicht als Content in der UI.
Schema-Ableitung
Feldtypen werden automatisch aus dem YAML-Wert ermittelt:
| YAML-Wert | Feld-Typ | UI-Komponente |
|---|---|---|
| "Einzeiliger Text" | text | Text Input |
| mehrzeiliger String mit \| | richtext | Textarea |
| "/uploads/bild.jpg" | image | Asset Picker (Bilder) |
| "/uploads/doc.pdf" | file | Asset Picker (alle Typen) |
| 2025-03-15 | date | Date Picker |
| true / false | boolean | Toggle |
| 42 / 3.14 | number | Number Input |
| ["a", "b"] | list | Tag-Liste |
| [{label: "X", url: "/"}] | list_objects | Verschachteltes Formular |
| {title: "...", body: "..."} | group | Fieldset / Accordion |
Schema-Override (optional)
Für Select-Felder oder Validierung kann pro Collection eine _schema.yaml angelegt werden:
# src/content/blog/_schema.yaml
fields:
category:
type: select
options:
- tutorial
- news
- reviewYAML-Editor
Jedes Dokument lässt sich über den YAML-Button in der Toolbar als Roh-YAML bearbeiten. Der Editor bietet:
- Syntax-Highlighting
- Echtzeit-Fehler- und Warnungsanzeige
- Formatieren-Button (nur bei gültigem YAML aktiv)
- Tab fügt 2 Leerzeichen ein
Wechsel zwischen Formular- und YAML-Modus ist verlustfrei — Änderungen werden sofort übernommen.
AI YAML-Assistent
Wenn OPENROUTER_API_KEY gesetzt ist, erscheint im YAML-Editor eine KI-Eingabezeile. Eingabe in natürlicher Sprache — das Modell (Claude 3.5 Haiku via OpenRouter) ergänzt oder verändert das YAML entsprechend.
Beispiele:
- „Füge ein neues Feld og_image hinzu"
- „Ändere den Titel auf Hallo Welt"
- „Ergänze drei weitere Navigationseinträge"
Asset Library
Dateien werden nach /public/uploads/ hochgeladen (max. 10 MB). Metadaten (Titel, Alt-Text) werden automatisch in src/content/_assets.yaml gespeichert. Image- und File-Felder öffnen einen Asset Picker.
Erlaubte Dateitypen: JPG, PNG, GIF, WebP, SVG, AVIF, MP4, WebM, MOV, AVI, MKV, OGV, PDF, DOC, DOCX, XLS und XLSX. Dateiendung und vom Browser gemeldeter MIME-Typ müssen zusammenpassen.
Dateinamen werden vor dem Upload in lowercase Kebab-Case normalisiert. Existiert
der normalisierte Name bereits, bleibt der Upload erfolgreich und erhält den
nächsten freien Suffix: hero.jpg wird beispielsweise zu hero-1.jpg. Die API
antwortet in diesem Fall mit HTTP 200 und einer nicht-fehlerhaften Warnung:
{
"ok": true,
"asset": {
"file": "/uploads/hero-1.jpg"
},
"warning": {
"code": "ASSET_RENAMED",
"message": "„hero.jpg“ existiert bereits und wurde als „hero-1.jpg“ gespeichert.",
"requestedFilename": "hero.jpg",
"filename": "hero-1.jpg"
}
}Die Asset-Bibliothek zeigt diese Antwort als gelben Hinweis. Im Feld-Picker bleibt
der Dialog geöffnet und markiert die umbenannte Datei, damit der neue Name vor der
Auswahl sichtbar ist. ASSET_RENAMED darf von Integrationen nicht als Uploadfehler
behandelt werden.
Passwort-Reset per E-Mail (optional)
Wenn MAILERSEND_API_KEY gesetzt ist, erscheint auf der Login-Seite ein „Passwort vergessen"-Link. Der Reset-Link ist 1 Stunde gültig und wird über MailerSend versendet.
Tastaturkürzel
| Kürzel | Funktion |
|---|---|
| Cmd+S / Ctrl+S | Dokument speichern |
Git-Commits
Jede schreibende Operation erzeugt automatisch einen Git Commit via GitHub API:
| Operation | Commit Message |
|---|---|
| Dokument anlegen | content: create blog/post-1 |
| Dokument bearbeiten | content: update blog/post-1 |
| Dokument duplizieren | content: create blog/post-1-kopie |
| Dokument löschen | content: delete blog/post-1 |
| Asset hochladen | assets: upload hero.jpg |
| Asset-Metadaten | assets: update metadata hero.jpg |
| Asset löschen | assets: delete hero.jpg |
| Passwort geändert | users: update account [email protected] |
| Passwort zurückgesetzt | users: reset password [email protected] |
Asset-Uploads und Asset-Löschungen schreiben Binärdatei und _assets.yaml in
einem gemeinsamen Git-Commit. Der Branch wird niemals erzwungen verschoben;
bei einem parallelen Commit wird die vollständige Änderung auf dem neuen HEAD
erneut aufgebaut.
Öffentliche Package-Exports
| Import | Umgebung | Inhalt |
| --- | --- | --- |
| ignis-cms | Server | Content-Typen, Schema-Helfer, hashPassword |
| ignis-cms/ui | Client | CmsClient und UI-Komponenten |
| ignis-cms/ui/styles.css | CSS | erforderliche CMS-Styles |
| ignis-cms/api | Route Handler | CMS-Catch-all-Handler und konfigurierbare API |
| ignis-cms/next | Server | Preview-Handler, Kontext und Server-Draft-Loader |
| ignis-cms/next/client | Client | Preview-Hook und IgnisDraftPreview |
| ignis-cms/preview | universell | Preview-Typen, Asset- und Routing-Utilities |
Nicht dokumentierte Pfade innerhalb von dist/ sind keine öffentliche API und
dürfen von Host-Apps oder Agents nicht importiert werden.
Deployment-Hinweise
- Content-, Benutzer- und Asset-Schreibvorgänge verwenden die GitHub API und benötigen kein persistentes lokales Dateisystem.
public/uploadsbeschreibt den Repository-Pfad; neu hochgeladene Preview-Assets werden bis zum nächsten Deployment über den geschützten CMS-Asset-Proxy geladen.- Preview-Daten werden aus der GitHub-basierten CMS-API geladen; es gibt keinen prozesslokalen Draft-Store und keine Anforderung an Session-Stickiness.
Anforderungen
- Next.js 14+ (App Router)
- Node.js 18+
- GitHub Repository (Public oder Private)
- Fine-grained GitHub Token: Contents (Read & Write), Metadata (Read)
Lizenz
MIT
ignis CMS — Migration Prompt
Diesen Prompt an eine KI übergeben, um eine bestehende Website ins ignis CMS zu migrieren.
Du migrierst eine bestehende Website ins ignis CMS. Das ignis CMS speichert alle Inhalte als YAML-Dateien in einem /src/content-Verzeichnis und schreibt sie via GitHub API ins Repository.
## Deine Aufgabe
Analysiere die Website (URL, Screenshot, HTML oder Beschreibung) und erstelle für jede Seite eine strukturierte YAML-Datei. Alle Inhalte werden 1:1 übernommen — kein Text wird verändert, gekürzt oder umformuliert.
## Dateistruktur
src/content/
pages/
home.yaml <- Startseite
about.yaml <- Über uns
leistungen.yaml <- Leistungsseite
kontakt.yaml <- Kontaktseite
[weitere].yaml
settings.yaml <- Globale Einstellungen (Firmenname, SEO, Navigation, Footer)
Kein globals/-Ordner. Navigation, Footer und SEO-Defaults gehören alle in settings.yaml.
## settings.yaml Struktur
companyName: "Firmenname GmbH"
seo:
title: "Firmenname GmbH"
description: "Kurze Beschreibung für Google (max. 160 Zeichen)."
keywords: "keyword1, keyword2"
ogImage: "/uploads/og-image.jpg"
header:
logo: "/uploads/logo.svg"
navigation:
- label: "Startseite"
url: "/"
- label: "Leistungen"
url: "/leistungen"
- label: "Über uns"
url: "/ueber-uns"
- label: "Kontakt"
url: "/kontakt"
cta:
label: "Anfragen"
url: "/kontakt"
footer:
tagline: "Ihr Partner für..."
links:
- label: "Impressum"
url: "/impressum"
- label: "Datenschutz"
url: "/datenschutz"
copyright: "© 2025 Firmenname GmbH"
## Sektions-Typen
Erkenne Sektionen anhand der visuellen Struktur. Typische Sektionen (beispielsweise, nicht exklusiv):
- hero — Hauptbanner: Überschrift, Subtext, CTA-Button, Bild/Video
- intro — Kurze Einleitung, oft zentriert
- features — Vorteile/Leistungen als Liste von Objekten
- about — Über-uns-Abschnitt, Bild + Text
- team — Personen-Grid (Liste mit name, role, image)
- testimonials — Kundenstimmen (Liste mit quote, author, company)
- gallery — Bildergalerie (Liste von Bild-Pfaden)
- cta — Call-to-Action-Block
- contact — Kontaktinformationen
- faq — Häufige Fragen (Liste mit question + answer)
- stats — Kennzahlen (Liste mit value + label)
- logos — Partner-Logos (Liste von Bild-Pfaden)
- text — Reiner Textblock (für rechtliche Seiten)
## Feldtypen
# Einzeilige Texte -> einfache Strings
title: "Willkommen bei uns"
button_text: "Jetzt anfragen"
button_url: "/kontakt"
# Mehrzeilige Texte / Absätze -> Block-Scalar mit |
body: |
Hier steht ein längerer Text.
Er kann über mehrere Zeilen gehen.
# Bilder -> Pfad (Platzhalter wenn noch kein Upload)
image: "/uploads/hero-bild.jpg"
# Video-URLs
video_url: "https://www.youtube.com/watch?v=..."
video_url: "/uploads/hero-video.mp4"
# Booleans
visible: true
# Listen von Strings
tags:
- "Solar"
- "Elektro"
# Listen von Objekten (Features, Team, FAQ, etc.)
items:
- title: "Leistung 1"
description: "Beschreibung der Leistung."
icon: "check"
- title: "Leistung 2"
description: "Weitere Leistung."
icon: "shield"
## YAML-Template pro Seite
# Seitenweite SEO (überschreibt settings.yaml Defaults)
seo:
title: "Seitentitel | Firmenname"
description: "Kurze Beschreibung der Seite (max. 160 Zeichen)."
hero:
title: "Hauptüberschrift"
subtitle: "Kurzer Satz darunter"
body: |
Optionaler längerer Text.
image: "/uploads/hero.jpg"
button:
text: "Jetzt kontaktieren"
href: "/kontakt"
intro:
title: "Wer wir sind"
body: |
Fließtext der Einleitung.
features:
title: "Unsere Leistungen"
items:
- title: "Leistung 1"
description: "Beschreibung."
icon: "zap"
- title: "Leistung 2"
description: "Beschreibung."
icon: "shield"
ctaSection:
title: "Bereit loszulegen?"
body: "Kontaktieren Sie uns noch heute."
button:
text: "Jetzt kontaktieren"
href: "/kontakt"
## Bilder
### Mit Tool-Zugriff (empfohlen)
1. Website analysieren -> alle Bild-URLs sammeln
2. Für jedes Bild: curl -L "<url>" -o /tmp/name.jpg, dann POST an /api/cms/upload
3. Zurückgegebenen Pfad (/uploads/name.jpg) im YAML verwenden
4. YouTube/Vimeo: URL direkt als String (kein Download)
5. Selbst gehostete Videos: herunterladen -> hochladen wie Bilder
Dateinamen: Englisch, kebab-case (hero-solar-panel.jpg, team-max-mueller.jpg).
### Ohne Tool-Zugriff
- Platzhalter eintragen: /uploads/beschreibender-name.jpg
- Am Ende Liste aller Platzhalter für manuellen Upload ausgeben
## Wichtige Regeln
1. Kein Inhalt weglassen — Jeder Text, jedes Bild, jede Information übernehmen
2. Keine Texte verändern — 1:1, kein Umformulieren, kein Kürzen
3. Kein globals/-Ordner — Navigation, Footer und Default SEO gehören in settings.yaml
4. Sinnvolle Sektions-Benennung — Nicht alle Felder flach auf oberster Ebene
5. Wiederholende Inhalte -> immer als items-Liste mit Objekten
6. Konsistente Feldnamen über alle Seiten: immer title, nie abwechselnd headline/heading
## Ausgabe
### Mit Tool-Zugriff
1. Bilder herunterladen und hochladen
2. YAML-Dateien direkt in src/content/ schreiben
3. Kurze Zusammenfassung: Seiten migriert, Bilder hochgeladen
### Ohne Tool-Zugriff
Pro Seite:
1. Dateipfad als Kommentar: # src/content/pages/home.yaml
2. Vollständiger YAML-Inhalt
3. Am Ende: Liste aller Bild-Platzhalter
Reihenfolge: settings.yaml -> Startseite -> weitere Seiten