Skip to main content

webiny-api-cms-content-models

Creating Headless CMS content models via code using the ModelFactory pattern. Use this skill when the developer wants to create, modify, or understand content model definitions, define fields and validators, set up reference fields between models, configure field layouts (including nested layouts inside object or dynamicZone fields), pick the correct Admin UI renderer for a field type (textInput/textInputs, lexicalEditor/lexicalEditors, file/files, objectAccordionSingle/objectAccordionMultiple, etc.), or work with the ModelFactory builder API. Also covers field types (text, longText, number, boolean, datetime, asset, file, ref, object, richText, dynamicZone), list (array) fields via .list() and the singular-vs-plural renderer rule, the mandatory datetime subtype (dateOnly/timeOnly/withTimezone/withoutTimezone), validation (required, unique, email, pattern, minLength, maxLength, gte, predefinedValues), single-entry (singleton) models via .singleEntry(), model/field tags via .tags(), and field rules via .rules(

소스 정보

저장소
webiny/webiny-js
최근 소스 활동
2026년 10월 8일 12:44
감지된 SKILL.md 언어
영어
스타
8,049
포크
682

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
webiny-api-cms-content-models
description
Creating Headless CMS content models via code using the ModelFactory pattern. Use this skill when the developer wants to create, modify, or understand content model definitions, define fields and validators, set up reference fields between models, configure field layouts (including nested layouts inside object or dynamicZone fields), pick the correct Admin UI renderer for a field type (textInput/textInputs, lexicalEditor/lexicalEditors, file/files, objectAccordionSingle/objectAccordionMultiple, etc.), or work with the ModelFactory builder API. Also covers field types (text, longText, number, boolean, datetime, asset, file, ref, object, richText, dynamicZone), list (array) fields via .list() and the singular-vs-plural renderer rule, the mandatory datetime subtype (dateOnly/timeOnly/withTimezone/withoutTimezone), validation (required, unique, email, pattern, minLength, maxLength, gte, predefinedValues), single-entry (singleton) models via .singleEntry(), model/field tags via .tags(), and field rules via .rules() for access-control and conditional visibility/editability. Includes the correct `fields` projection syntax when querying entries via the SDK: `ref` fields use double-`values.` nesting (e.g. `values.author.values.name`) because they resolve to another entry, while `object` and `dynamicZone` sub-fields are inline and use a single `values.` (e.g. `values.author.name`) — getting this wrong silently returns null.
# Creating Content Models via Code ## TL;DR Content models are created using the `ModelFactory` pattern. You define a class implementing `ModelFactory.Interface`, use the fluent `ModelFactory.Builder` API to declare fields, validators, layout, and API names, then export with `ModelFactory.createImplementation()`. Register in `webiny.config.tsx` as `<Api.Extension>`. ## The ModelFactory Pattern Every code-based content model follows the same structure: ```typescript import { ModelFactory } from "webiny/api/cms/model"; class MyModelImpl implements ModelFactory.Interface { async execute(builder: ModelFactory.Builder) { return [ builder .public({ modelId: "myModel", name: "My Model", group: "ungrouped" }) .description("Description of the model") .fields(fields => ({ // field definitions here })) .layout([/* row definitions */]) .titleFieldId("fieldId") .singularApiName("MyModel") .pluralApiName("MyModels") ]; } } export default ModelFactory.createImplementation({ implementation: MyModelImpl, dependencies: [] }); ``` Register in `webiny.config.tsx`: ```tsx <Api.Extension src={"/extensions/MyModel.ts"} /> ``` **YOU MUST include the full file path with the `.ts` extension in the `src` prop.** For example, use `src={"/extensions/MyModel.ts"}`, NOT `src={"/extensions/MyModel"}`. Omitting the file extension will cause a build failure. **YOU MUST use `export default` for the `createImplementation()` call** when the file is targeted directly by an Extension `src` prop. Using a named export (`export const MyModel = ...`) will cause a build failure. Named exports are only valid inside files registered via `createFeature`. ## Model Configuration Methods | Method | Purpose | | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.public({ modelId, name, group })` | Creates a public model (accessible via Read API). `modelId` is the internal DB identifier. `group` organizes it in the Admin sidebar. | | `.description("...")` | Model description shown in Admin UI | | `.fields(fields => ({ ... }))` | Define all fields using the fluent field builder | | `.layout([["field1", "field2"], ["field3"]])` | Arrange fields in rows in the Admin editor. Each inner array is one row. | | `.titleFieldId("name")` | Which field to use as the entry's display title | | `.descriptionFieldId("message")` | Which field to use as the entry's description | | `.singularApiName("Product")` | Singular name for GraphQL queries (e.g., `getProduct`) | | `.pluralApiName("Products")` | Plural name for GraphQL queries (e.g., `listProducts`) | | `.singleEntry()` | Makes the model a singleton (only one entry can exist). Automatically adds the `"singleEntry"` tag. | | `.tags(["tag1", "tag2"])` | Assign custom tags to the model. The tag `"type:model"` is always added automatically. Duplicates are removed. | | `.settings({ ... })` | Model settings. Supported properties: `aiEntryWizard` (boolean), `previewPath` (string — frontend path pattern that enables live preview, e.g. `"/articles/{values.slug}"`; see [Live Preview](#live-preview)). | ## Live Preview Setting `previewPath` turns on CMS Live Preview for the model. The entry editor then shows a split view: the form on the left and the frontend, loaded in an iframe, on the right. As the user edits the form, the values are pushed into the iframe live. ```typescript .settings({ previewPath: "/articles/{values.slug}" }) ``` - `previewPath` is a **path, not a full URL**. The domain comes from the project's Frontend Settings (preview domain), which Website Builder uses too. It defaults to `http://localhost:3000`. - `{...}` placeholders are dot-paths into the entry, e.g. `{values.slug}` or `{entryId}`. A placeholder with an empty value resolves to `new`. The resolved URL (e.g. `/articles/my-post`) is used for the address bar, "open in new tab" and "copy URL". - A placeholder can read through a `ref` field into the referenced entry's values, e.g. `/{values.location.slug}/{values.slug}` resolves to `/baker/climbing`. The editor loads the referenced entry when the ref is set; until it arrives, the placeholder resolves to `new`. For a multiple-value ref field, index into it: `{values.tags.0.slug}`. - **The iframe loads a dedicated preview route, not the resolved URL.** That route is the static part of the pattern before the first `{`, followed by `/preview`. For example, `/articles/{values.slug}` loads `<domain>/articles/preview?wb.editing=true&wb.type=entry&wb.id=<entry id>&...`. The frontend must implement that route, typically with `@webiny/cms-nextjs` (`EntryRenderer`, `useEntry`, `DynamicZoneField`), and it receives the entry data from the editor instead of the API. - With `previewPath` set, the model editor also discovers the components the frontend registers. It offers them in the **Frontend Component** dropdown of dynamic zone templates (see `componentName` below). ## Layout `.layout()` takes a two-dimensional array of field IDs. Each inner array is one row in the Admin editor, and each entry within a row is a column cell. Field IDs must exactly match the keys used in `.fields()`. ### Top-level layout ```typescript .layout([ ["name", "slug"], // row 1: two columns ["description"], // row 2: one column, full width ["category", "price"] ]) ``` ### Nested layout inside an `object` field `object` fields have their own `.fields()` and `.layout()` that only reference the object's own sub-fields. The outer model layout should refer to the object field as a whole; its internal arrangement is owned by the object itself. ```typescript .fields((fields) => ({ name: fields.text().renderer("textInput").label("Name"), address: fields .object() .renderer("objectAccordionSingle") .label("Address") .fields((sub) => ({ street: sub.text().renderer("textInput").label("Street"), city: sub.text().renderer("textInput").label("City"), zip: sub.text().renderer("textInput").label("ZIP") })) .layout([ ["street"], // inner layout — only uses sub-field IDs ["city", "zip"] ]) })) .layout([ ["name"], ["address"] // outer layout just references the object field ]) ``` ### Nested layout inside a `dynamicZone` field `dynamicZone` is an array field where each entry is one of several named templates. Every template declares its own fields **and** its own layout, scoped to that template. The outer model layout simply references the dynamicZone field by its ID. Each template config accepts an optional `componentName` property that maps the template to a frontend UI component (e.g. `"Custom/Hero"`). This is used by the CMS live preview and Website Builder to resolve which React component renders the template's data. The API exposes the mapping as `model.metadata.componentMap` (template ID → component name). On the frontend, the component is registered under the same name with `createComponent(Component, { name, label, description })` from `@webiny/cms-nextjs`, and it receives the template's field values directly as props. ```typescript .fields((fields) => ({ blocks: fields .dynamicZone() .label("Content blocks") .template("hero", { name: "Hero", gqlTypeName: "HeroBlock", componentName: "Custom/Hero", fields: (t) => ({ heading: t.text().renderer("textInput").label("Heading"), image: t.asset().label("Image") }), layout: [ ["heading"], // layout inside the "hero" template only ["image"] ] }) .template("quote", { name: "Quote", gqlTypeName: "QuoteBlock", componentName: "Custom/Quote", fields: (t) => ({ text: t.longText().renderer("textarea").label("Quote text"), author: t.text().renderer("textInput").label("Author") }), layout: [ ["text"], ["author"] ] }) })) .layout([ ["blocks"] // outer layout references the dynamicZone as a whole ]) ``` Rule of thumb: **a layout can only reference field IDs in the same scope it's declared in.** Model layout references model fields. Object layout references that object's sub-fields. Each dynamicZone template's layout references only that template's fields. ## Field Types and Renderers Every field type exposes two renderer variants: a **single-value** renderer (used by default) and a **multi-value** renderer (used when the field is marked as a list via `.list()`). You **MUST** pair the renderer with the cardinality: calling `.list()` requires a renderer from the `list: true` column, and omitting `.list()` requires one from the `list: false` column. Using the wrong variant will render incorrectly in the Admin UI and the field may fail to save values. Invented names (e.g. `"fileInput"`, `"lexicalTextInput"`, `"objectInput"`, `"boolean"`) will silently misbehave the same way. Exception: `fields.boolean()` has no multi-value variant — do not call `.list()` on boolean fields. The authoritative source for these field types is the `webiny/api/cms/model` barrel export — if you're unsure, check the catalog skill `webiny-api-cms-catalog`. | Builder Method | Description | Single (`list: false`) | Multiple (`list: true`) | | ---------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------- | | `fields.text()` | Single-line text | `"textInput"` | `"textInputs"` | | `fields.longText()` | Multi-line text | `"textarea"` | `"textareas"` | | `fields.richText()` | Rich text (Lexical) | `"lexicalEditor"` | `"lexicalEditors"` | | `fields.number()` | Numeric value | `"numberInput"` | `"numberInputs"` |
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기