---
title: "Custom Content Types in EmDash: A Practical Guide"
description: "How to define collections, fields, taxonomies, and relationships in EmDash's seed.json. Build any content model your site needs."
article_type: child
canonical: https://dashstro.com/learn/emdash-custom-content-types
---
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.

```json filename=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:

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

:::pullquote
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 →](/learn/build-first-emdash-website)

