Skip to content

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.

{
"$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

Optional metadata about the seed:

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

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.

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"
}
]
}
]
}
PropertyTypeRequiredDescription
slugstringYesURL-safe identifier (lowercase, underscores)
labelstringYesPlural display name
labelSingularstringNoSingular display name
descriptionstringNoAdmin UI description
iconstringNoLucide icon name
supportsarrayNoFeatures: "drafts", "revisions"
fieldsarrayYesField definitions
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
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

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"]
}
]
}
PropertyTypeRequiredDescription
namestringYesUnique identifier
labelstringYesPlural display name
labelSingularstringNoSingular display name
hierarchicalbooleanYesAllow nested terms (categories) or flat (tags)
collectionsarrayYesCollections this taxonomy applies to
termsarrayNoPre-defined terms
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

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

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"
}
]
}
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

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!" }]
}
]
}
]
}
]
}
TypeDescriptionRequired Fields
contentRich text contentcontent (Portable Text)
menuRenders a menumenuName
componentRegistered componentcomponentId
Component IDDescription
core:recent-postsList of recent posts
core:categoriesCategory list
core:tagsTag cloud
core:searchSearch form
core:archivesMonthly archives

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." }
]
}
]
}
]
}
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.

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." }]
}
]
}
}
]
}
}
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

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.

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"
}
}
}
}
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.

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 }
// }
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

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

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
Terminal window
# 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