Custom Content Types in EmDash: A Practical Guide
EmDash's content model is defined in seed.json, not in the admin panel. This is a deliberate design choice — your schema is version-controlled, diffable, and reproducible. Here's how to design and build custom content types for your site.
Collections: your content types
A collection is a content type. Each collection gets its own database table (ec_{slug}), admin interface, and TypeScript types. Dashstro uses four: learn (articles), docs (documentation), changelog (release notes), and pages (standalone pages).
Define a collection with a slug, label, and list of fields. The slug must be lowercase alphanumeric with underscores, max 63 characters. Add supports for drafts, revisions, search, and SEO as needed.
{
"collections": [
{
"slug": "learn",
"label": "Learn",
"labelSingular": "Article",
"supports": ["drafts", "revisions", "search", "seo"],
"fields": [
{ "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true },
{ "slug": "description", "label": "Description", "type": "text" },
{ "slug": "article_type", "label": "Article Type", "type": "select", "required": true,
"validation": { "options": ["pillar", "sub-pillar", "child"] } },
{ "slug": "parent", "label": "Parent Article", "type": "reference",
"options": { "collection": "learn" } },
{ "slug": "featured_image", "label": "Featured Image", "type": "image" },
{ "slug": "content", "label": "Content", "type": "portableText", "searchable": true },
{ "slug": "excerpt", "label": "Excerpt", "type": "text" }
]
}
]
} Field types
EmDash supports these field types:
| string | string | Single-line text: titles, labels, slugs |
| text | string | Multi-line text: descriptions, excerpts |
| number | number | Floating point values: prices, ratings |
| integer | number | Whole numbers: sort order, counts |
| boolean | boolean | True/false toggles: featured, published |
| datetime | Date | Date and time values: publish date, event date |
| image | { id, src?, alt?, width?, height? } | Featured images, thumbnails (use Image component) |
| reference | string (ULID) | Relations to other entries (parent, author) |
| select | string | Predefined options: article type, status |
| portableText | PortableTextBlock[] | Rich text: article body, page content |
| json | any | Arbitrary structured data: galleries, settings |
Practical example: article hierarchy
Dashstro's learn collection uses a select field (article_type with options pillar, sub-pillar, child) and a reference field (parent, pointing back to learn) to create a content hierarchy. Pillar articles are top-level. Sub-pillars link to a pillar parent. Children link to a sub-pillar parent.
This hierarchy drives breadcrumbs, internal linking, and content organization — all from two simple fields. No special plugin or custom code required.
Your content model is version-controlled, diffable, and reproducible. Spin up a new environment and you have an identical schema.
Taxonomies
Taxonomies classify content. EmDash supports hierarchical taxonomies (like categories) and flat taxonomies (like tags). Define them in the seed with a name, label, and list of initial terms. Apply them to specific collections.
Important: the taxonomy name in your code must exactly match the seed's name field. Use "category" not "categories". Using the wrong name returns empty results with no error.
Tips from building Dashstro
- Don't use "version" as a field slug — it's reserved. Dashstro's changelog uses "release_version" instead.
- Mark fields as searchable: true if you want them included in full-text search. Only title and content fields are typically worth indexing.
- Reference fields store the database ULID, not the slug. When you need to look up a referenced entry, you'll need to match against entry.data.id, not entry.id.
- Start simple. You can always add fields later. Removing fields is harder because existing content may use them.