---
title: Seed File Format
description: Reference for EmDash seed file structure and syntax.
---

import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";

Seed files are JSON documents that bootstrap EmDash sites. They define collections, fields, taxonomies, menus, redirects, widget areas, site settings, and optional sample content.

## Root Structure

```json
{
	"$schema": "https://emdashcms.com/seed.schema.json",
	"version": "1",
	"meta": {},
	"settings": {},
	"collections": [],
	"taxonomies": [],
	"bylines": [],
	"menus": [],
	"redirects": [],
	"widgetAreas": [],
	"sections": [],
	"content": {}
}
```

| Field         | Type     | Required | Description                           |
| ------------- | -------- | -------- | ------------------------------------- |
| `$schema`     | `string` | No       | JSON schema URL for editor validation |
| `version`     | `"1"`    | Yes      | Seed format version                   |
| `meta`        | `object` | No       | Metadata about the seed               |
| `settings`    | `object` | No       | Site settings                         |
| `collections` | `array`  | No       | Collection definitions                |
| `taxonomies`  | `array`  | No       | Taxonomy definitions                  |
| `bylines`     | `array`  | No       | Byline profile definitions            |
| `menus`       | `array`  | No       | Navigation menus                      |
| `redirects`   | `array`  | No       | Redirect rules                        |
| `widgetAreas` | `array`  | No       | Widget area definitions               |
| `sections`    | `array`  | No       | Reusable content blocks               |
| `content`     | `object` | No       | Sample content entries                |

## Meta

Optional metadata about the seed:

```json
{
	"meta": {
		"name": "Blog Starter",
		"description": "A simple blog with posts, pages, and categories",
		"author": "EmDash"
	}
}
```

## Settings

Site-wide configuration values:

```json
{
	"settings": {
		"title": "My Site",
		"tagline": "A modern CMS",
		"postsPerPage": 10,
		"dateFormat": "MMMM d, yyyy"
	}
}
```

Settings are applied to the `options` table with the `site:` prefix. The Setup Wizard lets users override `title` and `tagline`.

## Collections

Collection definitions create content types in the database:

```json
{
	"collections": [
		{
			"slug": "posts",
			"label": "Posts",
			"labelSingular": "Post",
			"description": "Blog posts",
			"icon": "file-text",
			"supports": ["drafts", "revisions"],
			"fields": [
				{
					"slug": "title",
					"label": "Title",
					"type": "string",
					"required": true
				},
				{
					"slug": "content",
					"label": "Content",
					"type": "portableText"
				},
				{
					"slug": "featured_image",
					"label": "Featured Image",
					"type": "image"
				}
			]
		}
	]
}
```

### Collection Properties

| Property        | Type     | Required | Description                                  |
| --------------- | -------- | -------- | -------------------------------------------- |
| `slug`          | `string` | Yes      | URL-safe identifier (lowercase, underscores) |
| `label`         | `string` | Yes      | Plural display name                          |
| `labelSingular` | `string` | No       | Singular display name                        |
| `description`   | `string` | No       | Admin UI description                         |
| `icon`          | `string` | No       | Lucide icon name                             |
| `supports`      | `array`  | No       | Features: `"drafts"`, `"revisions"`          |
| `fields`        | `array`  | Yes      | Field definitions                            |

### Field Properties

| Property       | Type      | Required | Description                          |
| -------------- | --------- | -------- | ------------------------------------ |
| `slug`         | `string`  | Yes      | Column name (lowercase, underscores) |
| `label`        | `string`  | Yes      | Display name                         |
| `type`         | `string`  | Yes      | Field type                           |
| `required`     | `boolean` | No       | Validation: field must have a value  |
| `unique`       | `boolean` | No       | Validation: value must be unique     |
| `defaultValue` | `any`     | No       | Default value for new entries        |
| `validation`   | `object`  | No       | Additional validation rules          |
| `widget`       | `string`  | No       | Admin UI widget override             |
| `options`      | `object`  | No       | Widget-specific configuration        |

