Design wechseln

Astro Content Collections – vollständiger Leitfaden: Von Konzepten zu Schema-Validierung

Easton editorial illustration: performance inspection lens

Die Blog-Startseite ist abgestürzt – Fehlermeldung: falsches Format im Feld publishDate. Eine halbe Stunde Suche in den Dateien, bis klar wurde: Datum als 2024/12/01 statt 2024-12-01. Bei nur 30 Artikeln schon mühsam; bei Hunderten wird jede neue Pflichtfeld-Änderung zum manuellen Durchchecken aller Dateien.

Content Collections lösen genau das: Astro erkennt Content-Fehler automatisch – ähnlich wie TypeScript Code-Fehler. Nach Schema-Setup gibt es intelligente Editor-Vorschläge, ohne ständig in der Doku nach Feldnamen zu suchen. Dieser Artikel erklärt, was Content Collections sind, wie die Konfiguration aussieht und wie Schema-Validierung funktioniert.

Was sind Content Collections – und warum braucht man sie?

Vielleicht denken Sie: Content Collections sind doch nur Markdown-Ordner – reicht nicht ein blog/-Ordner unter src/pages/?

Funktional ja. Aber ohne Typsicherheit.

Klassisch sieht Frontmatter so aus:


---

title: "Mein Blogtitel"
date: "2024-12-01"
tags: ["Astro", "Tutorial"]

---

Artikelinhalt...

Wirkt harmlos – bis:

  • tags als tag geschrieben (fehlendes s)
  • Datum als 12/01/2024 statt 2024-12-01
  • neues Feld author, in alten Artikeln vergessen

Diese Fehler meldet Astro nicht vorab. Erst zur Laufzeit bricht die Seite – dann suchen Sie.

Content Collections sind ein typsicheres Content-Management: TypeScript-Typprüfung für Markdown.

Konkret:

  1. Schema-Validierung: Frontmatter-Typen und -Struktur; Abweichungen = Build-Fehler
  2. Automatische Typen: TypeScript aus dem Schema, intelligente Editor-Vorschläge
  3. Einheitliche API: getCollection() usw. mit typsicheren Rückgaben
  4. Performance: Content Layer API in Astro 5.0 beschleunigt Abfragen

Kurz: klassisch „frei, aber unsicher“ – Content Collections „eingeschränkt, aber zuverlässig“. Etwas Setup-Zeit für das Schema spart fast alle trivialen Fehler.

Ehrlich gesagt nutze ich in allen Astro-Projekten Content Collections. Einmal konfiguriert, profitiert das ganze Projekt.

Content Collections – Konfiguration in der Praxis

Theorie reicht – wir konfigurieren direkt. Drei Schritte: Verzeichnis, Config, Content.

Schritt 1: Verzeichnis

Inhalte gehören nach src/content/. Reservierverzeichnis seit Astro v2.0.

Struktur etwa so:

src/
├── content/
│   ├── blog/          # Blog-Collection
│   │   ├── post-1.md
│   │   └── post-2.md
│   └── docs/          # Docs-Collection
│       ├── guide-1.md
│       └── guide-2.md
├── content.config.ts   # Konfiguration (Achtung: Ort!)
└── pages/
    └── ...

Hinweis: Die Datei heißt src/content.config.ts (oder .js/.mjs) – nicht in content/. Den Fehler habe ich am Anfang selbst gemacht.

Jeder Unterordner ist eine Collection: src/content/blog/ = Collection blog, docs/ = docs.

Schritt 2: Konfigurationsdatei

src/content.config.ts – Herzstück von Content Collections:

// src/content.config.ts
import { defineCollection, z } from 'astro:content';

// Blog-Collection definieren
const blogCollection = defineCollection({
  type: 'content',  // Markdown/MDX
  schema: z.object({
    title: z.string(),                    // Pflicht
    description: z.string(),              // Pflicht
    pubDate: z.coerce.date(),             // String → Date
    tags: z.array(z.string()).optional(), // optional
    draft: z.boolean().default(false),    // Standard false
  }),
});

export const collections = {
  'blog': blogCollection,  // Schlüssel = Verzeichnisname
};

Kurz erklärt:

  1. defineCollection() – Collection-Konfiguration
  2. type: 'content' – Markdown/MDX
  3. schema – Frontmatter-Struktur mit Zod
  4. collections – Export; Schlüssel = Ordnername

