Seed Files

On this page

A seed file describes the initial model and optional sample data for an EmDash site. Current templates store it at seed/seed.json and point to it with package.json#emdash.seed.

EmDash embeds the seed at build time. It is intended for first setup and explicit seed commands, not as a migration that runs on every deployment.

File discovery

The Astro integration searches for a seed in this order:

  1. .emdash/seed.json.
  2. The path in package.json#emdash.seed.
  3. seed/seed.json.
  4. The built-in default seed when no user seed exists.

The following package field selects the conventional template path:

{
  "emdash": {
    "seed": "seed/seed.json"
  }
}

Root shape

The following example contains every root property:

{
  "$schema": "https://emdashcms.com/seed.schema.json",
  "version": "1",
  "defaultLocale": "en",
  "meta": {
    "name": "Publication",
    "description": "A publication seed",
    "author": "Example Studio"
  },
  "settings": {},
  "collections": [],
  "taxonomies": [],
  "bylines": [],
  "content": {},
  "menus": [],
  "redirects": [],
  "widgetAreas": [],
  "sections": []
}
PropertyRequiredPurpose
$schemaNoEditor schema URL
versionYesSeed format; the only accepted value is "1"
defaultLocaleNoLocale for locale-bearing rows that omit locale; defaults to runtime configuration, then en
metaNoDescriptive name, description, and author shown during setup
settingsNoPartial site settings
collectionsNoCollection and field definitions
taxonomiesNoTaxonomy definitions and optional terms
bylinesNoOptional presentation-credit profiles
contentNoSample entries grouped by collection slug
menusNoMenus and nested items
redirectsNoLocal redirect rules
widgetAreasNoWidget areas and widgets
sectionsNoReusable Portable Text sections

defaultLocale must be a non-empty string without leading or trailing whitespace.

Settings

settings is a partial site-settings object. Common properties are title, tagline, logo, favicon, url, postsPerPage, dateFormat, timezone, social, and seo.

The setup wizard lets the administrator replace the seeded title and tagline. Applying a seed programmatically writes every supplied setting regardless of onConflict.

{
  "version": "1",
  "settings": {
    "title": "Field Notes",
    "tagline": "Reports from the team",
    "postsPerPage": 12,
    "dateFormat": "MMMM d, yyyy",
    "timezone": "Europe/London"
  }
}

Collections

A collection requires slug, label, and fields:

{
  "version": "1",
  "collections": [
    {
      "slug": "posts",
      "label": "Posts",
      "labelSingular": "Post",
      "description": "Published articles",
      "supports": ["drafts", "revisions", "scheduling", "search", "seo"],
      "urlPattern": "/posts/{slug}",
      "routable": true,
      "commentsEnabled": true,
      "editLocking": true,
      "titleField": "title",
      "dateField": "event_date",
      "admin": {
        "listColumns": ["event_date"]
      },
      "fields": [
        { "slug": "title", "label": "Title", "type": "string", "required": true },
        { "slug": "event_date", "label": "Event date", "type": "datetime", "indexed": true },
        { "slug": "content", "label": "Content", "type": "portableText" }
      ]
    }
  ]
}

Collection properties

PropertyTypeBehavior
slugstringRequired database and API name; starts with a lowercase letter and contains lowercase letters, digits, and underscores
labelstringRequired plural UI label
labelSingularstringOptional singular UI label
descriptionstringOptional admin description
iconstringOptional icon name
admin.listColumnsstring[]Up to four declared field slugs shown in the content list
supportsstring[]Any of drafts, revisions, preview, scheduling, search, and seo
urlPatternstringPublic pattern such as /posts/{slug}
routablebooleanWhether published entries require a slug; defaults to true
hiddenbooleanHides the generated sidebar link and dashboard quick action; the collection stays reachable by URL and API
sortOrdernumberExplicit admin-sidebar position; ordered collections come first, ascending
groupstringAdmin-sidebar folder; collections with the same group share one collapsible entry
commentsEnabledbooleanEnables comments for the collection
editLockingbooleanEnables edit locks; defaults to true
titleFieldstringField used for the content-list title
dateFieldstringdatetime field used for the content-list date
fieldsSeedField[]Required field definitions