### Field Types

| Type           | Description                | Stored As         |
| -------------- | -------------------------- | ----------------- |
| `string`       | Short text                 | `TEXT`            |
| `text`         | Long text (textarea)       | `TEXT`            |
| `number`       | Numeric value              | `REAL`            |
| `integer`      | Whole number               | `INTEGER`         |
| `boolean`      | True/false                 | `INTEGER`         |
| `date`         | Date value                 | `TEXT` (ISO 8601) |
| `datetime`     | Date and time              | `TEXT` (ISO 8601) |
| `email`        | Email address              | `TEXT`            |
| `url`          | URL                        | `TEXT`            |
| `slug`         | URL-safe string            | `TEXT`            |
| `portableText` | Rich text content          | `JSON`            |
| `image`        | Image reference            | `JSON`            |
| `file`         | File reference             | `JSON`            |
| `json`         | Arbitrary JSON             | `JSON`            |
| `reference`    | Reference to another entry | `TEXT`            |

## Taxonomies

Classification systems for content:

```json
{
	"taxonomies": [
		{
			"name": "category",
			"label": "Categories",
			"labelSingular": "Category",
			"hierarchical": true,
			"collections": ["posts"],
			"terms": [
				{ "slug": "news", "label": "News" },
				{ "slug": "tutorials", "label": "Tutorials" },
				{
					"slug": "advanced",
					"label": "Advanced Tutorials",
					"parent": "tutorials"
				}
			]
		},
		{
			"name": "tag",
			"label": "Tags",
			"labelSingular": "Tag",
			"hierarchical": false,
			"collections": ["posts"]
		}
	]
}
```

### Taxonomy Properties

| Property        | Type      | Required | Description                                    |
| --------------- | --------- | -------- | ---------------------------------------------- |
| `name`          | `string`  | Yes      | Unique identifier                              |
| `label`         | `string`  | Yes      | Plural display name                            |
| `labelSingular` | `string`  | No       | Singular display name                          |
| `hierarchical`  | `boolean` | Yes      | Allow nested terms (categories) or flat (tags) |
| `collections`   | `array`   | Yes      | Collections this taxonomy applies to           |
| `terms`         | `array`   | No       | Pre-defined terms                              |

### Term Properties

| Property      | Type     | Required | Description                          |
| ------------- | -------- | -------- | ------------------------------------ |
| `slug`        | `string` | Yes      | URL-safe identifier                  |
| `label`       | `string` | Yes      | Display name                         |
| `description` | `string` | No       | Term description                     |
| `parent`      | `string` | No       | Parent term slug (hierarchical only) |

## Menus

Navigation menus editable from the admin:

```json
{
	"menus": [
		{
			"name": "primary",
			"label": "Primary Navigation",
			"items": [
				{ "type": "custom", "label": "Home", "url": "/" },
				{ "type": "page", "ref": "about" },
				{ "type": "custom", "label": "Blog", "url": "/posts" },
				{
					"type": "custom",
					"label": "External",
					"url": "https://example.com",
					"target": "_blank"
				}
			]
		}
	]
}
```

### Menu Item Types

| Type         | Description                  | Required Fields     |
| ------------ | ---------------------------- | ------------------- |
| `custom`     | Custom URL                   | `url`               |
| `page`       | Link to a page entry         | `ref`               |
| `post`       | Link to a post entry         | `ref`               |
| `taxonomy`   | Link to a taxonomy archive   | `ref`, `collection` |
| `collection` | Link to a collection archive | `collection`        |

### Menu Item Properties

