Field Types Reference

On this page

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:

TypeSQLite ColumnDescription
stringTEXTShort text input
textTEXTMulti-line text
urlTEXTURL value
numberREALDecimal number
integerINTEGERWhole number
booleanINTEGERTrue/false
datetimeTEXTDate and time
selectTEXTSingle choice from options
multiSelectJSONMultiple choices
portableTextJSONRich text content
imageTEXTImage reference
fileTEXTFile reference
referencenoneLinks to entries in another collection
jsonJSONArbitrary JSON data
slugTEXTURL-safe identifier
repeaterJSONRepeating group of fields
blocksJSONTyped 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 count
  • maxLength — Maximum character count
  • pattern — 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 count
  • maxLength — Maximum character count
  • pattern — 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: or https: URL, such as https://example.com/page
  • a mailto: or tel: 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 value
  • max — 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 value
  • max — 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 alongside targetCollection, 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 of relation this 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 requires slug, label, and type; it can also set required. A select sub-field supplies its choices through options.
  • minItems — Minimum number of rows. Must be zero or greater.
  • maxItems — Maximum number of rows. Must be one or greater and cannot be less than minItems.

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:

PropertyTypeDescription
slugstringUnique identifier (required)
labelstringDisplay name (required)
typeFieldTypeField type (required)
requiredbooleanRequire a value (default: false)
uniquebooleanEnforce uniqueness (default: false)
searchablebooleanInclude the field in full-text search (default: false)
indexedbooleanEnable indexed field sorting/filtering
translatablebooleanStore a value per locale (default: true)
defaultValueunknownDefault value for new entries
validationobjectType-specific validation rules
widgetstringCustom widget override
optionsobjectWidget configuration
sortOrdernumberDisplay 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:

  • 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

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",
];