Référence des types de champs
Les champs se déclarent avec l’objet fields de @menestrel/fields, dans les collections et singletons de cms.config.ts. Cette page détaille chaque type : ses options, ce que l’éditeur voit dans l’admin, et la valeur que ton code Astro reçoit. La vue d’ensemble du schéma (locales, sync, diff) est dans Modéliser le contenu.
import { collection, defineConfig, fields, singleton } from '@menestrel/fields';Récapitulatif
Section intitulée « Récapitulatif »| Type | Rôle | Options propres |
|---|---|---|
text |
Texte court d’une ligne | default, min, max, pattern, placeholder |
textarea |
Texte long sans mise en forme | default, min, max, rows |
richtext |
Texte mis en forme | features |
number |
Nombre entier ou décimal | default, min, max, integer, step, unit |
boolean |
Interrupteur oui/non | default, trueLabel, falseLabel |
select |
Choix dans une liste fermée | options, multiple, default |
date |
Date seule | default, min, max |
datetime |
Date et heure, en UTC | default, min, max |
image |
Image de la médiathèque | aucune |
file |
Fichier téléchargeable | accept |
relation |
Lien vers des entrées d’une collection | to, many |
group |
Sous-champs regroupés | fields |
repeater |
Liste ordonnée de blocs | fields, min, max, itemLabel |
slug |
Adresse de la page | from |
seo |
Groupe SEO préfabriqué | aucune |
Les options communes
Section intitulée « Les options communes »Tous les types acceptent ces options, sauf exceptions listées juste après.
| Option | Type | Défaut | Description |
|---|---|---|---|
label |
string | { fr, en } |
clé capitalisée | Libellé dans l’admin. C’est la langue de l’interface (toujours fr et en), indépendante des locales de contenu. Une chaîne simple est dupliquée dans les deux langues. |
help |
string | { fr, en } |
aucun | Texte d’aide affiché sous le champ. |
required |
boolean |
false |
S’évalue par locale : publier une langue exige les champs requis dans cette langue. Un brouillon peut rester incomplet. |
localized |
boolean |
false |
Une valeur par langue de contenu. Non traduit, le champ garde une seule valeur pour toutes les langues. |
default |
typé par champ | null |
Appliqué à la création d’une entrée dans l’admin, jamais injecté en lecture : une valeur absente reste absente. |
Exceptions voulues :
booleanrefuserequired: un interrupteur a toujours un état. Sondefaultvautfalse, pasnull.groupetrepeaterne portent nirequired, nilocalized, nidefault: leurs sous-champs déclarent leurs propres règles.slugest toujours requis et sa localisation est héritée du champ source.richtextetimagen’ont pas dedefault: un document TipTap ou une image par défaut n’aurait pas de sens éditorial.seon’accepte quelabelethelp: ses sous-champs sont figés.
Texte court d’une ligne : titre, nom, référence. 320 caractères maximum par défaut.
| Option | Type | Description |
|---|---|---|
default |
string |
Valeur à la création. |
min |
number |
Longueur minimale. |
max |
number |
Longueur maximale (défaut : 320). |
pattern |
string |
Source d’une expression régulière, gardée en chaîne pour que la définition reste du JSON. |
placeholder |
string | { fr, en } |
Texte indicatif dans le champ vide. |
titre: fields.text({ label: 'Titre', localized: true, required: true, max: 120 }),reference: fields.text({ label: 'Référence', pattern: '^[A-Z]{2}-\\d{4}$', placeholder: 'ST-0042',}),Dans l’admin : un champ de saisie d’une ligne, avec le placeholder éventuel.
Côté Astro : une string.
textarea
Section intitulée « textarea »Texte long sans mise en forme : résumé, extrait, description. Pour du gras, des titres ou des liens, prends richtext.
| Option | Type | Description |
|---|---|---|
default |
string |
Valeur à la création. |
min |
number |
Longueur minimale. |
max |
number |
Longueur maximale (défaut : 20 000). |
rows |
number |
Hauteur de la zone de saisie en lignes (défaut : 5). |
resume: 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,}),Dans l’admin : une zone de texte multiligne, sans barre d’outils.
Côté Astro : une string.
richtext
Section intitulée « richtext »Texte mis en forme, sur un périmètre fermé de huit fonctionnalités. Tout ce qui sort de la liste blanche (nœud, marque, attribut) est rejeté à la validation, côté client comme côté serveur : pas de HTML arbitraire en base.
| Option | Type | Description |
|---|---|---|
features |
RichtextFeature[] |
Sous-ensemble des huit fonctionnalités. Défaut : toutes. |
Les huit valeurs possibles de features :
| Feature | Ce qu’elle active |
|---|---|
heading |
Titres H2 et H3 uniquement. Le H1 appartient au layout, pas au contenu. |
bold |
Gras. |
italic |
Italique. |
bullet_list |
Liste à puces. |
ordered_list |
Liste numérotée. |
link |
Liens. Schémas acceptés : http(s), mailto:, tel:, chemins internes (/...) et ancres (#...). |
image |
Images de la médiathèque, avec alt spécifique à l’emplacement. |
video |
Vidéos YouTube ou Vimeo, référencées par identifiant. |
contenu: fields.richtext({ label: 'Contenu', localized: true }),citation: fields.richtext({ label: 'Citation', features: ['bold', 'italic', 'link'],}),Dans l’admin : un éditeur TipTap dont la barre d’outils ne montre que les fonctionnalités déclarées.
Côté Astro : un document structuré à rendre avec <RichText />. Un nœud hors liste blanche est ignoré avec un avertissement de build, jamais rendu tel quel.
Nombre entier ou décimal : prix, surface, durée.
| Option | Type | Description |
|---|---|---|
default |
number |
Valeur à la création. |
min |
number |
Borne basse. |
max |
number |
Borne haute. |
integer |
boolean |
true : n’accepte que des entiers (défaut : false). |
step |
number |
Pas des flèches de saisie, affichage seulement (défaut : 1 si integer, sinon 0.01). |
unit |
string |
Unité affichée à côté du champ ('€', 'm²', 'h'). Jamais stockée. |
prix: fields.number({ label: { fr: 'Prix à partir de', en: 'Starting price' }, unit: '€', min: 0,}),duree_jours: fields.number({ label: 'Durée', unit: 'jours', integer: true, min: 1 }),Dans l’admin : une saisie numérique avec l’unité affichée à côté. La virgule française est acceptée puis normalisée en point. Un champ vidé reste absent, jamais transformé en zéro implicite.
Côté Astro : un number.
Interrupteur oui/non : mise en avant, disponibilité. required est interdit sur ce type, et le compilateur le refuse : un interrupteur a toujours un état.
| Option | Type | Description |
|---|---|---|
default |
boolean |
État à la création (défaut : false). |
trueLabel |
string | { fr, en } |
Libellé de l’état activé. |
falseLabel |
string | { fr, en } |
Libellé de l’état désactivé. |
en_avant: 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' },}),Dans l’admin : un interrupteur, avec les libellés d’état s’ils sont déclarés.
Côté Astro : un boolean.
Choix dans une liste fermée, simple ou multiple. La validation n’accepte que les valeurs déclarées : pas de valeur orpheline en base.
| Option | Type | Description |
|---|---|---|
options |
Array<string | { value, label? }> |
Obligatoire, non vide. Une chaîne nue est une valeur qui sert aussi de libellé. Valeurs de 1 à 64 caractères, uniques. |
multiple |
boolean |
true : plusieurs choix, stockés en tableau sans doublon (défaut : false). |
default |
string | string[] |
Valeur(s) à la création. |
statut: fields.select({ label: 'Statut', options: [ { value: 'neuf', label: { fr: 'Neuf', en: 'New' } }, { value: 'occasion', label: { fr: 'Occasion', en: 'Used' } }, ], default: 'neuf',}),poses: fields.select({ label: 'Types de pose', options: ['murale', 'plafond', 'encastree'], multiple: true,}),Dans l’admin : une liste déroulante, ou une sélection multiple si multiple est vrai.
Côté Astro : une string, ou un string[] si multiple.
Date seule, sans heure : date de publication, d’événement. Stockée au format YYYY-MM-DD. La validation vérifie que la date existe vraiment dans le calendrier : un 30 février est rejeté.
| Option | Type | Description |
|---|---|---|
default |
string |
Au format YYYY-MM-DD. |
min |
string |
Date plancher, même format. |
max |
string |
Date plafond, même format. |
date_evenement: fields.date({ label: 'Date', required: true, min: '2020-01-01' }),Dans l’admin : un sélecteur de date.
Côté Astro : une string au format YYYY-MM-DD.
datetime
Section intitulée « datetime »Date et heure. Stockée en UTC, au format YYYY-MM-DDTHH:mm:ssZ : le Z final est obligatoire, pas d’heure locale ambiguë en base.
| Option | Type | Description |
|---|---|---|
default |
string |
Au format YYYY-MM-DDTHH:mm:ssZ. |
min |
string |
Instant plancher, même format. |
max |
string |
Instant plafond, même format. |
publie_le: fields.datetime({ label: { fr: 'Publié le', en: 'Published at' } }),Dans l’admin : une saisie date et heure dans le fuseau du navigateur, avec le fuseau affiché sous le champ. La conversion vers l’UTC est faite à l’enregistrement.
Côté Astro : une string ISO en UTC, prête pour new Date(...).
Image choisie dans la médiathèque du projet. Aucune option propre, pas de default. Le texte alternatif principal vit sur l’asset, dans la médiathèque ; chaque emplacement peut le surcharger.
visuel: fields.image({ label: 'Visuel', required: true }),Dans l’admin : un bouton qui ouvre la médiathèque, un aperçu de l’image choisie et un champ d’alt spécifique à cet emplacement.
Côté Astro : un objet AssetRef résolu :
{ src: string; mime: string; bytes: number; width: number | null; height: number | null; alt: string; // alt de l'asset, ou la surcharge de l'emplacement focal_x: number | null; // point focal, utilisé en object-position focal_y: number | null; variants?: Array<{ width: number; format: 'avif' | 'webp' | 'jpg'; url: string }>;}Les variantes sont générées aux largeurs 400, 800, 1200, 1600 et 2048 px. Le composant <CmsImage /> en tire un <picture> responsive, point focal compris.
Fichier téléchargeable : PDF, archive, catalogue.
| Option | Type | Description |
|---|---|---|
accept |
string[] |
Types MIME autorisés. Le contrôle réel a lieu à l’upload. |
catalogue: fields.file({ label: 'Catalogue PDF', accept: ['application/pdf'] }),Dans l’admin : un bouton de choix dans la médiathèque, restreint aux types déclarés.
Côté Astro : le même objet AssetRef que pour image, sans variants ni point focal utile : src, mime, bytes suffisent pour un lien de téléchargement.
relation
Section intitulée « relation »Lien vers une ou plusieurs entrées d’une autre collection : services mis en avant, articles liés, auteur d’un billet.
| Option | Type | Description |
|---|---|---|
to |
string |
Obligatoire. Clé d’une collection du même config. Jamais un singleton : le compilateur le refuse. |
many |
boolean |
true : liste ordonnée d’entrées, sans doublon (défaut : false, une seule entrée). |
services_en_avant: fields.relation({ label: { fr: 'Services mis en avant', en: 'Featured services' }, to: 'services', many: true,}),Dans l’admin : une recherche parmi les entrées de la collection cible, avec le statut de chaque entrée (publiée ou brouillon). En multiple, la liste choisie se réordonne.
Côté Astro : un id d’entrée (string), ou un tableau d’ids si many. Le loader les réécrit en ids de store de la même locale : getEntry('services', id) fonctionne sans helper. Une cible dépubliée est omise avec un avertissement (relation simple) ou filtrée de la liste (relation multiple). Détail dans la référence du loader.
Sous-champs regroupés visuellement : une adresse, un bloc horaires. Un groupe est structurel : ni required, ni localized, ni default. Ses sous-champs portent leurs propres règles, y compris leur traduction.
| Option | Type | Description |
|---|---|---|
fields |
Record<string, Field> |
Obligatoire. Les sous-champs, dans l’ordre du formulaire. |
adresse: fields.group({ label: 'Adresse', fields: { rue: fields.text({ label: 'Rue', required: true }), ville: fields.text({ label: 'Ville', required: true }), code_postal: fields.text({ label: 'Code postal', pattern: '^\\d{5}$' }), },}),Dans l’admin : un bloc repliable qui contient les sous-champs.
Côté Astro : un objet imbriqué : entry.data.adresse.ville.
repeater
Section intitulée « repeater »Liste ordonnée de blocs de même forme : témoignages, étapes, questions fréquentes.
| Option | Type | Description |
|---|---|---|
fields |
Record<string, Field> |
Obligatoire. La forme de chaque élément. |
min |
number |
Nombre minimal d’éléments. |
max |
number |
Nombre maximal d’éléments. |
itemLabel |
string |
Clé d’un sous-champ affichée comme titre de l’élément replié. |
temoignages: fields.repeater({ label: { fr: 'Témoignages', en: 'Testimonials' }, itemLabel: 'auteur', max: 6, fields: { auteur: fields.text({ label: 'Auteur', required: true, max: 80 }), citation: fields.textarea({ label: 'Citation', localized: true, required: true, rows: 3 }), },}),Chaque élément reçoit un identifiant interne stable (_id) : réordonner ou traduire ne mélange jamais les blocs entre eux.
Dans l’admin : des blocs repliables à ajouter, dupliquer, supprimer (avec annulation) et réordonner, par boutons ou au clavier. Le bouton d’ajout se désactive à max.
Côté Astro : un tableau d’objets, chacun avec son _id et ses sous-valeurs.
L’adresse de la page. Obligatoire dans chaque collection (exactement un, à la racine), interdit dans un singleton : un contenu unique a déjà son adresse. Sa clé est imposée : slug.
| Option | Type | Description |
|---|---|---|
from |
string |
Obligatoire. Clé d’un champ text à la racine de la même collection. |
label |
string | { fr, en } |
Défaut : « Adresse de la page ». |
help |
string | { fr, en } |
Texte d’aide. |
Le raccourci slugFrom sur la collection évite la déclaration explicite :
services: collection({ slugFrom: 'titre', // équivaut à slug: fields.slug({ from: 'titre' }) fields: { titre: fields.text({ label: 'Titre', localized: true, required: true }), },}),Un slug est toujours requis. Sa localisation suit le champ source : un titre traduit donne un slug par langue. La valeur accepte minuscules, chiffres et tirets (stores-bannes), 160 caractères maximum.
Dans l’admin : le slug suit le champ source à mesure de la frappe, tant qu’il n’est pas figé. Une édition manuelle le fige ; un cadenas permet de le refaire suivre.
Côté Astro : une string (le slug nu). Il compose aussi l’id d’entrée du loader : fr/stores-bannes.
Groupe préfabriqué pour le référencement, à poser tel quel : ses quatre sous-champs sont figés par la version du package. À la racine uniquement, une occurrence par collection ou singleton.
| Option | Type | Description |
|---|---|---|
label |
string | { fr, en } |
Défaut : « Référencement » / « SEO ». |
help |
string | { fr, en } |
Texte d’aide. |
seo: fields.seo(),Les sous-champs, dans l’ordre du formulaire :
| Sous-champ | Type | Règles |
|---|---|---|
title |
text |
Traduit, 70 caractères max. |
description |
textarea |
Traduit, 160 caractères max. |
image |
image |
Image de partage (Open Graph). |
noindex |
boolean |
Masquer la page des moteurs de recherche. Défaut : faux. |
Dans l’admin : un bloc repliable avec un aperçu façon résultat de recherche et des compteurs 70/160 qui passent au rouge en cas de dépassement.
Côté Astro : un objet à passer à <CmsSeo />, qui écrit <title>, la meta description, le canonical et les balises Open Graph.
Les règles que le compilateur applique
Section intitulée « Les règles que le compilateur applique »Rappel des règles structurelles, vérifiées à la compilation avant tout appel réseau :
- Clés de champs en minuscules :
^[a-z][a-z0-9_]{0,63}$.idest réservée ;slugest réservée au typeslug. slugetseovivent à la racine, jamais dans ungroupou unrepeater.- Trois niveaux maximum de conteneurs imbriqués.
- 200 champs maximum par collection (sous-champs compris), 100 collections et singletons par projet.
Le pourquoi de ces règles et le fonctionnement du sync sont dans Modéliser le contenu.