| Property     | Type     | Description                                      |
| ------------ | -------- | ------------------------------------------------ |
| `type`       | `string` | Item type (see above)                            |
| `label`      | `string` | Display text (auto-generated for page/post refs) |
| `url`        | `string` | Custom URL (for `custom` type)                   |
| `ref`        | `string` | Content ID in seed (for `page`/`post` types)     |
| `collection` | `string` | Collection slug                                  |
| `target`     | `string` | `"_blank"` for new window                        |
| `titleAttr`  | `string` | HTML title attribute                             |
| `cssClasses` | `string` | Custom CSS classes                               |
| `children`   | `array`  | Nested menu items                                |

## Bylines

Byline profiles are separate from ownership (`author_id`). Define reusable byline identities once, then reference them from content entries.

```json
{
	"bylines": [
		{
			"id": "editorial",
			"slug": "emdash-editorial",
			"displayName": "EmDash Editorial"
		},
		{
			"id": "guest",
			"slug": "guest-contributor",
			"displayName": "Guest Contributor",
			"isGuest": true
		}
	]
}
```

| Property     | Type      | Required | Description                                |
| ------------ | --------- | -------- | ------------------------------------------ |
| `id`         | `string`  | Yes      | Seed-local ID used by `content[].bylines`  |
| `slug`       | `string`  | Yes      | URL-safe byline slug                       |
| `displayName`| `string`  | Yes      | Name shown in templates and APIs           |
| `bio`        | `string`  | No       | Optional profile bio                       |
| `websiteUrl` | `string`  | No       | Optional website URL                       |
| `isGuest`    | `boolean` | No       | Marks byline as guest profile              |

## Redirects

Redirect rules to preserve legacy URLs after migration:

```json
{
	"redirects": [
		{ "source": "/old-about", "destination": "/about" },
		{ "source": "/legacy-feed", "destination": "/rss.xml", "type": 308 },
		{
			"source": "/category/news",
			"destination": "/categories/news",
			"groupName": "migration"
		}
	]
}
```

### Redirect Properties

| Property      | Type      | Required | Description                                        |
| ------------- | --------- | -------- | -------------------------------------------------- |
| `source`      | `string`  | Yes      | Source path (must start with `/`)                  |
| `destination` | `string`  | Yes      | Destination path (must start with `/`)             |
| `type`        | `number`  | No       | HTTP status: `301`, `302`, `307`, or `308`         |
| `enabled`     | `boolean` | No       | Whether the redirect is active (default: `true`)   |
| `groupName`   | `string`  | No       | Optional grouping label for admin filtering/search |

<Aside type="caution">
	`source` and `destination` must be local paths. External URLs, protocol-relative paths (`//...`),
	path traversal segments (`..`), and newline characters are rejected by seed validation.
</Aside>

## Widget Areas

Configurable content regions:

```json
{
	"widgetAreas": [
		{
			"name": "sidebar",
			"label": "Main Sidebar",
			"description": "Appears on blog posts and pages",
			"widgets": [
				{
					"type": "component",
					"title": "Recent Posts",
					"componentId": "core:recent-posts",
					"props": { "count": 5 }
				},
				{
					"type": "menu",
					"title": "Quick Links",
					"menuName": "footer"
				},
				{
					"type": "content",
					"title": "About",
					"content": [
						{
							"_type": "block",
							"style": "normal",
							"children": [{ "_type": "span", "text": "Welcome to our site!" }]
						}
					]
				}
			]
		}
	]
}
```

### Widget Types

| Type        | Description          | Required Fields           |
| ----------- | -------------------- | ------------------------- |
| `content`   | Rich text content    | `content` (Portable Text) |
| `menu`      | Renders a menu       | `menuName`                |
| `component` | Registered component | `componentId`             |

### Built-in Components

| Component ID        | Description          |
| ------------------- | -------------------- |
| `core:recent-posts` | List of recent posts |
| `core:categories`   | Category list        |
| `core:tags`         | Tag cloud            |
| `core:search`       | Search form          |
| `core:archives`     | Monthly archives     |

## Sections

Reusable content blocks that editors can insert into Portable Text fields via the `/section` slash command:

