field.media attaches assets from the Media Library
to a content type.
fields: {
coverImage: field.media({
accept: { kinds: ['image'] },
admin: { label: 'Cover image' }
}),
gallery: field.media({
multiple: true,
accept: { mimeTypes: ['image/*'] }
})
}| Option | Default | Effect |
|---|---|---|
multiple | false | Hold an ordered list of assets rather than one. |
accept | any asset | Restrict which assets may be attached. |
required | false | The field must hold an asset. |
localized | false | The attachment differs per locale. |
How it is stored
A single media field is a uuid('<field>') column. A multiple: true field is a
jsonb('<field>') array of ids, mirroring how multiselect stores its values.
Both live in the values bag, not in a join table, and that has a set of consequences that all fall out for free:
- Media values are captured by the revision snapshot, so version history includes what was attached.
- They are synced across locale siblings when the field is shared, and are
per-locale when it is marked
localized— exactly like a scalar. - They travel in the same
valuesobject as everything else on the API.
No foreign key
Asset ids are plain uuids with no Postgres foreign key. The assets live in the
media plugin’s own schema, and content-server cannot reference another plugin’s
tables — the same convention as workspace_id.
Existence and the accept restriction are enforced in the application layer
instead, on every write. What that buys you is a system where the media plugin
can be absent entirely: with no media plugin bound, media fields still
shape-check (is this a uuid?) and still store, they just skip the
existence and restriction checks.
accept
Both filters are optional, and they combine as OR within each list. An asset
passes when it matches any listed kind or any listed MIME pattern. An
accept with neither list accepts anything.
// Any image
field.media({ accept: { kinds: ['image'] } })
// PNGs and PDFs specifically
field.media({ accept: { mimeTypes: ['image/png', 'application/pdf'] } })
// Any image, plus PDFs
field.media({
accept: { kinds: ['image'], mimeTypes: ['application/pdf'] }
})The five coarse kinds are image, video, audio, document and archive.
An unknown kind is rejected at define time. A MIME pattern is either exact
(application/pdf) or a type/* wildcard (image/*).
What a failed check looks like
An asset that does not exist, one that belongs to another workspace, and one
that accept disallows all produce the same 422.
That uniformity is deliberate. Answering “no such asset” differently from “not allowed here” would tell a caller whether an id they cannot otherwise see is real, which is an enumeration signal. Every relation and media target check in the system behaves this way.
Reading assets back
An entry’s stored value is a bare id — or an array of them. To turn those into something displayable:
- In the admin,
GET /api/content/:type/:id/mediaresolves an entry’s media ids to display references: name, thumbnail URL and kind. - On the public API, add
?media=preview&mediaFields=coverImage,galleryto a read. See expanding relations and media.
Both go through the same query, so the admin and a public consumer see the same resolution rules.