Skip to content

Content types in code

A content type in the config file is the same thing an editor builds under Content types in the admin, written down. Put it in contentTypes of defineConfig(), or declare it with defineContentType() and import it: the result is identical.

import { defineContentType } from '@manablox/core';
export const teaser = defineContentType({
name: 'teaser',
label: 'Teaser',
kind: 'block',
fields: [
{ name: 'headline', type: 'string', required: true },
{ name: 'body', type: 'richtext' },
{ name: 'image', type: 'asset', settings: { accept: ['image/'] } },
],
});
export const page = defineContentType({
name: 'page',
label: 'Page',
icon: 'doc',
hasSlug: true,
isPublishable: true,
fields: [
{ name: 'summary', type: 'string', settings: { editor: 'textarea', max: 300 }, localized: true },
{
name: 'components',
type: 'blocks',
settings: { types: [teaser.id] },
admin: { zone: 'main' },
},
{ name: 'meta_title', type: 'string', settings: { max: 60 }, admin: { zone: 'sidebar' } },
],
});

Type options

OptionDefaultMeaning
namerequiredThe technical name: ^[a-z][a-z0-9_-]*$. Becomes the GraphQL type name (blog-post -> BlogPost) and the type a frontend sees. Immutable once documents exist
labelfrom nameWhat the admin shows
description, iconicon is the symbol the admin draws for the type everywhere it appears - the tree, the lists, the pickers. It is one of the admin’s own icon names, the set the builder’s Icon picker offers - Lucide icons chosen for what a type is about (newspaper, book-open, calendar, map-pin, shopping-bag, building-2, image, video, …); a name it does not ship falls back to the default for the kind
kindcontentcontent lives in the tree; block only ever lives inside another document’s block field; data is a databag, flat records outside the tree
spaceIdnullnull means global: available in every space. Set it to confine a code type to one space
hasSlugtrueThe type has a slug and a permalink, so its documents are pages with URLs. Off for a container or a data record
isPublishabletrueDocuments have a draft and a published state. Off for a type that is live as soon as it is saved
isVisibleInTreetrueDocuments appear in the admin’s content tree and can be created from it
canBeVisibleInMenutrueDocuments may be added to a menu
requiresApprovalfalseDocuments go through review: an author who cannot publish asks, whoever can is told. Ignored when isPublishable is off
pluginsData for plugins by plugin id, checked by each plugin when the config loads. The website plugin takes a BlockDesign on block types (plugins.website), which a designed site uses until a space stores its own. Data for a plugin that is not loaded is refused (contentType.plugin.unknown), invalid data too (contentType.plugin.invalid)
idderived from nameLeave it out. Pinning an id ties the config to one database

A block type may not set hasSlug, isPublishable, isVisibleInTree, canBeVisibleInMenu or requiresApproval (a block is not a page), and the config refuses to load if it does (contentType.block.flagsNotAllowed). A databag type may not turn on hasSlug, isVisibleInTree or canBeVisibleInMenu (contentType.data.flagsNotAllowed); it keeps isPublishable and requiresApproval.

Every document additionally carries the built-in fields (title, slug, permalink, status, publishedAt, parentId, position, locale, createdAt, updatedAt, version), which follow the switches above. Do not declare fields with those names.

Field options

OptionDefaultMeaning
namerequiredTechnical name, same rules as a type’s. Immutable: it is part of the GraphQL schema
labelfrom nameShown in the editor
typerequiredA field type’s name: string, richtext, number, boolean, date, select, asset, content, user, block, blocks, or one you added. See Field types
settings{}The field type’s own settings, validated by its settingsSchema
requiredfalseA save without a value is refused (field.required)
localizedfalseEach translation keeps its own value. Off, the value is shared: saving one translation copies it to the others. See Locales and translations
uniquefalseRecorded on the definition and carried through the admin’s config export; the server does not enforce it yet
readRoleseveryoneRoles that may read the field. A field with readRoles is absent from the public GraphQL schema, not present-and-null
writeRoleseveryoneRoles that may change it
admin.zonemainmain or sidebar: which column of the editor
admin.width100Percent of the column
admin.positionits indexOrder within the zone
admin.help, admin.placeholderText under and inside the input

blocks and block settings name the allowed block types by id. A code-defined type’s id is derived from its name, so teaser.id above is stable across databases; a runtime type’s id is the one shown in the admin.

Fields for several types

A group of fields several types share, such as search engine fields, is either a list the config spreads into each type, or fields a plugin adds with extend to types declared in the config or by another plugin (settings of a field two plugins touch are merged). The first keeps the type’s fields in one place; the second lets a plugin bring its fields along. See Adding fields to other content types.

import { defineContentType, type FieldInput } from '@manablox/core';
const seoFields: FieldInput[] = [
{ name: 'meta_title', type: 'string', settings: { max: 60 }, admin: { zone: 'sidebar' } },
];
export const article = defineContentType({
name: 'article',
fields: [{ name: 'body', type: 'richtext' }, ...seoFields],
});

From the admin to code

Build the type in the admin, then let the admin write this file for you: Settings > Spaces > Transfer > The space as config renders the runtime types of a space as defineContentType calls, along with its workflows, endpoints, vault slots and templates if you tick them. What to watch for when you deploy the result is in Moving a space.