sortOrder belongs to the collection and controls sidebar order. SeedField has no sortOrder property. Fields are created in their array order.

Field properties

PropertyTypePurpose
slugstringRequired field name using the collection-slug pattern
labelstringRequired UI label
typeFieldTypeRequired stored field type
requiredbooleanRejects an empty required value
uniquebooleanAdds a uniqueness constraint
searchablebooleanIncludes the field in collection search
indexedbooleanAdds a query index for supported scalar types
defaultValueanyInitial value when the field is omitted
validationobjectValidation rules used by generated content schemas
widgetstringAdmin field widget override
optionsobjectWidget-specific options

Supported field types are:

  • string, text, url, and slug.
  • number, integer, and boolean.
  • datetime.
  • select and multiSelect.
  • portableText, json, and repeater.
  • image, file, and reference.

Only string, url, number, integer, boolean, datetime, select, reference, and slug can set indexed: true.

Field validation

The generated collection schema recognizes these rules where the field type supports them:

RuleUsed by
min, maxNumeric fields
minLength, maxLength, patternString-shaped fields
optionsselect and multiSelect
subFields, minItems, maxItemsrepeater
allowedMimeTypesMedia fields

validateSeed() does not deeply type-check every rule in validation or options. An invalid rule can therefore pass seed validation and fail later when the collection schema is built or content is written.

Taxonomies

Taxonomy definitions identify their target collections. Terms are sample data and are applied only when includeContent is true.

{
  "version": "1",
  "taxonomies": [
    {
      "name": "category",
      "label": "Categories",
      "labelSingular": "Category",
      "hierarchical": true,
      "collections": ["posts"],
      "terms": [
        { "slug": "engineering", "label": "Engineering" },
        { "slug": "platform", "label": "Platform", "parent": "engineering" }
      ]
    }
  ]
}

A taxonomy can carry a seed-local id, locale, and translationOf. Terms can also carry those properties. translationOf refers to another seed-local ID and must be ordered after its source when applying the seed.

Term parent is the parent term’s slug in the same locale. A parent on a non-hierarchical taxonomy produces a warning and is ignored.

Bylines

Root bylines define presentation credits. They are sample data and require includeContent: true.

{
  "version": "1",
  "bylines": [
    {
      "id": "byline-editor",
      "slug": "alex-editor",
      "displayName": "Alex Editor",
      "isGuest": true
    }
  ]
}

The id is seed-local and is used by content credits. Optional properties are bio, websiteUrl, isGuest, and avatar.

A byline avatar points to a file that already exists in configured storage:

{
  "id": "byline-editor",
  "slug": "alex-editor",
  "displayName": "Alex Editor",
  "avatar": {
    "storageKey": "avatars/alex.jpg",
    "filename": "alex.jpg",
    "mimeType": "image/jpeg",
    "alt": "Alex Editor",
    "width": 400,
    "height": 400
  }
}

Byline avatar seeding creates or reuses a media row for the storage key. It does not upload or download the file.

Content

content groups entries by collection slug. Each entry requires a seed-local id and a data object. Routable collections also require a non-empty slug.

{
  "version": "1",
  "content": {
    "posts": [
      {
        "id": "post-welcome",
        "slug": "welcome",
        "status": "published",
        "data": {
          "title": "Welcome",
          "content": []
        },
        "taxonomies": {
          "category": ["engineering"]
        },
        "bylines": [
          { "byline": "byline-editor", "roleLabel": "Editor" }
        ]
      }
    ]
  }
}
PropertyRequiredBehavior
idYesSeed-local reference ID
slugFor routable collectionsPublic slug and conflict key
statusNopublished or draft; defaults to published
dataYesValues keyed by collection field slug
taxonomiesNoTaxonomy name to term-slug array
bylinesNoOrdered credits referencing root byline IDs
localeNoBCP 47 locale; defaults through defaultLocale
translationOfNoSeed-local content ID in the same collection

