Skip to content
On this pageCollections: your content types
  1. Collections: your content types
  2. Field types
  3. Practical example: article hierarchy
  4. Taxonomies
  5. Tips from building Dashstro

Custom Content Types in EmDash: A Practical Guide

Ben 3 min read

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.

seed/seed.json
{
  "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:

stringstringSingle-line text: titles, labels, slugs
textstringMulti-line text: descriptions, excerpts
numbernumberFloating point values: prices, ratings
integernumberWhole numbers: sort order, counts
booleanbooleanTrue/false toggles: featured, published
datetimeDateDate and time values: publish date, event date
image{ id, src?, alt?, width?, height? }Featured images, thumbnails (use Image component)
referencestring (ULID)Relations to other entries (parent, author)
selectstringPredefined options: article type, status
portableTextPortableTextBlock[]Rich text: article body, page content
jsonanyArbitrary 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.
Full tutorial →