Im schema definiert jedes Feld den Typ:

  • z.string() – String
  • z.coerce.date() – String wird zu Date
  • z.array(z.string()) – String-Array
  • .optional() – optional
  • .default(false) – Standardwert

Schritt 3: Content-Dateien

Unter src/content/blog/ Markdown anlegen:


---

title: "Einstieg in Astro Content Collections"
description: "Content Collections konfigurieren und nutzen"
pubDate: "2024-12-01"
tags: ["Astro", "Tutorial"]

---

Artikelinhalt...

Passt Frontmatter zum Schema, parst Astro korrekt. Weicht etwas ab (z. B. pubDate-Format), Fehler beim Build.

Daten in Seiten abfragen

In jeder Astro-Datei:


---

// src/pages/blog/index.astro
import { getCollection } from 'astro:content';

const allPosts = await getCollection('blog');
const publishedPosts = allPosts.filter(post => !post.data.draft);

---

<ul>
  {publishedPosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>
        {post.data.title}
      </a>
      <p>{post.data.description}</p>
    </li>
  ))}
</ul>

post.data ist das Frontmatter – mit vollständigen TypeScript-Typen. Bei post.data. schlägt der Editor title, description, pubDate usw. vor.

Das ist der Gewinn: Typsicherheit plus intelligente Vorschläge – spürbar bessere Developer Experience.

Schema-Validierung im Detail

z.string() und z.coerce.date() waren nur der Anfang. Hier vertiefen wir Zod.

Basis-Typen

import { z } from 'astro:content';

z.string()           // String
z.number()           // Zahl
z.boolean()          // Boolean
z.date()             // Date-Objekt
z.coerce.date()      // String → Date
z.array(z.string())  // String-Array
z.enum(['draft', 'published'])  // Enum

z.coerce.date() ist besonders praktisch: In Frontmatter stehen Daten als Strings ("2024-12-01"). z.date() verlangt ein Date-Objekt und scheitert – z.coerce.date() konvertiert automatisch.

Optionale Felder und Defaults

Nicht alles ist Pflicht – z. B. tags:

schema: z.object({
  title: z.string(),                    // Pflicht
  tags: z.array(z.string()).optional(), // optional
  draft: z.boolean().default(false),    // Default
})

Fehlt ein Feld mit .default(), setzt Astro den Standardwert.

Erweitert: Bildvalidierung

Astro bietet image() für Bildpfade:

import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  schema: ({ image }) => z.object({  // Funktionsform
    title: z.string(),
    cover: image(),  // Bildpfad-Validierung
  }),
});

image() prüft gültige Bildpfade (auch relativ) – ideal für Cover auf der Blog-Übersicht.

Referenz auf andere Collections: z.reference()

Inhalte verknüpfen – z. B. Blog-Beitrag gehört zu einer Kategorie-Collection:

const categoryCollection = defineCollection({
  schema: z.object({
    name: z.string(),
    slug: z.string(),
  }),
});

const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    category: z.reference('category'),  // Referenz
  }),
});

export const collections = {
  'category': categoryCollection,
  'blog': blogCollection,
};

Im Frontmatter reicht der Dateiname der Kategorie (ohne Endung):


---

title: "Mein Beitrag"
category: "tech"  # src/content/category/tech.md

---

Astro prüft Existenz und Typ der Referenz.

Verschachtelte Objekte

Komplexes Frontmatter:

schema: z.object({
  title: z.string(),
  author: z.object({
    name: z.string(),
    email: z.string().email(),
    avatar: z.string().url(),
  }),
  seo: z.object({
    keywords: z.array(z.string()),
    description: z.string().max(160),
  }).optional(),
})

Entsprechendes Frontmatter:


---

title: "Artikeltitel"
author:
  name: "Max Mustermann"
  email: "[email protected]"
  avatar: "https://example.com/avatar.jpg"
seo:
  keywords: ["Astro", "Tutorial"]
  description: "Tutorial zu Astro Content Collections"

---

Typsicherheit: automatische TypeScript-Inferenz

Nach Schema-Setup generiert Astro die Typen. Beim Abfragen:

import { getCollection } from 'astro:content';

const posts = await getCollection('blog');

posts.forEach(post => {
  console.log(post.data.title);       // ✅ string
  console.log(post.data.pubDate);     // ✅ Date
  console.log(post.data.tags);        // ✅ string[] | undefined
  console.log(post.data.notExist);    // ❌ Feld existiert nicht
});

Keine manuellen Typdefinitionen – Schema und Code bleiben synchron.

