Field types reference
Fields are declared with the fields object from @menestrel/fields, inside the collections and singletons of cms.config.ts. This page details each type: its options, what the editor sees in the admin, and the value your Astro code receives. The big picture of the schema (locales, sync, diff) lives in Modeling content.
import { collection, defineConfig, fields, singleton } from '@menestrel/fields';At a glance
Section titled “At a glance”| Type | Role | Own options |
|---|---|---|
text |
Short single-line text | default, min, max, pattern, placeholder |
textarea |
Long plain text | default, min, max, rows |
richtext |
Formatted text | features |
number |
Integer or decimal number | default, min, max, integer, step, unit |
boolean |
On/off switch | default, trueLabel, falseLabel |
select |
Choice from a closed list | options, multiple, default |
date |
Date only | default, min, max |
datetime |
Date and time, in UTC | default, min, max |
image |
Image from the media library | none |
file |
Downloadable file | accept |
relation |
Link to entries of a collection | to, many |
group |
Grouped sub-fields | fields |
repeater |
Ordered list of blocks | fields, min, max, itemLabel |
slug |
Page address | from |
seo |
Prefabricated SEO group | none |
Common options
Section titled “Common options”Every type accepts these options, except the cases listed right after.
| Option | Type | Default | Description |
|---|---|---|---|
label |
string | { fr, en } |
capitalized key | Label in the admin. This is the interface language (always fr and en), independent from content locales. A bare string is duplicated into both languages. |
help |
string | { fr, en } |
none | Help text shown under the field. |
required |
boolean |
false |
Evaluated per locale: publishing a language requires the required fields in that language. A draft can stay incomplete. |
localized |
boolean |
false |
One value per content language. When not localized, the field keeps a single value across languages. |
default |
typed per field | null |
Applied when an entry is created in the admin, never injected on read: an absent value stays absent. |
Deliberate exceptions:
booleanrefusesrequired: a switch always has a state. Itsdefaultisfalse, notnull.groupandrepeatercarry neitherrequired,localizednordefault: their sub-fields declare their own rules.slugis always required and inherits its localization from the source field.richtextandimagehave nodefault: a default TipTap document or image makes no editorial sense.seoonly acceptslabelandhelp: its sub-fields are frozen.
Short single-line text: title, name, reference. 320 characters max by default.
| Option | Type | Description |
|---|---|---|
default |
string |
Value on creation. |
min |
number |
Minimum length. |
max |
number |
Maximum length (default: 320). |
pattern |
string |
Source of a regular expression, kept as a string so the definition stays JSON. |
placeholder |
string | { fr, en } |
Hint text in the empty field. |
title: fields.text({ label: 'Title', localized: true, required: true, max: 120 }),reference: fields.text({ label: 'Reference', pattern: '^[A-Z]{2}-\\d{4}$', placeholder: 'ST-0042',}),In the admin: a single-line input, with the optional placeholder.
On the Astro side: a string.
textarea
Section titled “textarea”Long text without formatting: summary, excerpt, description. For bold, headings or links, use richtext.
| Option | Type | Description |
|---|---|---|
default |
string |
Value on creation. |
min |
number |
Minimum length. |
max |
number |
Maximum length (default: 20,000). |
rows |
number |
Height of the input area in lines (default: 5). |
summary: fields.textarea({ label: { fr: 'Résumé', en: 'Summary' }, help: { fr: 'Affiché sur les cartes de la liste.', en: 'Shown on the list cards.' }, localized: true, max: 300, rows: 3,}),In the admin: a multi-line text area, no toolbar.
On the Astro side: a string.
richtext
Section titled “richtext”Formatted text, over a closed perimeter of eight features. Anything outside the whitelist (node, mark, attribute) is rejected at validation, on the client and on the server alike: no arbitrary HTML in the database.
| Option | Type | Description |
|---|---|---|
features |
RichtextFeature[] |
Subset of the eight features. Default: all of them. |
The eight possible features values:
| Feature | What it enables |
|---|---|
heading |
H2 and H3 headings only. The H1 belongs to the layout, not the content. |
bold |
Bold. |
italic |
Italic. |
bullet_list |
Bullet list. |
ordered_list |
Numbered list. |
link |
Links. Accepted schemes: http(s), mailto:, tel:, internal paths (/...) and anchors (#...). |
image |
Images from the media library, with a placement-specific alt. |
video |
YouTube or Vimeo videos, referenced by id. |
body: fields.richtext({ label: 'Body', localized: true }),quote: fields.richtext({ label: 'Quote', features: ['bold', 'italic', 'link'],}),In the admin: a TipTap editor whose toolbar only shows the declared features.
On the Astro side: a structured document to render with <RichText />. A node outside the whitelist is skipped with a build warning, never rendered as-is.
number
Section titled “number”Integer or decimal number: price, area, duration.
| Option | Type | Description |
|---|---|---|
default |
number |
Value on creation. |
min |
number |
Lower bound. |
max |
number |
Upper bound. |
integer |
boolean |
true: only accepts integers (default: false). |
step |
number |
Step of the input arrows, display only (default: 1 when integer, else 0.01). |
unit |
string |
Unit shown next to the field ('€', 'm²', 'h'). Never stored. |
price: fields.number({ label: { fr: 'Prix à partir de', en: 'Starting price' }, unit: '€', min: 0,}),duration_days: fields.number({ label: 'Duration', unit: 'days', integer: true, min: 1 }),In the admin: a numeric input with the unit displayed next to it. The French comma is accepted and normalized to a dot. A cleared field stays absent, never turned into an implicit zero.
On the Astro side: a number.
boolean
Section titled “boolean”On/off switch: featured, availability. required is forbidden on this type, and the compiler rejects it: a switch always has a state.
| Option | Type | Description |
|---|---|---|
default |
boolean |
State on creation (default: false). |
trueLabel |
string | { fr, en } |
Label of the on state. |
falseLabel |
string | { fr, en } |
Label of the off state. |
featured: fields.boolean({ label: { fr: 'Mettre en avant', en: 'Feature' }, trueLabel: { fr: 'Sur la page d’accueil', en: 'On the home page' }, falseLabel: { fr: 'Liste uniquement', en: 'List only' },}),In the admin: a switch, with the state labels when declared.
On the Astro side: a boolean.
select
Section titled “select”Choice from a closed list, single or multiple. Validation only accepts declared values: no orphan value in the database.
| Option | Type | Description |
|---|---|---|
options |
Array<string | { value, label? }> |
Required, non-empty. A bare string is a value that also serves as its label. Values of 1 to 64 characters, unique. |
multiple |
boolean |
true: several choices, stored as an array without duplicates (default: false). |
default |
string | string[] |
Value(s) on creation. |
condition: fields.select({ label: 'Condition', options: [ { value: 'new', label: { fr: 'Neuf', en: 'New' } }, { value: 'used', label: { fr: 'Occasion', en: 'Used' } }, ], default: 'new',}),mounting: fields.select({ label: 'Mounting types', options: ['wall', 'ceiling', 'recessed'], multiple: true,}),In the admin: a dropdown, or a multi-select when multiple is true.
On the Astro side: a string, or a string[] with multiple.
Date only, no time: publication date, event date. Stored as YYYY-MM-DD. Validation checks the date really exists in the calendar: a February 30th is rejected.
| Option | Type | Description |
|---|---|---|
default |
string |
In YYYY-MM-DD format. |
min |
string |
Earliest date, same format. |
max |
string |
Latest date, same format. |
event_date: fields.date({ label: 'Date', required: true, min: '2020-01-01' }),In the admin: a date picker.
On the Astro side: a string in YYYY-MM-DD format.
datetime
Section titled “datetime”Date and time. Stored in UTC, as YYYY-MM-DDTHH:mm:ssZ: the trailing Z is mandatory, no ambiguous local time in the database.
| Option | Type | Description |
|---|---|---|
default |
string |
In YYYY-MM-DDTHH:mm:ssZ format. |
min |
string |
Earliest instant, same format. |
max |
string |
Latest instant, same format. |
published_at: fields.datetime({ label: { fr: 'Publié le', en: 'Published at' } }),In the admin: a date and time input in the browser’s timezone, with the timezone shown under the field. Conversion to UTC happens on save.
On the Astro side: a UTC ISO string, ready for new Date(...).
Image picked from the project’s media library. No own option, no default. The main alt text lives on the asset, in the media library; each placement can override it.
picture: fields.image({ label: 'Picture', required: true }),In the admin: a button that opens the media library, a preview of the chosen image and a placement-specific alt field.
On the Astro side: a resolved AssetRef object:
{ src: string; mime: string; bytes: number; width: number | null; height: number | null; alt: string; // the asset's alt, or the placement override focal_x: number | null; // focal point, used as object-position focal_y: number | null; variants?: Array<{ width: number; format: 'avif' | 'webp' | 'jpg'; url: string }>;}Variants are generated at widths 400, 800, 1200, 1600 and 2048 px. The <CmsImage /> component turns them into a responsive <picture>, focal point included.
Downloadable file: PDF, archive, catalog.
| Option | Type | Description |
|---|---|---|
accept |
string[] |
Allowed MIME types. The real check happens at upload. |
catalog: fields.file({ label: 'PDF catalog', accept: ['application/pdf'] }),In the admin: a media library picker, restricted to the declared types.
On the Astro side: the same AssetRef object as image, without variants or a useful focal point: src, mime and bytes are enough for a download link.
relation
Section titled “relation”Link to one or several entries of another collection: featured services, related articles, the author of a post.
| Option | Type | Description |
|---|---|---|
to |
string |
Required. Key of a collection of the same config. Never a singleton: the compiler rejects it. |
many |
boolean |
true: ordered list of entries, without duplicates (default: false, a single entry). |
featured_services: fields.relation({ label: { fr: 'Services mis en avant', en: 'Featured services' }, to: 'services', many: true,}),In the admin: a search across the entries of the target collection, with each entry’s status (published or draft). With many, the chosen list can be reordered.
On the Astro side: an entry id (string), or an array of ids with many. The loader rewrites them into store ids of the same locale: getEntry('services', id) works without a helper. An unpublished target is omitted with a warning (single relation) or filtered out of the list (multiple relation). Details in the loader reference.
Visually grouped sub-fields: an address, an opening-hours block. A group is structural: no required, localized or default. Its sub-fields carry their own rules, including their translation.
| Option | Type | Description |
|---|---|---|
fields |
Record<string, Field> |
Required. The sub-fields, in form order. |
address: fields.group({ label: 'Address', fields: { street: fields.text({ label: 'Street', required: true }), city: fields.text({ label: 'City', required: true }), zip: fields.text({ label: 'Zip code', pattern: '^\\d{5}$' }), },}),In the admin: a collapsible block containing the sub-fields.
On the Astro side: a nested object: entry.data.address.city.
repeater
Section titled “repeater”Ordered list of blocks with the same shape: testimonials, steps, frequently asked questions.
| Option | Type | Description |
|---|---|---|
fields |
Record<string, Field> |
Required. The shape of each item. |
min |
number |
Minimum number of items. |
max |
number |
Maximum number of items. |
itemLabel |
string |
Key of a sub-field shown as the collapsed item title. |
testimonials: fields.repeater({ label: { fr: 'Témoignages', en: 'Testimonials' }, itemLabel: 'author', max: 6, fields: { author: fields.text({ label: 'Author', required: true, max: 80 }), quote: fields.textarea({ label: 'Quote', localized: true, required: true, rows: 3 }), },}),Each item gets a stable internal identifier (_id): reordering or translating never mixes blocks up.
In the admin: collapsible blocks to add, duplicate, remove (with undo) and reorder, by buttons or keyboard. The add button disables at max.
On the Astro side: an array of objects, each with its _id and its sub-values.
The page address. Mandatory in every collection (exactly one, at the root), forbidden in a singleton: a unique content already has its address. Its key is imposed: slug.
| Option | Type | Description |
|---|---|---|
from |
string |
Required. Key of a text field at the root of the same collection. |
label |
string | { fr, en } |
Default: “Page address”. |
help |
string | { fr, en } |
Help text. |
The slugFrom shortcut on the collection avoids the explicit declaration:
services: collection({ slugFrom: 'title', // equivalent to slug: fields.slug({ from: 'title' }) fields: { title: fields.text({ label: 'Title', localized: true, required: true }), },}),A slug is always required. Its localization follows the source field: a translated title gives one slug per language. The value accepts lowercase letters, digits and hyphens (outdoor-blinds), 160 characters max.
In the admin: the slug follows the source field as the editor types, as long as it is not locked. A manual edit locks it; a padlock lets it follow again.
On the Astro side: a string (the bare slug). It also composes the loader’s entry id: en/outdoor-blinds.
Prefabricated group for search engines, dropped in as-is: its four sub-fields are frozen by the package version. Root only, one occurrence per collection or singleton.
| Option | Type | Description |
|---|---|---|
label |
string | { fr, en } |
Default: “Référencement” / “SEO”. |
help |
string | { fr, en } |
Help text. |
seo: fields.seo(),The sub-fields, in form order:
| Sub-field | Type | Rules |
|---|---|---|
title |
text |
Localized, 70 characters max. |
description |
textarea |
Localized, 160 characters max. |
image |
image |
Share image (Open Graph). |
noindex |
boolean |
Hide the page from search engines. Default: false. |
In the admin: a collapsible block with a search-result-style preview and 70/160 counters that turn red when exceeded.
On the Astro side: an object to pass to <CmsSeo />, which writes <title>, the meta description, the canonical and the Open Graph tags.
The rules the compiler enforces
Section titled “The rules the compiler enforces”A reminder of the structural rules, checked at compile time before any network call:
- Lowercase field keys:
^[a-z][a-z0-9_]{0,63}$.idis reserved;slugis reserved to theslugtype. slugandseolive at the root, never inside agroupor arepeater.- Three nested container levels at most.
- 200 fields max per collection (sub-fields included), 100 collections and singletons per project.
The reasoning behind these rules and how sync works are in Modeling content.