EmDash: Content Modeling
Beta preview software. Verify field types, commands, and APIs against the docs and repo — they change between releases.
When to Use
Use this when designing a content model in EmDash: defining content types ("collections") and their fields in the visual schema builder, generating typed query helpers, querying content in Astro templates, and deciding how to structure rich content (Portable Text) plus editorial workflow (revisions, drafts, scheduling, search).
Core Concept — Collections and Fields
A collection is a content type (posts, pages, products); its field definitions set the shape of each entry (Collections, Content Model). You "create and edit collections and fields visually in the admin panel under Content Types, or with the CLI" (Content Model). A collection is described by:
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview", "scheduling"]
}
Collection properties: slug, label, labelSingular, description, icon (Lucide icon name), and supports ("Features like drafts, revisions, preview, scheduling, search, seo").
Every entry also carries system fields managed by EmDash: id, slug, status, author_id, created_at/updated_at/published_at, deleted_at (soft delete — "the row is kept"), and version ("increments on each save") (Content Model). Reserved slugs you cannot reuse for your own fields include id, slug, status, author_id, created_at, updated_at, published_at, scheduled_at, deleted_at, version, terms, bylines.
Schema safety: "Adding a field is always safe. Removing one drops its data. Rename rather than remove-and-recreate to preserve values."
Each field type "maps to a SQLite column type" (Field Types), so a collection's fields are backed by real, typed database columns.
Field Types
EmDash "provides 16 field types… Each type maps to a SQLite column type and provides appropriate admin UI" (Field Types):
| Field type | SQLite column | Use for |
|---|---|---|
string |
TEXT | titles, names, short values |
text |
TEXT | descriptions, excerpts, longer plain text |
url |
TEXT | URL value |
number |
REAL | prices, ratings, measurements |
integer |
INTEGER | quantities, counts, order |
boolean |
INTEGER | toggles, flags |
datetime |
TEXT | ISO 8601 timestamps |
select |
TEXT | single choice from options |
multiSelect |
JSON | multiple choices |
portableText |
JSON | rich text (Portable Text) |
image |
TEXT | reference to an uploaded image (dimensions, alt) |
file |
TEXT | reference to an uploaded file (doc/PDF) |
reference |
TEXT | reference to another entry |
json |
JSON | arbitrary nested structures |
slug |
TEXT | URL-safe identifier |
repeater |
JSON | repeating group of fields |
(The CLI schema add-field help text lists only 10 of these — a docs gap, not necessarily a feature gap.)
Steps — Define a Model and Generate Types
- In the admin, open Content Types and use the schema builder to create a collection, then add fields (from the 16 field types) and toggle
supportsfeatures. - Generate TypeScript types from the live model:
This "writes
npx emdash types.emdash/types.tswith an interface per collection and typed query overloads" plus aschema.jsonalongside for reference. "Re-run the command after changing the model to keep types in sync." Useful flags:--url(defaulthttp://localhost:4321),--token,--output(default.emdash/types.ts). You can also pass--typestonpx emdash devto regenerate on dev start. (Content Model, CLI) - Types augment the
"emdash"module via declaration merging, so the query helpers become fully typed per collection.
Pattern — Querying Content
Content is queried in Astro templates with two helpers — getEmDashCollection / getEmDashEntry — using "Astro's live content collections with automatic caching" (Querying Content, API Reference, Create a Blog).
import { getEmDashCollection, getEmDashEntry } from "emdash";
// List (filtered)
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
where: { category: "news" }, // multiple values use OR logic
});
// Single entry — returns { entry, error, isPreview }; entry is null (not an error) when missing
const { entry, isPreview } = await getEmDashEntry("posts", Astro.params.slug);
CollectionFilter accepts status ("draft" | "published" | "archived"), limit, and where. Sort order is not guaranteed — "Sort results in your template." Field values are read from entry.data.* (e.g. post.data.slug, post.data.content).
For visual/inline editing, spread the edit proxy onto elements ({...entry.edit.title}); "in production, the proxy spreads produce no output."
Pattern — Portable Text vs WordPress Serialized HTML
The portableText field stores rich content as structured JSON (SQLite JSON column), not as an HTML string. "Portable Text is a specification for structured rich text. See portabletext.org for details." (Field Types). It is authored in the TipTap editor (README) and rendered with a component:
import { PortableText } from "emdash/ui";
<PortableText value={post.data.content} />
Why this matters for a content model: WordPress's the_content() emits a single serialized HTML blob, mixing content with presentation. EmDash maps the_content() → <PortableText /> and "Gutenberg blocks convert to Portable Text"; collections "work like Custom Post Types" (Coming from WordPress). Because the body is structured JSON, the same content can be rendered to different presentations, queried, and transformed — decoupling content from markup.
Conversion helpers prosemirrorToPortableText / portableTextToProsemirror are exported from emdash for moving between the editor and the stored Portable Text (API Reference).
Editing, Revisions, Drafts, and Scheduling
The editor supports headings (H2–H6), inline formatting, lists, links, media-library images, code blocks (syntax highlighting), sanitized HTML blocks, embeds (YouTube/Vimeo/Twitter), and reusable Sections via a /section command. HTML blocks "are sanitized before rendering on the frontend to prevent XSS"; by default iframes are allowed only from www.youtube.com and player.vimeo.com (Working with Content).
- Statuses: "Every entry has one of three statuses": Draft (admin only), Published (public), Archived (admin only). The
CollectionFiltertype confirms"draft" | "published" | "archived". Scheduling is achieved via Draft + a future publication date, not a discretescheduledstatus. - Revisions: enabled by the
revisionssupport; "track content history with version snapshots," viewable from a sidebar Revisions list with timestamps. No maximum revision count is documented.versionincrements on each save. (REST exposes revision list/get/restore endpoints.) - Scheduled publishing: with the
schedulingsupport, set status to Draft and a future Publication date; "when the publication date arrives, the content automatically becomes published." (scheduled_atis a system field.) - Drafts & preview: the
previewsupport generates "secure, time-limited URLs" with "HMAC-SHA256 signed tokens";getEmDashEntryreturnsisPreview(Preview).
Search
Collections opt into search via the search support. EmDash exposes full-text search:
import { search } from "emdash";
const results = await search("hello world", {
collections: ["posts", "pages"],
status: "published",
limit: 20,
});
There is also a REST surface: GET /_emdash/api/search?q=… (params q, collections, status, limit, cursor) and POST /_emdash/api/search/rebuild to rebuild the FTS index (REST API, API Reference).
The docs describe a full-text search index (the engine is not named in the docs) plus the rebuild endpoint above.
Common Mistakes
- Expecting guaranteed ordering from
getEmDashCollection. It is not guaranteed — sort in the template. - Treating Portable Text as an HTML string. It is structured JSON; render with
<PortableText>(or convert via the prosemirror helpers), not by injecting HTML. - Forgetting to re-run
npx emdash types. Types drift from the model after schema edits; regenerate to keep query helpers typed. - Assuming a
scheduledstatus exists. Statuses are draft/published/archived; scheduling = Draft + future publication date. - Removing a field to "rename" it. Removing drops its data; rename in place to preserve values.
See Also
- Previous: Getting Started and Architecture
- Reference: Collections, Content Model, Field Types, Querying Content