Seed File Format

On this page

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": {}
}
FieldTypeRequiredDescription
$schemastringNoJSON schema URL for editor validation
version"1"YesSeed format version
metaobjectNoMetadata about the seed
settingsobjectNoSite settings
collectionsarrayNoCollection definitions
taxonomiesarrayNoTaxonomy definitions
bylinesarrayNoByline profile definitions
menusarrayNoNavigation menus
redirectsarrayNoRedirect rules
widgetAreasarrayNoWidget area definitions
sectionsarrayNoReusable content blocks
contentobjectNoSample 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

PropertyTypeRequiredDescription
slugstringYesURL-safe identifier (lowercase, underscores)
labelstringYesPlural display name
labelSingularstringNoSingular display name
descriptionstringNoAdmin UI description
iconstringNoLucide icon name
supportsarrayNoFeatures: "drafts", "revisions"
fieldsarrayYesField definitions

Field Properties

PropertyTypeRequiredDescription
slugstringYesColumn name (lowercase, underscores)
labelstringYesDisplay name
typestringYesField type
requiredbooleanNoValidation: field must have a value
uniquebooleanNoValidation: value must be unique
defaultValueanyNoDefault value for new entries
validationobjectNoAdditional validation rules
widgetstringNoAdmin UI widget override
optionsobjectNoWidget-specific configuration

Field Types

TypeDescriptionStored As
stringShort textTEXT
textLong text (textarea)TEXT
numberNumeric valueREAL
integerWhole numberINTEGER
booleanTrue/falseINTEGER
dateDate valueTEXT (ISO 8601)
datetimeDate and timeTEXT (ISO 8601)
emailEmail addressTEXT
urlURLTEXT
slugURL-safe stringTEXT
portableTextRich text contentJSON
imageImage referenceJSON
fileFile referenceJSON
jsonArbitrary JSONJSON
referenceReference to another entryTEXT

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

PropertyTypeRequiredDescription
namestringYesUnique identifier
labelstringYesPlural display name
labelSingularstringNoSingular display name
hierarchicalbooleanYesAllow nested terms (categories) or flat (tags)
collectionsarrayYesCollections this taxonomy applies to
termsarrayNoPre-defined terms

Term Properties

PropertyTypeRequiredDescription
slugstringYesURL-safe identifier
labelstringYesDisplay name
descriptionstringNoTerm description
parentstringNoParent 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"
				}
			]
		}
	]
}
TypeDescriptionRequired Fields
customCustom URLurl
pageLink to a page entryref
postLink to a post entryref
taxonomyLink to a taxonomy archiveref, collection
collectionLink to a collection archivecollection
PropertyTypeDescription
typestringItem type (see above)
labelstringDisplay text (auto-generated for page/post refs)
urlstringCustom URL (for custom type)
refstringContent ID in seed (for page/post types)
collectionstringCollection slug
targetstring"_blank" for new window
titleAttrstringHTML title attribute
cssClassesstringCustom CSS classes
childrenarrayNested 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
		}
	]
}
PropertyTypeRequiredDescription
idstringYesSeed-local ID used by content[].bylines
slugstringYesURL-safe byline slug
displayNamestringYesName shown in templates and APIs
biostringNoOptional profile bio
websiteUrlstringNoOptional website URL
isGuestbooleanNoMarks 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

PropertyTypeRequiredDescription
sourcestringYesSource path (must start with /)
destinationstringYesDestination path (must start with /)
typenumberNoHTTP status: 301, 302, 307, or 308
enabledbooleanNoWhether the redirect is active (default: true)
groupNamestringNoOptional 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

TypeDescriptionRequired Fields
contentRich text contentcontent (Portable Text)
menuRenders a menumenuName
componentRegistered componentcomponentId

Built-in Components

Component IDDescription
core:recent-postsList of recent posts
core:categoriesCategory list
core:tagsTag cloud
core:searchSearch form
core:archivesMonthly 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

PropertyTypeRequiredDescription
slugstringYesURL-safe identifier
titlestringYesDisplay name shown in the section picker
descriptionstringNoExplains when to use this section
keywordsarrayNoSearch terms for finding the section
contentarrayYesPortable Text blocks
sourcestringNo"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

PropertyTypeRequiredDescription
idstringYesSeed-local ID for references
slugstringYesURL slug
statusstringNo"published" or "draft" (default: "published")
dataobjectYesField values
bylinesarrayNoOrdered byline credits (byline, optional roleLabel)
taxonomiesobjectNoTerm 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

PropertyTypeRequiredDescription
urlstringYes*Remote URL to download
filestringYes*Local filename in .emdash/media/
altstringNoAlt text for accessibility
filenamestringNoOverride filename
captionstringNoMedia 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

OptionTypeDefaultDescription
includeContentbooleanfalseCreate sample content entries
onConflictstring"skip""skip", "update", or "error"
mediaBasePathstringBase path for local media files
storageStorageStorage adapter for media uploads
baseUrlstringBase URL for media URLs

Idempotency

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

EntityBehavior
CollectionSkip if slug exists
FieldSkip if collection + slug exists
Taxonomy definitionSkip if name exists
Taxonomy termSkip if name + slug exists
Byline profileSkip if slug exists
MenuSkip if name exists
Menu itemsReplace all (menu is recreated)
RedirectSkip if source exists
Widget areaSkip if name exists
WidgetsReplace all (area is recreated)
SectionSkip if slug exists
SettingsUpdate (settings are meant to change)
ContentSkip 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