getEntry() vs. getCollection()

  • getCollection('blog') – gesamte Collection
  • getEntry('blog', 'my-post') – ein Beitrag per Slug, effizienter für Detailseiten

---

// src/pages/blog/[slug].astro
import { getEntry } from 'astro:content';

const { slug } = Astro.params;
const post = await getEntry('blog', slug);

if (!post) {
  return Astro.redirect('/404');
}

const { Content } = await post.render();

---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

Zod wirkt am Anfang ungewohnt – nach wenigen Projekten sitzt es; Fehlermeldungen sind meist eindeutig.

Häufige Probleme und Lösungen

Beim Setup tauchen typische Fehler auf – hier die häufigsten aus eigener Erfahrung.

Fehler 1: MarkdownContentSchemaValidationError

Frontmatter entspricht nicht dem Schema, z. B.:

blog → my-post.md frontmatter does not match collection schema.
- "title" is required
- "pubDate" must be a valid date

Astro nennt Datei (my-post.md) und Feld (title, pubDate).

Ursachen und Fixes:

  1. Feld fehlt – Pflichtfeld im Schema, nicht im Frontmatter → ergänzen oder .optional()
  2. Tippfehler – z. B. publishDate statt pubDate → einheitliche Namen, Editor-Autocomplete
  3. Typ passt nicht – z. B. z.number(), Wert ist String → Format prüfen

Fehler 2: InvalidContentEntryFrontmatterError

YAML-Syntax defekt – Parsing scheitert:


---

title: "Mein Titel
description: "Anführungszeichen nicht geschlossen"

---

Lösung: YAML prüfen (Anführungszeichen, Doppelpunkt, Einrückung). Editor mit YAML-Unterstützung hilft.

Fehler 3: Datumsformat

Mit z.date() statt z.coerce.date() verlangt Astro ein Date-Objekt im Frontmatter – YAML erlaubt aber nur Strings.

Lösung:

// ❌ erwartet Date-Objekt, Frontmatter ist String
pubDate: z.date()

// ✅ konvertiert String → Date
pubDate: z.coerce.date()

Legacy-Daten: .passthrough()

Viele alte Artikel mit uneinheitlichem Frontmatter – vorübergehend lockern:

schema: z.object({
  title: z.string(),
  // ... weitere Felder
}).passthrough()  // zusätzliche Felder erlaubt

Das ist eine Übergangslösung – langfristig Frontmatter vereinheitlichen.

Mehrere Collections

Blog, Docs, Cases parallel:

src/content/
├── blog/
├── docs/
└── case-studies/

In content.config.ts:

const blogCollection = defineCollection({ /* ... */ });
const docsCollection = defineCollection({ /* ... */ });
const caseStudiesCollection = defineCollection({ /* ... */ });

export const collections = {
  'blog': blogCollection,
  'docs': docsCollection,
  'case-studies': caseStudiesCollection,
};

Jede Collection kann ein eigenes Schema haben.

Schema – Best Practices

  1. Wenige Pflichtfelder – nur wirklich Nötiges; Rest .optional() oder .default()
  2. Datum: z.coerce.date() – keine manuelle Konvertierung
  3. camelCasepubDate statt pub_date
  4. Komplexität splitten – mehrere Collections, z.reference() für Verknüpfungen
  5. Kommentare im Schema – Zweck jedes Felds für das Team dokumentieren

Checkliste bei Fehlern

  • Existiert src/content/?
  • Liegt src/content.config.ts unter src/ (nicht in content/)?
  • Stimmen collections-Schlüssel mit Ordnernamen überein?
  • Ist YAML syntaktisch korrekt?
  • Sind alle Pflichtfelder gesetzt?
  • Entsprechen Feldtypen dem Schema?

Die Meldungen von Astro sind meist klar – Fehlertext lesen reicht oft zur schnellen Lokalisierung.

Fazit

Die drei Ausgangsprobleme im Überblick:

Was sind Content Collections? Typsichere Verwaltung für Markdown – Fehler beim Build, nicht erst wenn die Seite bricht.

Wie schreibt man die Config? src/content/ anlegen, src/content.config.ts mit defineCollection() und Zod – Schlüssel = Verzeichnisname.

Wie nutzt man Schema-Validierung? Basis: z.string(), z.coerce.date(), z.array(); .optional() und .default(); bei Fehlern die Astro-Meldung lesen.

Content Collections gehören für mich zu den wichtigsten Astro-Features. Kurzes Setup, langfristig weniger Debug-Zeit – plus Editor-Hints, die sich beim Schreiben sofort auszahlen.