For a routable entry, the seed-local id is not its database identity. EmDash creates a database ID and records the mapping for later references. For a slugless entry in a collection with routable: false, EmDash uses the seed id as the stored ID so reapplication remains idempotent.

On reads, entry.id is the Astro route identifier and is normally the slug. The stored database ID is entry.data.id.

Content references

Use a $ref: string inside data to replace a seed-local content ID with the created database ID:

{
  "id": "event-opening",
  "slug": "opening-night",
  "data": {
    "title": "Opening night",
    "venue": "$ref:venue-main-hall"
  }
}

Reference targets must appear early enough to be present in the apply engine’s ID map. An unresolved $ref: value remains as the original literal string; validateSeed() does not reject it.

Media references

Use $media in content data to download a URL, upload it with the supplied storage adapter, create a media row, and replace the object with a media field value:

{
  "featured_image": {
    "$media": {
      "url": "https://example.com/images/launch.jpg",
      "filename": "launch.jpg",
      "alt": "A product launch on stage",
      "caption": "Launch event"
    }
  }
}

Within one apply call, repeated references to the same URL reuse the resolved media value. Seed media references do not accept a local file property. mediaBasePath remains in the public SeedApplyOptions type but the current apply engine does not read it.

When no storage adapter is supplied, $media references are skipped and resolve to null. With skipMediaDownload: true, they become external media values and no storage adapter is required.

Menus are structural data and are applied even when includeContent is false:

{
  "version": "1",
  "menus": [
    {
      "name": "primary",
      "label": "Primary navigation",
      "items": [
        {
          "type": "page",
          "label": "About",
          "ref": "page-about",
          "collection": "pages"
        },
        {
          "type": "custom",
          "label": "Contact",
          "url": "/contact",
          "target": "_self"
        }
      ]
    }
  ]
}

Allowed item types are custom, page, post, taxonomy, and collection. custom requires url; page and post require ref. Items can include id, translationOf, label, collection, titleAttr, cssClasses, locale, target, and nested children.

For page and post, ref names a seed content ID. A missing target produces a validation warning and a menu item without a resolved content reference. Existing menu items are deleted and recreated whenever that menu is applied, independent of onConflict.

Redirects

Redirects require local source and destination paths:

{
  "version": "1",
  "redirects": [
    {
      "source": "/old-path",
      "destination": "/new-path",
      "type": 308,
      "enabled": true,
      "groupName": "WordPress migration"
    }
  ]
}

Both paths must start with one /. Protocol-relative URLs, path traversal segments, and newlines are rejected. Allowed status codes are 301, 302, 307, and 308.

Widget areas

A widget area contains content, menu, or component widgets:

{
  "version": "1",
  "widgetAreas": [
    {
      "name": "sidebar",
      "label": "Sidebar",
      "widgets": [
        {
          "type": "menu",
          "title": "Explore",
          "menuName": "primary"
        },
        {
          "type": "component",
          "title": "Recent posts",
          "componentId": "core:recent-posts",
          "props": { "count": 5 }
        }
      ]
    }
  ]
}

A content widget stores Portable Text in content. A menu widget requires menuName. A component widget requires componentId and can pass props. There is no settings property on SeedWidget.

Existing widgets in an area are deleted and recreated whenever the area is applied, independent of onConflict.

Sections

Sections contain reusable Portable Text content:

{
  "version": "1",
  "sections": [
    {
      "slug": "newsletter-signup",
      "title": "Newsletter signup",
      "description": "Signup call to action",
      "keywords": ["newsletter", "email"],
      "source": "theme",
      "content": []
    }
  ]
}

