Skip to content

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';
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

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:

  • boolean refuses required: a switch always has a state. Its default is false, not null.
  • group and repeater carry neither required, localized nor default: their sub-fields declare their own rules.
  • slug is always required and inherits its localization from the source field.
  • richtext and image have no default: a default TipTap document or image makes no editorial sense.
  • seo only accepts label and help: 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.

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.

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.

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.

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.

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.

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.

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.

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.

A reminder of the structural rules, checked at compile time before any network call:

  • Lowercase field keys: ^[a-z][a-z0-9_]{0,63}$. id is reserved; slug is reserved to the slug type.
  • slug and seo live at the root, never inside a group or a repeater.
  • 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.