Seed File Format
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
Section titled “Root Structure”{ "$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 |
Optional metadata about the seed:
{ "meta": { "name": "Blog Starter", "description": "A simple blog with posts, pages, and categories", "author": "EmDash" }}Settings
Section titled “Settings”Site-wide configuration values:
{ "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
Section titled “Collections”Collection definitions create content types in the database:
{ "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
Section titled “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
Section titled “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
Section titled “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
Section titled “Taxonomies”Classification systems for content:
{ "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
Section titled “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
Section titled “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) |
Navigation menus editable from the admin:
{ "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
Section titled “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
Section titled “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
Section titled “Bylines”Byline profiles are separate from ownership (author_id). Define reusable byline identities once, then reference them from content entries.
{ "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
Section titled “Redirects”Redirect rules to preserve legacy URLs after migration:
{ "redirects": [ { "source": "/old-about", "destination": "/about" }, { "source": "/legacy-feed", "destination": "/rss.xml", "type": 308 }, { "source": "/category/news", "destination": "/categories/news", "groupName": "migration" } ]}Redirect Properties
Section titled “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 |
Widget Areas
Section titled “Widget Areas”Configurable content regions:
{ "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
Section titled “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
Section titled “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
Section titled “Sections”Reusable content blocks that editors can insert into Portable Text fields via the /section slash command:
{ "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
Section titled “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
Section titled “Content”Sample content organized by collection:
{ "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
Section titled “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
Section titled “Content References”Reference other content entries using the $ref: prefix:
{ "data": { "related_posts": ["$ref:another-post", "$ref:third-post"] }}The $ref: prefix resolves seed IDs to database IDs during seeding.
Media References
Section titled “Media References”Include images from URLs:
{ "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/:
{ "data": { "featured_image": { "$media": { "file": "hero.jpg", "alt": "Description of the image" } } }}Media Properties
Section titled “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.
Applying Seeds Programmatically
Section titled “Applying Seeds Programmatically”Use the seed API for CLI tools or scripts:
import { applySeed, validateSeed } from "emdash/seed";import seedData from "./.emdash/seed.json";
// Validate firstconst validation = validateSeed(seedData);if (!validation.valid) { console.error(validation.errors); process.exit(1);}
// Apply seedconst 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
Section titled “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
Section titled “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 |
Validation
Section titled “Validation”Seed files are validated before application:
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
Section titled “CLI Commands”# Apply seed filenpx emdash seed .emdash/seed.json
# Apply without sample contentnpx emdash seed .emdash/seed.json --no-content
# Validate onlynpx emdash seed .emdash/seed.json --validate
# Export current schema as seednpx emdash export-seed > seed.json
# Export with contentnpx emdash export-seed --with-content > seed.jsonNext Steps
Section titled “Next Steps”- Creating Themes — Build a complete theme
- Themes Overview — How themes work