Nächste Schritte

  1. Neues Projekt: von Anfang an Content Collections – saubere Konvention von Tag eins
  2. Bestehendes Projekt: zuerst .passthrough(), dann Frontmatter schrittweise vereinheitlichen
  3. Dokumentation: Astro Content Collections für die vollständige API

Am meisten hilft Ausprobieren – einmal content.config.ts selbst schreiben. Danach wirkt typsicheres Content-Management selbstverständlich.

Astro Content Collections – vollständiger Konfigurationsablauf

Vom leeren Setup bis zur Schema-Validierung: typsicheres Content-Management mit Content Collections

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Verzeichnisstruktur anlegen

    Legen Sie im Projektroot das Verzeichnis src/content/ an:
    • Astro-Reservierverzeichnis (seit v2.0)
    • Unter content/ Unterordner als Collections (z. B. blog/, docs/)
    • Jeder Unterordner ist eine Collection

    Hinweis: Die Konfigurationsdatei src/content.config.ts liegt nicht in content/, sondern unter src/.
  2. 2

    Step 2: Konfigurationsdatei erstellen

    Erstellen Sie src/content.config.ts:

    1. Abhängigkeiten importieren:
    import { defineCollection, z } from 'astro:content'

    2. Collection konfigurieren:
    const blogCollection = defineCollection({
    type: 'content',
    schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).optional(),
    draft: z.boolean().default(false)
    })
    })

    3. collections exportieren:
    export const collections = { 'blog': blogCollection }
    Hinweis: Der Schlüssel muss exakt dem Verzeichnisnamen entsprechen
  3. 3

    Step 3: Content-Dateien anlegen

    Markdown-Dateien unter src/content/blog/ erstellen:

    Das Frontmatter muss dem Schema entsprechen:
    • title (Pflicht, String)
    • description (Pflicht, String)
    • pubDate (Datumsformat z. B. "2024-12-01", z.coerce.date() konvertiert automatisch)
    • tags (optional, String-Array)
    • draft (optional, Boolean, Standard false)

    Weicht ein Feld vom Schema ab, meldet Astro den Fehler bereits beim Build.
  4. 4

    Step 4: Daten in Seiten abfragen

    In Astro-Dateien getCollection importieren:
    import { getCollection } from 'astro:content'

    Alle Blog-Beiträge:
    const allPosts = await getCollection('blog')

    Entwürfe filtern:
    const publishedPosts = allPosts.filter(post => !post.data.draft)

    Daten nutzen:
    • post.data hat vollständige TypeScript-Typen
    • Der Editor schlägt title, description, pubDate usw. vor
    • Einzelbeitrag effizienter mit getEntry('blog', slug)
  5. 5

    Step 5: Erweiterte Schema-Konfiguration

    Bildvalidierung:
    schema: ({ image }) => z.object({
    cover: image()
    })

    Andere Collections referenzieren:
    z.reference('category')

    Verschachtelte Objekte:
    z.object({
    author: z.object({
    name: z.string(),
    email: z.string().email(),
    avatar: z.string().url()
    })
    })

    Legacy-Daten:
    .passthrough() erlaubt zusätzliche, nicht definierte Felder

    Mehrere Collections:
    Im collections-Objekt blog, docs, case-studies usw. getrennt definieren
  6. 6

    Step 6: Typische Fehler beheben

    MarkdownContentSchemaValidationError: fehlende Felder ergänzen oder .optional(), Tippfehler in Feldnamen vereinheitlichen, Typen prüfen. InvalidContentEntryFrontmatterError: YAML-Syntax (Anführungszeichen, Doppelpunkt, Einrückung). Datumsformat: z.coerce.date() statt z.date(). Checkliste: content/-Verzeichnis vorhanden, content.config.ts am richtigen Ort, collections-Schlüssel = Verzeichnisname, YAML korrekt, Pflichtfelder gesetzt, Typen passend zum Schema.

FAQ

Was sind Content Collections – und warum braucht man sie?
Content Collections sind ein typsicheres Content-Management-System – im Kern TypeScript-Typprüfung für Markdown-Dateien.

Klassisch (blog/ direkt unter src/pages/) gibt es keine Typsicherheit:
• Tippfehler (tags statt tag)
• falsches Datumsformat (12/01/2024 statt 2024-12-01)
• neue Felder in alten Artikeln vergessen
• Astro warnt nicht vorab – erst beim Rendern bricht die Seite

