npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-cms

Kein 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_SECRET mit 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.com

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

  1. CmsClient.preview.routes beschreibt die Frontend-Routen.
  2. app/api/preview/route.ts exportiert createIgnisPreviewHandler().
  3. Die jeweilige Frontend-Seite lädt den zuletzt im GitHub-CMS gespeicherten Stand mit getIgnisPreviewDraft() oder useIgnisPreviewDraft().

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

  • CmsClient gehört in app/admin/layout.tsx, nicht in die Catch-all-Seite.
  • Die CMS-Catch-all-Route muss GET, POST, PUT, DELETE und PATCH exportieren.
  • Für die API muss runtime = "nodejs" gesetzt sein.
  • GitHub- und Auth-Secrets bleiben ausschließlich auf dem Server.
  • Es sind weder Middleware, NextAuth, transpilePackages noch 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 in src/content sind intern und dürfen nicht als Seiten gerendert werden.

Prüfung nach der Integration

  1. /admin zeigt Setup oder Login.
  2. /api/cms/session antwortet mit JSON statt 404 oder 500.
  3. Nach dem Login werden Collections und Singletons angezeigt.
  4. Eine YAML-Änderung erzeugt einen GitHub-Commit.
  5. Ein Upload erscheint in public/uploads und src/content/_assets.yaml.
  6. Bei gleichem Dateinamen wird der Upload erfolgreich umbenannt und als Warnung gemeldet.
  7. 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.yaml

Beispiel:

# src/content/_users.yaml
users:
  - email: [email protected]
    name: Admin
    password: "scrypt:SALT:HASH"
    role: admin

Beim 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-Seite

Kein 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: editor

Passwort-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 httpOnly Cookie
  • 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-reset wenn 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
      - review

YAML-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/uploads beschreibt 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