```json
{
	"sections": [
		{
			"slug": "hero-centered",
			"title": "Centered Hero",
			"description": "Full-width hero with centered heading and CTA button",
			"keywords": ["hero", "banner", "header", "landing"],
			"content": [
				{
					"_type": "block",
					"style": "h1",
					"children": [{ "_type": "span", "text": "Welcome to Our Site" }]
				},
				{
					"_type": "block",
					"children": [
						{ "_type": "span", "text": "Your compelling tagline goes here." }
					]
				}
			]
		}
	]
}
```

### Section Properties

| Property      | Type     | Required | Description                                     |
| ------------- | -------- | -------- | ----------------------------------------------- |
| `slug`        | `string` | Yes      | URL-safe identifier                             |
| `title`       | `string` | Yes      | Display name shown in the section picker        |
| `description` | `string` | No       | Explains when to use this section               |
| `keywords`    | `array`  | No       | Search terms for finding the section            |
| `content`     | `array`  | Yes      | Portable Text blocks                            |
| `source`      | `string` | No       | `"theme"` (default for seeds) or `"import"`     |

Sections from seed files are marked `source: "theme"` and cannot be deleted from the admin UI. Editors can create their own sections (`source: "user"`) and insert any section type when editing content.

## Content

Sample content organized by collection:

```json
{
	"content": {
		"posts": [
			{
				"id": "hello-world",
				"slug": "hello-world",
				"status": "published",
				"bylines": [
					{ "byline": "editorial" },
					{ "byline": "guest", "roleLabel": "Guest essay" }
				],
				"data": {
					"title": "Hello World",
					"content": [
						{
							"_type": "block",
							"style": "normal",
							"children": [{ "_type": "span", "text": "Welcome!" }]
						}
					],
					"excerpt": "Your first post."
				},
				"taxonomies": {
					"category": ["news"],
					"tag": ["welcome", "first-post"]
				}
			}
		],
		"pages": [
			{
				"id": "about",
				"slug": "about",
				"status": "published",
				"data": {
					"title": "About Us",
					"content": [
						{
							"_type": "block",
							"style": "normal",
							"children": [{ "_type": "span", "text": "About page content." }]
						}
					]
				}
			}
		]
	}
}
```

### Content Entry Properties

| Property     | Type     | Required | Description                                         |
| ------------ | -------- | -------- | --------------------------------------------------- |
| `id`         | `string` | Yes      | Seed-local ID for references                        |
| `slug`       | `string` | Yes      | URL slug                                            |
| `status`     | `string` | No       | `"published"` or `"draft"` (default: `"published"`) |
| `data`       | `object` | Yes      | Field values                                        |
| `bylines`    | `array`  | No       | Ordered byline credits (`byline`, optional `roleLabel`) |
| `taxonomies` | `object` | No       | Term assignments by taxonomy name                   |

## Content References

Reference other content entries using the `$ref:` prefix:

```json
{
	"data": {
		"related_posts": ["$ref:another-post", "$ref:third-post"]
	}
}
```

The `$ref:` prefix resolves seed IDs to database IDs during seeding.

## Media References

Include images from URLs:

```json
{
	"data": {
		"featured_image": {
			"$media": {
				"url": "https://images.unsplash.com/photo-xxx",
				"alt": "Description of the image",
				"filename": "hero.jpg",
				"caption": "Photo by Someone"
			}
		}
	}
}
```

Include local images from `.emdash/media/`:

```json
{
	"data": {
		"featured_image": {
			"$media": {
				"file": "hero.jpg",
				"alt": "Description of the image"
			}
		}
	}
}
```

### Media Properties

| Property   | Type     | Required | Description                          |
| ---------- | -------- | -------- | ------------------------------------ |
| `url`      | `string` | Yes\*    | Remote URL to download               |
| `file`     | `string` | Yes\*    | Local filename in `.emdash/media/` |
| `alt`      | `string` | No       | Alt text for accessibility           |
| `filename` | `string` | No       | Override filename                    |
| `caption`  | `string` | No       | Media caption                        |