Content Collections bieten:
1) Schema-Validierung (Frontmatter-Typen und -Struktur, Fehler sofort beim Build)
2) automatische Typgenerierung (TypeScript aus Schema, intelligente Editor-Vorschläge)
3) einheitliche Query-API (getCollection() usw. mit typsicheren Rückgaben)
4) Performance (Content Layer API in Astro 5.0 beschleunigt Abfragen)

Klassisch: frei, aber unsicher. Content Collections: eingeschränkt, aber zuverlässig – einmal konfiguriert, profitiert das ganze Projekt.
Wie konfiguriert man Content Collections – der komplette Ablauf?
Drei Schritte:

1) Verzeichnis:
• src/content/ anlegen (Astro-Reservierverzeichnis seit v2.0)
• Unterordner als Collections (blog/, docs/) – jeder Ordner = eine Collection

2) Konfiguration:
• src/content.config.ts (nicht in content/)
• defineCollection und z importieren
• type: 'content' für Markdown/MDX, schema mit Zod für Frontmatter
• collections exportieren – Schlüssel = Verzeichnisname, z. B. 'blog': blogCollection

3) Content:
• Markdown unter src/content/blog/
• Frontmatter muss Schema entsprechen, sonst Build-Fehler
Wie nutzt man Schema-Validierung – welche Zod-Typen sind üblich?
Mit Zod typisieren:
• z.string() String
• z.number() Zahl
• z.boolean() Boolean
• z.date() Date-Objekt
• z.coerce.date() String → Date (praktisch, YAML nur Strings)
• z.array(z.string()) String-Array
• z.enum(['draft', 'published']) Enum

Optional und Defaults:
• .optional() optional
• .default(false) Standardwert

Erweitert:
• image() für Bildpfade: schema: ({ image }) => z.object({ cover: image() })
• z.reference('category') andere Collection referenzieren
• verschachtelte Objekte: z.object({ author: z.object({ name: z.string(), email: z.string().email() }) })

Nach Schema-Setup generiert Astro TypeScript-Typen – post.data ist vollständig typisiert.
Wie fragt man Content Collections ab – getCollection vs. getEntry?
API:
• getCollection('blog') – gesamte Collection, Array
• getEntry('blog', 'my-post') – ein Eintrag per Slug, effizienter für Detailseiten

Import:
import { getCollection, getEntry } from 'astro:content'

Nutzung:
• post.data = Frontmatter mit TypeScript-Hints
• Editor schlägt title, description, pubDate vor

Entwürfe filtern:
const publishedPosts = allPosts.filter(post => !post.data.draft)

Einzelbeitrag:
const post = await getEntry('blog', slug)
if (!post) return Astro.redirect('/404')
const { Content } = await post.render()
Welche Fehler treten häufig auf – und wie behebt man sie?
Typische Fehler:

1) MarkdownContentSchemaValidationError (Frontmatter ≠ Schema):
• fehlende Felder ergänzen oder .optional()
• Feldnamen vereinheitlichen (Editor-Autocomplete)
• Typen und Werte prüfen

2) InvalidContentEntryFrontmatterError (YAML-Syntax):
• Anführungszeichen, Doppelpunkt, Einrückung
• Editor mit YAML-Plugin empfohlen

3) Datumsformat:
• z.coerce.date() statt z.date() (YAML nur Strings)

4) Legacy-Daten:
• .passthrough() lockert kurzfristig, langfristig Frontmatter vereinheitlichen

Checkliste:
• content/ vorhanden
• content.config.ts unter src/, nicht in content/
• collections-Schlüssel = Ordnername
• YAML korrekt
• Pflichtfelder gesetzt
• Typen = Schema
Mehrere Collections – Schema-Best Practices?
Mehrere Collections:
• blog/, docs/, case-studies/ unter src/content/
• in content.config.ts getrennt:
const blogCollection = defineCollection({...})
const docsCollection = defineCollection({...})
• export: 'blog': blogCollection, 'docs': docsCollection, 'case-studies': caseStudiesCollection
• jedes Schema unabhängig

Best Practices:
1) wenige Pflichtfelder – Rest .optional() oder .default()
2) Datum mit z.coerce.date()
3) camelCase (pubDate statt pub_date)
4) komplexes Frontmatter aufteilen, z.reference() für Verknüpfungen
5) Kommentare im Schema für das Team

7 Min. Lesezeit · Veröffentlicht am: 24. Nov. 2025 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog