---
title: Collections & Fields
description: Define content types with collections and fields—supported field types, validation, and relationships.
---

import { Aside, Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
import contentTypesImg from "../../../assets/screenshots/admin-content-types.png";

Collections are the foundation of EmDash's content model. Each collection represents a content type (posts, pages, products) and contains field definitions that determine the shape of your data.

## Creating Collections

Create collections through the admin panel under **Content Types**. Each collection has:

<img src={contentTypesImg.src} alt="EmDash content types showing Pages, Posts, and custom collections with their features" />

| Property        | Description                                          |
| --------------- | ---------------------------------------------------- |
| `slug`          | URL-safe identifier (e.g., `posts`, `products`)      |
| `label`         | Display name (e.g., "Blog Posts")                    |
| `labelSingular` | Singular form (e.g., "Post")                         |
| `description`   | Optional description for editors                     |
| `icon`          | Lucide icon name for the admin sidebar               |
| `supports`      | Features like drafts, revisions, preview, scheduling |

<Aside type="note">
	Some collection slugs are reserved: `content`, `media`, `users`, `revisions`, `taxonomies`,
	`options`, `audit_logs`.
</Aside>

## Collection Features

When creating a collection, enable the features you need:

| Feature      | Description                                    |
| ------------ | ---------------------------------------------- |
| `drafts`     | Enable draft/published workflow                |
| `revisions`  | Track content history with version snapshots   |
| `preview`    | Generate signed preview URLs for draft content |
| `scheduling` | Schedule content to publish at a future date   |

```ts
// Example collection with all features enabled
{
  slug: "posts",
  label: "Blog Posts",
  labelSingular: "Post",
  supports: ["drafts", "revisions", "preview", "scheduling"]
}
```

## Field Types

EmDash supports 15 field types that map to SQLite column types:

### Text Fields

<Tabs>
  <TabItem label="string">
    Short text input. Maps to `TEXT` column.
    
    ```ts
    { slug: "title", type: "string", label: "Title" }
    ```
  </TabItem>
  <TabItem label="text">
    Multi-line textarea. Maps to `TEXT` column.
    
    ```ts
    { slug: "excerpt", type: "text", label: "Excerpt" }
    ```
  </TabItem>
  <TabItem label="slug">
    URL-safe slug field. Maps to `TEXT` column.
    
    ```ts
    { slug: "handle", type: "slug", label: "URL Handle" }
    ```
  </TabItem>
</Tabs>

### Rich Content

<Tabs>
  <TabItem label="portableText">
    Rich text editor (TipTap/ProseMirror). Stored as JSON.
    
    ```ts
    { slug: "content", type: "portableText", label: "Content" }
    ```
    
    Portable Text is a block-based format that preserves structure without embedding HTML.
  </TabItem>
  <TabItem label="json">
    Arbitrary JSON data. Stored as JSON.
    
    ```ts
    { slug: "metadata", type: "json", label: "Custom Metadata" }
    ```
  </TabItem>
</Tabs>

### Numbers

<Tabs>
  <TabItem label="number">
    Decimal numbers. Maps to `REAL` column.
    
    ```ts
    { slug: "price", type: "number", label: "Price" }
    ```
  </TabItem>
  <TabItem label="integer">
    Whole numbers. Maps to `INTEGER` column.
    
    ```ts
    { slug: "quantity", type: "integer", label: "Stock Quantity" }
    ```
  </TabItem>
</Tabs>

### Booleans & Dates

<Tabs>
  <TabItem label="boolean">
    True/false toggle. Maps to `INTEGER` (0/1).
    
    ```ts
    { slug: "featured", type: "boolean", label: "Featured Post" }
    ```
  </TabItem>
  <TabItem label="datetime">
    Date and time picker. Stored as ISO 8601 string.
    
    ```ts
    { slug: "eventDate", type: "datetime", label: "Event Date" }
    ```
  </TabItem>
</Tabs>

### Selection

<Tabs>
  <TabItem label="select">
    Single option from a list. Maps to `TEXT` column.
    
    ```ts
    {
      slug: "status",
      type: "select",
      label: "Product Status",
      validation: {
        options: ["active", "discontinued", "coming_soon"]
      }
    }
    ```
  </TabItem>
  <TabItem label="multiSelect">
    Multiple options from a list. Stored as JSON array.
    
    ```ts
    {
      slug: "features",
      type: "multiSelect",
      label: "Product Features",
      validation: {
        options: ["wireless", "waterproof", "eco-friendly"]
      }
    }
    ```
  </TabItem>
</Tabs>

### Media & References

<Tabs>
  <TabItem label="image">
    Image picker from media library. Stores media ID as `TEXT`.
    
    ```ts
    { slug: "featuredImage", type: "image", label: "Featured Image" }
    ```
  </TabItem>
  <TabItem label="file">
    File picker from media library. Stores media ID as `TEXT`.
    
    ```ts
    { slug: "attachment", type: "file", label: "PDF Attachment" }
    ```
  </TabItem>
  <TabItem label="reference">
    Reference to another collection's entry. Stores entry ID as `TEXT`.
    
    ```ts
    {
      slug: "author",
      type: "reference",
      label: "Author",
      options: {
        collection: "authors"
      }
    }
    ```
  </TabItem>
</Tabs>

## Field Properties

Every field supports these properties:

| Property       | Type        | Description                                  |
| -------------- | ----------- | -------------------------------------------- |
| `slug`         | `string`    | Column name in the database                  |
| `label`        | `string`    | Display label in admin UI                    |
| `type`         | `FieldType` | One of the 15 field types                    |
| `required`     | `boolean`   | Whether the field must have a value          |
| `unique`       | `boolean`   | Whether values must be unique across entries |
| `defaultValue` | `unknown`   | Default value for new entries                |
| `validation`   | `object`    | Type-specific validation rules               |
| `widget`       | `string`    | Custom widget identifier                     |
| `options`      | `object`    | Widget-specific configuration                |
| `sortOrder`    | `number`    | Display order in the editor                  |

<Aside type="caution">
	Some field slugs are reserved and cannot be used: `id`, `slug`, `status`, `author_id`,
	`created_at`, `updated_at`, `published_at`, `deleted_at`, `version`.
</Aside>

## Validation Rules

The `validation` object varies by field type:

```ts
interface FieldValidation {
	required?: boolean; // All types
	min?: number; // number, integer
	max?: number; // number, integer
	minLength?: number; // string, text
	maxLength?: number; // string, text
	pattern?: string; // string (regex)
	options?: string[]; // select, multiSelect
}
```

Example with validation:

```ts
{
  slug: "email",
  type: "string",
  label: "Email Address",
  required: true,
  unique: true,
  validation: {
    pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
  }
}
```

## Widget Options

The `options` object configures field-specific UI behavior:

```ts
interface FieldWidgetOptions {
	rows?: number; // text (textarea rows)
	showPreview?: boolean; // image, file
	collection?: string; // reference (target collection)
	allowMultiple?: boolean; // reference (multiple refs)
	[key: string]: unknown; // Custom widget options
}
```

Example reference field:

```ts
{
  slug: "relatedProducts",
  type: "reference",
  label: "Related Products",
  options: {
    collection: "products",
    allowMultiple: true
  }
}
```

## Querying Collections

Use the provided query functions to fetch content. These follow Astro's live collections pattern, returning structured results:

```ts
import { getEmDashCollection, getEmDashEntry } from "emdash";

// Get all entries - returns { entries, error }
const { entries: posts } = await getEmDashCollection("posts");

// Filter by status
const { entries: drafts } = await getEmDashCollection("posts", {
	status: "draft",
});

// Limit results
const { entries: recent } = await getEmDashCollection("posts", {
	limit: 5,
});

// Filter by taxonomy
const { entries: newsPosts } = await getEmDashCollection("posts", {
	where: { category: "news" },
});

// Get a single entry by slug - returns { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post-slug");

// Handle errors
const { entries, error } = await getEmDashCollection("posts");
if (error) {
	console.error("Failed to load posts:", error);
}
```

## Type Generation

Run `npx emdash types` to generate TypeScript types from your schema:

```ts
// .emdash/types.ts (generated)
export interface Post {
	title: string;
	content: PortableTextBlock[];
	excerpt?: string;
	featuredImage?: string;
	author: string; // reference ID
}

export interface Product {
	title: string;
	price: number;
	description: PortableTextBlock[];
}
```

<Aside type="tip">
	Re-run `emdash types` after modifying collections in the admin panel to keep types in sync.
</Aside>

## Database Mapping

Field types map to SQLite column types:

| Field Type     | SQLite Type | Notes                 |
| -------------- | ----------- | --------------------- |
| `string`       | `TEXT`      |                       |
| `text`         | `TEXT`      |                       |
| `slug`         | `TEXT`      |                       |
| `number`       | `REAL`      | 64-bit floating point |
| `integer`      | `INTEGER`   | 64-bit signed integer |
| `boolean`      | `INTEGER`   | 0 or 1                |
| `datetime`     | `TEXT`      | ISO 8601 format       |
| `select`       | `TEXT`      |                       |
| `multiSelect`  | `JSON`      | Array of strings      |
| `portableText` | `JSON`      | Block array           |
| `image`        | `TEXT`      | Media ID              |
| `file`         | `TEXT`      | Media ID              |
| `reference`    | `TEXT`      | Entry ID              |
| `json`         | `JSON`      | Arbitrary JSON        |

## Next Steps

<CardGrid>
	<Card title="Content Model" icon="open-book">
		Understand the [database-first approach](/concepts/content-model/).
	</Card>
	<Card title="Taxonomies" icon="list-format">
		Organize content with [categories and tags](/guides/taxonomies/).
	</Card>
	<Card title="Media Library" icon="seti:image">
		Manage [images and files](/guides/media/).
	</Card>
</CardGrid>