\*Either `url` or `file` is required, not both.

<Aside>
	Media is downloaded during seeding. Large images may slow down the Setup Wizard. Consider using
	compressed images or thumbnail versions for sample content.
</Aside>

## Applying Seeds Programmatically

Use the seed API for CLI tools or scripts:

```typescript
import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";

// Validate first
const validation = validateSeed(seedData);
if (!validation.valid) {
	console.error(validation.errors);
	process.exit(1);
}

// Apply seed
const result = await applySeed(db, seedData, {
	includeContent: true,
	onConflict: "skip",
	storage: myStorage,
	baseUrl: "http://localhost:4321",
});

console.log(result);
// {
//   collections: { created: 2, skipped: 0 },
//   fields: { created: 8, skipped: 0 },
//   taxonomies: { created: 2, terms: 5 },
//   bylines: { created: 2, skipped: 0 },
//   menus: { created: 1, items: 4 },
//   redirects: { created: 3, skipped: 0 },
//   widgetAreas: { created: 1, widgets: 3 },
//   settings: { applied: 3 },
//   content: { created: 3, skipped: 0 },
//   media: { created: 2, skipped: 0 }
// }
```

### Apply Options

| Option           | Type      | Default  | Description                        |
| ---------------- | --------- | -------- | ---------------------------------- |
| `includeContent` | `boolean` | `false`  | Create sample content entries      |
| `onConflict`     | `string`  | `"skip"` | `"skip"`, `"update"`, or `"error"` |
| `mediaBasePath`  | `string`  | —        | Base path for local media files    |
| `storage`        | `Storage` | —        | Storage adapter for media uploads  |
| `baseUrl`        | `string`  | —        | Base URL for media URLs            |

## Idempotency

Seeding is safe to run multiple times. Conflict behavior by entity type:

| Entity              | Behavior                              |
| ------------------- | ------------------------------------- |
| Collection          | Skip if slug exists                   |
| Field               | Skip if collection + slug exists      |
| Taxonomy definition | Skip if name exists                   |
| Taxonomy term       | Skip if name + slug exists            |
| Byline profile      | Skip if slug exists                   |
| Menu                | Skip if name exists                   |
| Menu items          | Replace all (menu is recreated)       |
| Redirect            | Skip if source exists                 |
| Widget area         | Skip if name exists                   |
| Widgets             | Replace all (area is recreated)       |
| Section             | Skip if slug exists                   |
| Settings            | Update (settings are meant to change) |
| Content             | Skip if slug exists in collection     |

<Aside type="caution">
	Menu items and widgets are **replaced**, not merged. The seed file is the source of truth for menu
	and widget area structure.
</Aside>

## Validation

Seed files are validated before application:

```typescript
import { validateSeed } from "emdash/seed";

const { valid, errors, warnings } = validateSeed(seedData);

if (!valid) {
	errors.forEach((e) => console.error(e));
}

warnings.forEach((w) => console.warn(w));
```

Validation checks:

- Required fields are present
- Slugs follow naming conventions (lowercase, underscores)
- Field types are valid
- References point to existing content
- Hierarchical term parents exist
- Redirect paths are safe local URLs
- Redirect sources are unique
- No duplicate slugs within collections

## CLI Commands

```bash
# Apply seed file
npx emdash seed .emdash/seed.json

# Apply without sample content
npx emdash seed .emdash/seed.json --no-content

# Validate only
npx emdash seed .emdash/seed.json --validate

# Export current schema as seed
npx emdash export-seed > seed.json

# Export with content
npx emdash export-seed --with-content > seed.json
```

## Next Steps

- **[Creating Themes](/themes/creating-themes/)** — Build a complete theme
- **[Themes Overview](/themes/overview/)** — How themes work
