How to Customize Content Models in EmDash CMS

EmDash's content model is the part of the platform closest to WordPress's mental model — collections work like post types, fields work like custom fields — but the implementation is fully typed and changeable at runtime with no code required for the schema itself. Here's how to actually define and evolve it.
Table of Contents
- The Core Idea: Change It Anytime, Type-Safe Throughout
- Creating a Collection
- The 16 Field Types
- Defining a Field: The Common Shape
- Reference Fields for Relational Content
- Reserved Field Slugs
- Portable Text: The Rich Content Field
- Exporting Your Model as a Reusable Seed
- Frequently Asked Questions
- Can a non-developer safely create new collections?
- What happens to existing content if I change a field's type?
- Should I use json or repeater for structured, repeating data?
- Do generated TypeScript types update automatically when I change a field?
- The Bottom Line
The Core Idea: Change It Anytime, Type-Safe Throughout
You define collections and fields — in the admin panel or with the CLI — and EmDash stores content against them. Add, rename, remove, or retype collections and fields whenever you need to; changes take effect immediately. Generate TypeScript types from the current model for autocomplete from query to template.
That combination — no-code schema changes plus generated types — is the specific thing to understand about EmDash's content model. A content editor with no development background can design a whole collection through the admin UI, and the moment they do, your Astro templates get autocomplete for the fields they just created.
Creating a Collection
- In the admin panel, navigate to the Visual Schema Builder.
- Create a new collection and give it a slug (e.g. `testimonials`).
- Add fields one at a time, choosing a type for each.
- Save — the collection and its fields are live immediately, with no redeploy required.
The same thing scripted via the CLI, useful for reproducible setups or CI:
npx emdash schema list
npx emdash schema get postsThe 16 Field Types
Every field maps to a specific SQLite column type under the hood, which is worth knowing when you're deciding between similar-looking options:
- string — short single-line text (TEXT). Titles, names, short values.
- text — multi-line plain text (TEXT). Descriptions, excerpts.
- slug — URL-safe identifier (TEXT), auto-sanitized: lowercased, spaces to hyphens, special characters stripped.
- number — decimal values (REAL). Prices, ratings, measurements.
- integer — whole numbers (INTEGER). Quantities, counts.
- boolean — true/false (INTEGER 0/1). Toggles and flags.
- datetime — ISO 8601 date and time (TEXT).
- select — single choice from a fixed option list (TEXT).
- multiSelect — multiple choices from a fixed option list (JSON array).
- portableText — the rich text editor, block-based structured content (JSON).
- image — reference to an uploaded image via the media library (TEXT/JSON, includes dimensions and alt text).
- file — reference to an uploaded document or other file (TEXT/JSON).
- reference — link to another collection's entry, single or with allowMultiple (TEXT or array).
- json — arbitrary, unvalidated JSON for genuinely dynamic data.
- repeater — a repeating group of sub-fields (JSON).
Defining a Field: The Common Shape
{
slug: "price",
label: "Price",
type: "number",
required: true,
validation: {
min: 0,
max: 999999.99,
},
}Every field type accepts the same base properties: `slug` (the stored key, required), `label` (admin display name), `type`, `required`, `unique`, `defaultValue`, `validation` (type-specific rules), `widget` (override the default input UI), `options` (widget configuration), and `sortOrder` (admin display order).
Reference Fields for Relational Content
A `reference` field is how EmDash models relationships — an author linked to posts, a category linked to products:
{
slug: "author",
label: "Author",
type: "reference",
required: true,
options: {
collection: "authors",
},
}Set `allowMultiple: true` in `options` for a many-relationship (a post with several contributors, say) — the value is then stored as an array of entry IDs instead of a single one.
Reserved Field Slugs
A specific, easy-to-hit gotcha: certain slugs are reserved by EmDash's core schema and can't be reused for a custom field. If you name a field one of these, the schema builder will reject it:
- id, slug, status, author_id, primary_byline_id
- created_at, updated_at, published_at, scheduled_at, deleted_at
- version, live_revision_id, draft_revision_id
- terms, bylines, byline
If you're migrating a WordPress custom field named one of these, rename it during the import's field-mapping step rather than fighting the reserved name.
Portable Text: The Rich Content Field
The `portableText` field type is what gives EmDash structured (not raw-HTML) rich content — headings, lists, links, images, code blocks, and custom plugin-defined block types, stored as a JSON array of typed blocks rather than an HTML string:
[
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "Hello world" }]
}
]That structure is exactly what makes EmDash content portable and queryable in ways raw HTML fields aren't — you can programmatically find every post referencing a specific link, or bulk-transform a block type, without parsing HTML.
Exporting Your Model as a Reusable Seed
Once you've built a content model you're happy with, export it as a portable JSON seed file — useful for version control, or bootstrapping a second project from the same starting structure:
npx emdash export-seed --with-content posts,pages > seed.jsonThe seed captures collections, fields, taxonomies, menus, widget areas, and settings, plus content if you request it — and a new EmDash site reads it automatically on first boot if placed at `.emdash/seed.json`.
Frequently Asked Questions
Can a non-developer safely create new collections?
Yes — that's a deliberate design goal. The Visual Schema Builder is admin-panel-only, no code required, and a content editor can design an entire collection structure themselves.
What happens to existing content if I change a field's type?
Retyping a field is supported, but incompatible value conversions (e.g., turning free text into a strict `select` with a fixed option list) can surface conflicts the admin UI will flag — review existing data before a type change on a field with real content in it.
Should I use json or repeater for structured, repeating data?
Prefer `repeater` when the shape is known and consistent (a list of FAQ question/answer pairs, for instance) since it gives you defined sub-fields and admin UI. Reserve `json` for genuinely freeform or third-party-integration data without a fixed structure.
Do generated TypeScript types update automatically when I change a field?
Types regenerate from your current schema — run the type-generation step (via the CLI's `--types` flag on `emdash dev`, or your project's configured flow) after a schema change to get updated autocomplete in your editor.
The Bottom Line
EmDash's content model is genuinely no-code to design and fully typed to consume — 16 field types cover most structured-content needs, reference fields handle relationships, and Portable Text keeps rich content queryable instead of opaque HTML. Once you've modeled something you like, export it as a seed and reuse it. See our guide to querying that content from Astro once your schema is in place.




Comments