Markdown source
Content Collections Markdown source
Readable source view for humans. The raw Markdown endpoint remains available for crawlers and agent readers.
---
title: "Content Collections"
description: "Typed, validated markdown — like a DataFrame for your articles."
kind: guide
maturity: evergreen
confidence: high
origin: ai-drafted
author: "Agent"
directedBy: "krow"
tags: [astro, fundamentals]
published: 2026-03-15
modified: 2026-05-31
wordCount: 815
readingTime: 4
series: "learn-astro"
series_order: 6
prerequisites: [astro-layouts]
related: [agentic-coding-context-management]
url: https://krowdev.com/guide/astro-content-collections/
---
## Agent Context
- Canonical: https://krowdev.com/guide/astro-content-collections/
- Markdown: https://krowdev.com/guide/astro-content-collections.md
- Full corpus: https://krowdev.com/llms-full.txt
- Kind: guide
- Maturity: evergreen
- Confidence: high
- Origin: ai-drafted
- Author: Agent
- Directed by: krow
- Published: 2026-03-15
- Modified: 2026-05-31
- Words: 815 (4 min read)
- Tags: astro, fundamentals
- Series: learn-astro (#6)
- Prerequisites: astro-layouts
- Related: agentic-coding-context-management
- Content map:
- h2: The Problem Collections Solve
- h2: Defining a Collection
- h3: Schema ↔ Pydantic Mapping
- h2: What Happens When Validation Fails
- h2: Querying Collections
- h2: Entry Shape
- h2: krowdev's Collection
- h2: Sources
- Crawl policy: same canonical content is exposed through HTML, Markdown, and llms-full; no crawler-specific content gate.
## The Problem Collections Solve
Prereq: [layouts](/guide/astro-layouts/). After this you will want [markdown and code blocks](/guide/astro-markdown-and-code/) which describes how the body renders, and [build and deploy](/guide/astro-build-and-deploy/) to ship it. For the typed-content philosophy see the [mental model](/guide/astro-mental-model/).
Without collections, you'd have loose markdown files with no validation. Typo in frontmatter? Wrong date format? Missing required field? You'd only find out when the page looks broken.
Content Collections give you:
- **Schema validation** — catch errors at build time, not in production
- **Type safety** — autocomplete and type checking on frontmatter fields
- **Querying** — filter, sort, and group articles with typed API
:::analogy
**Without collections:** a folder of CSV files with no schema. Every script that reads them has to handle missing columns, wrong types, inconsistent formats.
**With collections:** a Pandas DataFrame with defined dtypes + Pydantic validation. If a row doesn't match the schema, you get an error *before* analysis runs.
:::
## Defining a Collection
In `src/content.config.ts`:
```ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const kb = defineCollection({
// Where to find the files
loader: glob({ pattern: '**/*.md', base: '../../content/kb' }),
// The schema — validates every file's frontmatter
schema: z.object({
title: z.string(),
description: z.string().optional(),
kind: z.enum(['snippet', 'note', 'guide', 'article', 'showcase']),
tags: z.array(z.string()).optional(),
maturity: z.enum(['seedling', 'budding', 'evergreen']).optional(),
origin: z.enum(['human', 'ai-assisted', 'ai-drafted']).optional(),
confidence: z.enum(['high', 'medium', 'low']).optional(),
}),
});
export const collections = { kb };
```
### Schema ↔ Pydantic Mapping
| Zod (Astro) | Pydantic (Python) | What It Does |
|---|---|---|
| `z.string()` | `str` | Must be a string |
| `z.number()` | `int \| float` | Must be a number |
| `z.boolean()` | `bool` | Must be true/false |
| `z.enum(['a', 'b'])` | `Literal['a', 'b']` | Must be one of these values |
| `z.string().optional()` | `Optional[str] = None` | Can be omitted |
| `z.coerce.date()` | `datetime` with validator | Parses string to Date |
| `z.array(z.string())` | `list[str]` | Array of strings |
| `z.object({...})` | Nested Pydantic model | Nested structure |
## What Happens When Validation Fails
If a markdown file has invalid frontmatter:
```markdown
---
title: 42 # ERROR: expected string, got number
difficulty: "expert" # ERROR: not in enum
---
```
The build **fails** with a clear error:
```
[ERROR] kb → bad-file.md frontmatter does not match schema
title: Expected string, received number
kind: Invalid enum value. Expected 'snippet' | 'note' | 'guide' | 'article' | 'showcase'
```
:::key
This is the same principle as unit tests for your data. Every `npm run build` validates every article against the schema. Bad data never reaches production.
:::
## Querying Collections
Once defined, you query collections with `getCollection()`:
```astro
---
import { getCollection } from 'astro:content';
// Get all entries
const allKB = await getCollection('kb');
// Filter
const guides = await getCollection('kb', ({ data }) => {
return data.kind === 'guide';
});
// Sort
const sorted = allKB.sort((a, b) => {
return (a.data.series_order ?? 99) - (b.data.series_order ?? 99);
});
// Group by kind
const byKind = Object.groupBy(allKB, entry => entry.data.kind);
---
```
:::analogy
```python
# Pandas equivalent
df = pd.read_csv("kb_entries.csv") # getCollection('kb')
guides = df[df.kind == "guide"] # filter by data.kind
sorted = df.sort_values("series_order") # .sort() by data.series_order
grouped = df.groupby("kind") # Object.groupBy by data.kind
```
:::
## Entry Shape
Each entry from `getCollection()` has:
```ts
entry.id // "astro-mental-model" (file name without extension)
entry.data // { title: "...", kind: "guide", ... } (validated frontmatter)
```
To get the rendered HTML:
```ts
import { render } from 'astro:content';
const { Content, headings } = await render(entry);
// Content — an Astro component that outputs the rendered markdown
// headings — array of { depth, slug, text } for ToC generation
```
## krowdev's Collection
```ts
// kb — unified knowledge base
const kb = defineCollection({
loader: glob({ pattern: '**/*.md', base: '../../content/kb' }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
kind: z.enum(['snippet', 'note', 'guide', 'article', 'showcase']),
created: z.coerce.date(),
tags: z.array(z.string()).optional(),
maturity: z.enum(['seedling', 'budding', 'evergreen']).optional(),
origin: z.enum(['human', 'ai-assisted', 'ai-drafted']).optional(),
confidence: z.enum(['high', 'medium', 'low']).optional(),
series: z.string().optional(),
series_order: z.number().optional(),
}),
});
```
The `kind` field determines the URL prefix (`/guide/`, `/article/`, `/snippet/`, etc.) — one collection, multiple content types.
**Challenge: Break the schema on purpose**
1. Create a file `content/kb/test-break.md`
2. Give it intentionally invalid frontmatter:
```yaml
---
title: 42
kind: "expert"
---
Test content.
```
3. Run `npm run build` and read the error
4. Fix the frontmatter and rebuild
5. Delete the test file when done
This teaches you to read Zod validation errors — you'll see them when you make typos in real articles.
---
Previous: [Layouts](/guide/astro-layouts/) | Next: [Styling](/guide/astro-styling/)
## Sources
- Astro Docs, [Content collections](https://docs.astro.build/en/guides/content-collections/)
- Astro Docs, [Content loader API](https://docs.astro.build/en/reference/content-loader-reference/)
- Zod, [Schema definition](https://zod.dev/?id=basic-usage)