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
{
"$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:
{
"meta": {
"name": "Blog Starter",
"description": "A simple blog with posts, pages, and categories",
"author": "EmDash"
}
}
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
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
| 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:
{
"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:
{
"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.
{
"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:
{
"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 |
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
| 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:
{
"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:
{
"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:
{
"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:
{
"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
| 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
Use the seed API for CLI tools or scripts:
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 |
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
# 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 — Build a complete theme
- Themes Overview — How themes work