Types and fields
A type is a schema. Entry types describe things editors create, such as pages, articles or the navigation. Block types describe sections that editors stack inside a blocks field.
Type
type CmsType = {
key: string // camelCase, unique per site, e.g. "page", "hero"
kind: 'entry' | 'block'
label: string
description?: string
icon?: string
group?: string // groups types in the editor's pickers
routable?: boolean // entry type whose entries get a path (pages)
singleton?: boolean // exactly one entry per site (navigation, settings)
titleField?: string // a text or longText field shown as the entry's name
listFields?: string[] // fields included in the manifest and in entry references
fields: FieldDef[]
example?: object | null // sample content for the editor's block picker
source: 'app' | 'code' // read only: "code" when pushed with avvyr-cms push
}
Rules the API enforces:
- A block type cannot be routable or a singleton, and a singleton cannot be routable.
titleFieldmust point at atextorlongTextfield.allowedTypesonblocksmust list block types, and onentryorentriesit must list entry types.- A type in use cannot be deleted (
409 CMS_TYPE_IN_USE). A type pushed from code cannot be edited in the app or throughPUT /types/{key}(409 CMS_TYPE_MANAGED_IN_CODE).
Use camelCase keys without underscores (productGrid, not product_grid). The API accepts _, but defineType in @avvyr/cms does not.
Fields
type FieldDef = {
key: string
type: FieldType
label?: string
description?: string
required?: boolean // checked when the entry is published
localized?: boolean // one value per language
options?: {
choices?: { value: string, label?: string }[] // select, multiSelect
allowedTypes?: string[] // blocks, entry, entries
accept?: ('image' | 'video' | 'document')[] // media, mediaList
min?: number, max?: number // number value, or number of items
maxLength?: number // text, longText
fields?: FieldDef[] // object
linkKinds?: ('entry' | 'url' | 'email' | 'phone' | 'media')[] // link, default all
}
ui?: { section?: string, help?: string, width?: 'full' | 'half' }
}
Field types
The draft value is what you write through the Management API. The published value is what the delivery API returns.
| Type | Draft value | Published value |
|---|---|---|
text, longText | string | string |
markdown | string | string (not rendered) |
richText | { "type": "doc", "content": [...] } | The same document; links to entries get href filled in |
number | number | number |
toggle | boolean | boolean |
date | "YYYY-MM-DD" | "YYYY-MM-DD" |
dateTime | ISO 8601 | ISO 8601 |
select | A choice's value | string |
multiSelect | List of choice values | string[] |
media | { assetId, alt?, focalPoint?: [x, y] }, x and y from 0 to 1 | { id, url, width, height, alt, focalPoint, mime, name } |
mediaList | List of media values | List of media |
link | See Links | { kind, href, title, newTab }, plus id, anchor and missing? for entry links |
entry | An entry id | { id, type, path, title, list, missing? } |
entries | List of entry ids | List of the above; unpublished entries are left out |
blocks | [{ id, type, fields }] | [{ id, type, fields }]; blocks of unknown types are left out |
object | { [fieldKey]: value } | { [fieldKey]: value } |
An empty field is null in the published document.
Links
{ "kind": "entry", "entryId": "…", "anchor": "faq", "title": "Read more", "newTab": false }
{ "kind": "url", "url": "https://example.com", "title": "Example", "newTab": true }
{ "kind": "email", "email": "hello@example.com" }
{ "kind": "phone", "phone": "+46 8 123 45 67" }
{ "kind": "media", "assetId": "…", "title": "Price list (PDF)" }
Link to entries by id, never by a typed path. The published href then follows the target when its slug or folder changes. When the target is not published, the link has missing: true. Render it as plain text.
Media
A media value points at an asset in the Avvyr media library. Upload with POST /v1/assets and use the _id it returns as assetId. Alt text belongs to the field, not the asset, so the same image can have different alt text in different places.
The published url is on assets.avvyr.com and takes the Assets API transforms (?width=&height=&fit=&fm=&q=). SVG and GIF are served unchanged.
Rich text
Rich text is a ProseMirror/Tiptap JSON document. Nodes: paragraph, heading (attrs.level), bulletList, orderedList, listItem, blockquote, codeBlock, horizontalRule, hardBreak and image. Marks: bold, italic, underline, strike, code and link. A link to an entry carries attrs.entryId, and the delivery API fills in attrs.href.
Render it with <CmsRichText> in Nuxt or renderRichTextHtml() from @avvyr/cms.
Localization
A field with localized: true stores one value per language, at every level, inside blocks and objects too:
{
"title": { "sv": "Om oss", "en": "About us" },
"body": [
{ "id": "h1x9k2", "type": "hero", "fields": { "title": { "sv": "Vi gör möbler", "en": "We make furniture" }, "layout": "imageRight" } }
]
}
The delivery API returns only the requested language: "title": "Om oss". A language without a value falls back to the site's default language. localized changes the draft, never the shape of the published document.
An entry can also be limited to some of the site's languages with locales. An empty list means every language.
Schema as code
Keep the types in your site's repository with @avvyr/cms, and push them from CI. The editor shows pushed types as locked.
// cms.config.ts
import { defineType, f } from '@avvyr/cms'
export const site = '<site id>' // optional, AVVYR_CMS_SITE_ID wins
export const textSection = defineType({
key: 'textSection',
kind: 'block',
fields: {
heading: f.text({ localized: true }),
body: f.richText({ localized: true }),
layout: f.select(['narrow', 'wide']),
},
})
export const article = defineType({
key: 'article',
kind: 'entry',
routable: true,
titleField: 'title',
listFields: ['title', 'image', 'excerpt'],
fields: {
title: f.text({ localized: true, required: true }),
excerpt: f.longText({ localized: true, maxLength: 240 }),
image: f.media({ accept: ['image'] }),
body: f.blocks([textSection]),
related: f.entries({ allowedTypes: ['article'], max: 3 }),
},
})
Builders: f.text, f.longText, f.richText, f.markdown, f.number, f.toggle, f.date, f.dateTime, f.select(choices), f.multiSelect(choices), f.media, f.mediaList, f.link, f.entry, f.entries, f.blocks(allowedTypes) and f.object(fields). Each takes label, description, required, localized and ui, plus its type's options. Field order follows the object.
Every exported defineType(...) is collected. An exported array or a default defineConfig({ site, types }) works too.
npx avvyr-cms push --dry-run # show what would change
npx avvyr-cms push # create and update types
npx avvyr-cms push --prune # also delete types the config no longer defines
npx avvyr-cms types --out cms.d.ts
types writes TypeScript interfaces for the published documents, such as ArticleFields, ArticleDocument, TextSectionBlock, CmsAnyBlock, CmsBlockTypes and CmsEntryTypes.
| Env | |
|---|---|
AVVYR_API_KEY | Management API key. Required for push. |
AVVYR_CMS_SITE_ID | Site id. Overrides site in the config. |
AVVYR_API_URL | Default https://api.avvyr.com. |
The config is loaded with jiti, so cms.config.ts works without a build step.