Section slugs contain lowercase letters, digits, and hyphens. source is theme or import; a seed defaults it to theme. Sections are structural and are applied even when includeContent is false.

Localization

defaultLocale fills missing locales for taxonomies, terms, menus, menu items, and content. The active runtime i18n configuration takes precedence when present.

Localized taxonomies, terms, menus, menu items, and content use seed-local id and translationOf fields. Place the source item before a translation so the apply engine can resolve its translation group. A translated content entry must set locale, and its translationOf must name another entry in the same collection.

Apply a seed programmatically

applySeed() and validateSeed() are exported from emdash/seed. The following helper validates before applying:

import {
  applySeed,
  validateSeed,
  type SeedApplyOptions,
  type SeedFile,
} from "emdash/seed";

type SeedDatabase = Parameters<typeof applySeed>[0];

export async function applyProjectSeed(
  db: SeedDatabase,
  seed: SeedFile,
  options: SeedApplyOptions,
) {
  const validation = validateSeed(seed);
  if (!validation.valid) {
    throw new Error(validation.errors.join("\n"));
  }

  return applySeed(db, seed, options);
}

SeedApplyOptions

OptionDefaultCurrent behavior
includeContentfalseIncludes content entries, bylines, and taxonomy terms
onConflict"skip""skip", "update", or "error" for supported entity conflicts
storagenoneStorage adapter required to download $media URLs
skipMediaDownloadfalseKeeps $media URLs as external media values
mediaBasePathnonePresent in the public type but not used by the current apply engine

Programmatic application defaults includeContent to false. The setup wizard passes the administrator’s sample-content choice. The emdash seed CLI includes content by default unless --no-content is set.

Conflict behavior

onConflict is not a transaction policy for the entire seed:

  • Collections, fields, bylines, content, redirects, and sections support skip, update, and error behavior.
  • Taxonomy definitions and terms honor the applicable conflict mode.
  • Settings are always applied.
  • Existing menus retain their menu row but replace all items.
  • Existing widget areas retain their area row but replace all widgets.
  • A content conflict is matched by collection, slug, and locale. A slugless entry in a non-routable collection is matched by its seed ID.

With onConflict: "update", content data is replaced and its byline and taxonomy assignments are reconciled to the seed. Test update mode on a copy before using it against an existing site.

applySeed() returns counters for collections, fields, taxonomies, bylines, menus, redirects, widget areas, sections, settings, content, and media.

Validation behavior

validateSeed() returns { valid, errors, warnings }. applySeed() calls it and throws Invalid seed file when errors are present.

The validator checks the structural rules required by the apply engine, including:

  • Version and non-empty defaultLocale.
  • Collection, field, taxonomy, term, menu, widget-area, section, byline, and content container shapes.
  • Required names, labels, IDs, slugs, and supported field or widget types.
  • Duplicate identifiers in their relevant scope.
  • Indexed field types and admin.listColumns references.
  • Taxonomy parents, content translations, content byline references, and menu item requirements.
  • Safe local redirect paths and status codes.

Some conditions are warnings rather than errors. Examples include a taxonomy with no collections, a parent on a flat taxonomy, or a menu content reference that is absent from the seed.

The validator does not prove that all data values conform to their collection fields. It also does not deeply validate site settings, field validation, field options, arbitrary Portable Text blocks, component widget props, $ref: targets in content data, or remote $media availability. A valid seed can still fail during schema creation, content validation, network download, or storage upload.

Use the $schema URL for editor assistance and run the executable validator before applying:

npx emdash seed seed/seed.json --validate

CLI commands

Apply a seed to a local SQLite database with explicit conflict behavior:

npx emdash seed seed/seed.json --database ./data.db --on-conflict skip

Export the current local model and all content back to the template path:

npx emdash export-seed --database ./data.db --with-content=all > seed/seed.json

export-seed works directly on a local SQLite file. For a deployed D1 database, export it to a local file first. Review exported settings, content, and media references before committing the result.

Next steps