EmDash supports 17 field types for defining content schemas. Each type maps to a SQLite column type and provides appropriate admin UI.
Overview
The following table lists every field type and its SQLite column:
| Type | SQLite Column | Description |
|---|---|---|
string | TEXT | Short text input |
text | TEXT | Multi-line text |
url | TEXT | URL value |
number | REAL | Decimal number |
integer | INTEGER | Whole number |
boolean | INTEGER | True/false |
datetime | TEXT | Date and time |
select | TEXT | Single choice from options |
multiSelect | JSON | Multiple choices |
portableText | JSON | Rich text content |
image | TEXT | Image reference |
file | TEXT | File reference |
reference | none | Links to entries in another collection |
json | JSON | Arbitrary JSON data |
slug | TEXT | URL-safe identifier |
repeater | JSON | Repeating group of fields |
blocks | JSON | Typed page composition |
Text types
string
Short, single-line text. Use for titles, names, and short values.
{
slug: "title",
label: "Title",
type: "string",
required: true,
validation: {
minLength: 1,
maxLength: 200,
},
}
Validation options:
minLength— Minimum character countmaxLength— Maximum character countpattern— Regular expression the value must match
The editor caps input at maxLength and shows a live character count against the length rules.
Widget options:
- None specific
text
Multi-line plain text. Use for descriptions, excerpts, and longer plain text.
{
slug: "excerpt",
label: "Excerpt",
type: "text",
options: {
rows: 3,
},
}
Validation options:
minLength— Minimum character countmaxLength— Maximum character countpattern— Regular expression the value must match
The editor caps input at maxLength and shows a live character count against the length rules.
Widget options:
rows— Number of rows in textarea (default: 3)
url
A link target. A value must be one of:
- an absolute
http:orhttps:URL, such ashttps://example.com/page - a
mailto:ortel:link - a site-relative path, such as
/about, or a fragment, such as#contact
The content API and the admin editor reject anything else, including protocol-relative URLs (//example.com), backslash forms that browsers resolve the same way (/\\example.com), control characters, and schemes that can run script, such as javascript: and data:.
{
slug: "website",
label: "Website",
type: "url",
required: true,
}
URL fields are stored as text. Seeds and plugin content updates preserve scheme-less values such as www.example.com, but reject unsafe schemes and values that a browser resolves to another site. Site imports apply the same checks to URL fields in entries, revisions, repeaters, and blocks.
Entries saved by earlier EmDash releases can hold an unsafe url value. Those values stay stored and queries return them unchanged. A write that sends the value again is rejected until the value is corrected. This includes an admin save of that entry and duplicating it. To render such values safely, pass url values through sanitizeHref() before putting them in an href.
slug
Text intended to hold a slug-like value. This custom field type does not generate or sanitize its value.
{
slug: "legacy_slug",
label: "Legacy Slug",
type: "slug",
required: true,
unique: true,
}
Every content entry already has a reserved system slug, which EmDash manages separately for public URLs. Use a custom slug field only when the content model needs another stored slug-like value.
Number types
number
Decimal number. Use for prices, ratings, and measurements.
{
slug: "price",
label: "Price",
type: "number",
required: true,
validation: {
min: 0,
max: 999999.99,
},
}
Validation options:
min— Minimum valuemax— Maximum value
The editor sets min and max on the number input and shows the allowed range beneath it.
Stored as SQLite REAL (64-bit floating point).
integer
Whole number. Use for quantities, counts, and order values.
{
slug: "quantity",
label: "Quantity",
type: "integer",
defaultValue: 1,
validation: {
min: 0,
max: 1000,
},
}
Validation options:
min— Minimum valuemax— Maximum value
The editor sets min and max on the number input and shows the allowed range beneath it.
Stored as SQLite INTEGER.
boolean
True or false. Use for toggles and flags.
{
slug: "featured",
label: "Featured",
type: "boolean",
defaultValue: false,
}
Stored as SQLite INTEGER (0 or 1).
Date and time
datetime
An instant in time. API, MCP, and CLI writes must include Z or an explicit UTC offset. The admin interprets its date-and-time picker in the timezone configured under Settings → General.
{
slug: "publishedAt",
label: "Published At",
type: "datetime",
}
Storage format: 2025-01-24T12:00:00.000Z
EmDash converts accepted input to UTC with three-digit milliseconds before storing it. For example, 2025-01-24T21:00:00+09:00 is stored as 2025-01-24T12:00:00.000Z. Use a string field instead when the value is a calendar date or local wall-clock time rather than an instant.
Selection types
select
Single selection from predefined options.
{
slug: "status",
label: "Status",
type: "select",
required: true,
defaultValue: "draft",
validation: {
options: ["draft", "published", "archived"],
},
}
Validation options:
options— Optional array of allowed values. Provide it to present predefined choices and reject other strings; without it, validation accepts any string.
Stored as TEXT containing the selected value.
multiSelect
Multiple selections from predefined options.
{
slug: "tags",
label: "Tags",
type: "multiSelect",
validation: {
options: ["news", "tutorial", "review", "opinion"],
},
}
Validation options:
options— Optional array of allowed values. Provide it to present predefined choices and reject other strings; without it, validation accepts any string array.
Stored as JSON array: ["news", "tutorial"]
Rich content
portableText
Rich text content using Portable Text format. Supports headings, lists, links, images, and custom blocks.
{
slug: "content",
label: "Content",
type: "portableText",
required: true,
}
The value is stored as a JSON array of Portable Text blocks, for example:
[
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "Hello world" }]
}
]
Plugins can add custom block types (embeds, widgets, etc.) to the editor. These appear in the slash command menu. Rendering the saved block on the public site requires an Astro component from a native plugin or companion package. See Portable Text rendering components.
Media types
image
Reference to an uploaded image. Includes metadata like dimensions and alt text.
{
slug: "featuredImage",
label: "Featured Image",
type: "image",
validation: {
allowedMimeTypes: ["image/jpeg", "image/png"],
},
options: {
darkVariant: true,
},
}
Widget options:
darkVariant— Offer editors a second slot for an image shown in dark color schemes (default: false). See Dark Mode.
Validation options:
allowedMimeTypes— Non-empty list of exact MIME types accepted for the selected media
The value is stored as an object with the media reference and its metadata:
{
"id": "01HXK5MZSN...",
"src": "/_emdash/api/media/file/01HXK5MZSN...",
"alt": "Description",
"width": 1920,
"height": 1080,
"provider": "local",
"meta": {
"storageKey": "01HXK5MZSN....jpg"
}
}
With darkVariant enabled, the value can carry the dark counterpart under darkVariant, in the same shape:
{
"id": "01HXK5MZSN...",
"alt": "Architecture diagram",
"width": 1920,
"height": 1080,
"darkVariant": {
"id": "01HXK5N2QT...",
"width": 1920,
"height": 1080
}
}
file
Reference to an uploaded file such as a document or PDF.
{
slug: "document",
label: "Document",
type: "file",
validation: {
allowedMimeTypes: ["application/pdf"],
},
}
Validation options:
allowedMimeTypes— Non-empty list of exact MIME types accepted for the selected media
The value is stored as a provider reference with cached metadata:
{
"id": "01HXK5MZSN...",
"provider": "local",
"filename": "report.pdf",
"mimeType": "application/pdf",
"meta": {
"storageKey": "01HXK5MZSN....pdf"
}
}
url and size, like the other cached metadata fields, are optional. Content queries return the
persisted value as-is and do not hydrate it from the media library. See
File values and current metadata for the
canonical lookup APIs when you need fresh metadata or a provider-specific URL.
Relational types
reference
Links an entry to entries in another collection, and renders as an entry picker in the admin. Its links belong to a relation: a schema object that joins two collections, names each side, and limits how many entries each side may link. Relations covers the editor and admin workflow.
The following field links a post to one entry in the authors collection:
{
slug: "author",
label: "Author",
type: "reference",
required: true,
validation: {
targetCollection: "authors",
multiple: false,
},
}
Validation:
targetCollection— Slug of the collection this field links to. Creating the field creates a relation for it.multiple— Allow more than one linked entry (default: false). Read only alongsidetargetCollection, because the limit then belongs to the new relation.relation— Slug of an existing relation to bind to instead of creating one.relationSide— Which end ofrelationthis collection sits on,"parent"or"child". Set it only for a relation whose two ends are the same collection, where both ends match.
Give either targetCollection or relation. A field created from targetCollection becomes the
parent end of a relation named {collection}_{field}, whose child side takes the field’s label and
holds one entry unless multiple is set. A field created from relation views that relation from
the end its collection sits on, and the collection at the other end is the field’s target. One
relation accepts one field per end, so a second field over the same end is rejected. Both forms
store relation, relationSide, and targetCollection on the created field, and the target
collection is fixed from then on: to change it, delete the field and add a new one. Renaming the
field renames the side of the relation it views.
A reference field adds no column to the collection table. Its links live in
_emdash_content_references, keyed by each entry’s translation group, so a selection is shared
across an entry’s translations rather than set per locale. Content reads return the linked entries
under references, keyed by field slug, instead of in data. In a template, ask for the field by
name — see Read reference fields.
A field bound to a relation cannot set indexed, because it stores no column to index, and site
search does not cover it.
Fields with no relation
A reference field that names neither a relation nor a target collection keeps a TEXT column and
holds one entry ID in it:
"01HXK5MZSN..."
A field whose options.allowMultiple is set holds a JSON array of entry IDs in the same column:
["01HXK5MZSN...", "01HXK6NATS..."]
The column is read and written like any other text column, the collection schema validates the
value as a string, and the field can set indexed and act as a content-list filter. It renders as a
text box rather than a picker.
To turn it into a picker, open the field under Content Types and choose a referenced collection.
EmDash creates the relation, copies the entry IDs in the column in as links, and clears the field’s
searchable and indexed flags, so content-list filters and site search stop covering it. The
column is left in place and stops being written. Relations
covers the same step from the editor’s side.
A site updated from an earlier release can hold fields in this state. See Reference fields bind to relations for what the update binds and what it leaves for you to bind.
Flexible types
json
Arbitrary JSON data. Use for complex nested structures, third-party integrations, or data without a fixed schema.
{
slug: "metadata",
label: "Metadata",
type: "json",
}
Stored as-is in SQLite JSON column.
repeater
A repeating list of structured rows. Define at least one sub-field in validation.subFields; editors can then add, remove, reorder, and edit rows without entering raw JSON.
The following field stores a list of product specifications:
{
slug: "specifications",
label: "Specifications",
type: "repeater",
validation: {
minItems: 1,
maxItems: 12,
subFields: [
{ slug: "label", label: "Label", type: "string", required: true },
{ slug: "value", label: "Value", type: "text", required: true },
{ slug: "source", label: "Source", type: "url" },
],
},
}
Repeater values are stored as an array of objects:
[
{
"label": "Weight",
"value": "1.2 kg",
"source": "https://example.com/specifications"
}
]
The allowed sub-field types are string, text, url, number, integer, boolean, datetime, select, and image. Repeaters cannot contain another repeater or a complex field such as portableText, reference, or file.
Repeater validation accepts these properties:
subFields— One or more sub-field definitions. Each definition requiresslug,label, andtype; it can also setrequired. Aselectsub-field supplies its choices throughoptions.minItems— Minimum number of rows. Must be zero or greater.maxItems— Maximum number of rows. Must be one or greater and cannot be less thanminItems.
blocks
An ordered list of typed content blocks. Each block type has its own fields and retained numbered versions. Define block types through the schema API, MCP, or a seed file before adding them to a collection field.
The following field allows editors to compose a page from hero and call-to-action blocks:
{
slug: "layout",
label: "Layout",
type: "blocks",
validation: {
allowedTypes: ["hero", "call_to_action"],
maxItems: 20,
},
}
Every stored block carries its type, version, and stable key alongside the fields declared by that version:
[
{
"_type": "hero",
"_version": 1,
"_key": "01K5AB3F7M9QZ2X8W4V6T1R0YH",
"heading": "Bread made slowly, by hand."
}
]
Blocks validation accepts these properties:
allowedTypes— Ordered block type slugs available for new blocks.retiredTypes— Server-managed block types retained for stored content but unavailable for new blocks.minItems— Minimum number of blocks. Raising it above zero on a populated collection requires a content migration.maxItems— Maximum number of blocks, up to 100.
A blocks field is optional and defaults to an empty array. It cannot be required, unique, searchable, indexed, or rendered by a custom field widget. Block definitions can use scalar, text, selection, Portable Text, image, file, and repeater fields. They cannot contain references, JSON, slugs, or nested blocks.
Render the stored array with <Blocks value components fallback> from emdash/ui. See Build pages with blocks for seed, component-map, missing-renderer, activation, and migration examples.
Field properties
All fields support these common properties:
| Property | Type | Description |
|---|---|---|
slug | string | Unique identifier (required) |
label | string | Display name (required) |
type | FieldType | Field type (required) |
required | boolean | Require a value (default: false) |
unique | boolean | Enforce uniqueness (default: false) |
searchable | boolean | Include the field in full-text search (default: false) |
indexed | boolean | Enable indexed field sorting/filtering |
translatable | boolean | Store a value per locale (default: true) |
defaultValue | unknown | Default value for new entries |
validation | object | Type-specific validation rules |
widget | string | Custom widget override |
options | object | Widget configuration |
sortOrder | number | Display order in admin |
indexed is available for scalar fields: string, url, number, integer, boolean,
datetime, select, reference, and slug. An indexed field can be passed as the orderBy
field or used in fieldFilters in content list queries. Avoid indexing fields that are not used
for sorting or filtering because every index adds storage and write overhead.
searchable adds the field’s text to the collection’s full-text search index. Set translatable: false for identifiers, prices, flags, and other values that must stay the same across every translation of an entry; when one locale changes a non-translatable field, EmDash synchronizes the value to its translated entries.
The blocks type uses only slug, label, type, translatable, validation, and sortOrder from this common set. Its array is never column-required and always defaults to [].
Reserved field slugs
These slugs are reserved and cannot be used:
idslugstatusauthor_idprimary_byline_idcreated_atupdated_atpublished_atscheduled_atdeleted_atversionlive_revision_iddraft_revision_idtermsbylinesbyline
TypeScript types
Import the field type definitions for programmatic use:
import type { FieldType, Field, CreateFieldInput } from "emdash";
const fieldTypes: FieldType[] = [
"string",
"text",
"url",
"number",
"integer",
"boolean",
"datetime",
"select",
"multiSelect",
"portableText",
"image",
"file",
"reference",
"json",
"slug",
"repeater",
"blocks",
];