Documentation
Feedback
Guides
API Reference

Guides
Guides
CMS
Integrations

Defining Content Types for Headless stores

A Content Type is the schema template to create page entries. In a headless project, you declare each Content Type as an individual .jsonc file, combine them into a schema bundle with the Content plugin, and upload the bundle to the Schema Registry.

This guide covers how to define Content Types: where files live, which properties are required, and two annotated examples (minimal and full).

For modeling concepts (Content Type vs. Component, singleton patterns, recommended page structures), see Understanding content modeling and architecture for headless stores.

Before you begin

Organizing Content Type files

Each Content Type lives in its own file. The Content plugin discovers files by prefix and merges them into the content-types key of your schema bundle.

Structuring directories

For headless storefronts, keep Content Type files separate from component schemas:


_11
your-headless-project/
_11
├── cms/
_11
│ ├── components/
_11
│ │ ├── cms_component__SEO.jsonc
_11
│ │ └── cms_component__PromoBanner.jsonc
_11
│ └── pages/
_11
│ ├── cms_content_type__home.jsonc
_11
│ ├── cms_content_type__landingPage.jsonc
_11
│ └── cms_content_type__aboutPage.jsonc
_11
└── src/
_11
└── … # Your storefront implementation

You can co-locate Content Type files under cms/pages/ (shown above) or use another directory; the CLI accepts custom paths as arguments. What matters is the file prefix, not the folder name.

Naming Content Type files

RuleExampleResult in schema bundle
File prefixcms_content_type__Required. Files without this prefix are ignored.
Content Type IDcms_content_type__landingPage.jsoncBecomes the key landingPage under content-types.
CasingUse camelCase for the ID segmentlandingPage, blogPost, globalHeader
Extension.jsonc (recommended) or .json.jsonc allows inline comments for documentation.

The Content Type ID (the segment after cms_content_type__) is what editors see in the CMS Admin and what your storefront uses in Data Plane URLs: …/{storeId}/landingPage/entries/slug/{slug}.

Uploading the schema bundle

Remember, this step applies only to headless stores. Don't use the vtex.headless base schema for FastStore stores, as it may remove the default FastStore content types and components from the store schema.

Before uploading the schema, open the generated schema.json file and review its contents. Make sure it includes the expected content types, components, and pages for your headless store.

Uploading a schema replaces the current schema configuration for the selected store. Reviewing the file first helps prevent accidentally removing existing content types or components.

Generate and upload the bundle with the headless base schema:


_10
vtex content generate-schema cms/components cms/pages \
_10
--out schema.json \
_10
--base vtex.headless
_10
_10
vtex content upload-schema schema.json

Replace {storeId} with your headless store ID when prompted, or pass it with the store flag documented in the Content plugin.

Declaring required and optional properties

A Content Type schema is a JSON Schema object with CMS-specific metadata. The table below lists every property you're likely to use.

CMS-specific properties

PropertyRequiredDescription
typeMust be "object".
titleDisplay name in the CMS Admin (for example, "Landing Page").
$singletontrue if only one entry is allowed store-wide (home); false if editors can create many entries.
identifierKeysArray of field names that identify an entry. Use [] for singletons; ["slug"] for multi-instance pages.
propertiesField and relation definitions for the Content Type.
$extendsOptionalInherits structure from a base template (for example, #/$defs/base-page-template).
descriptionOptionalHelp text shown to editors in the Admin.
requiredOptionalLists top-level fields editors must complete before saving.

Common fields inside properties

FieldTypical useNotes
slugMulti-instance pagesUse "ui:widget": "slug". Add "slug" to identifierKeys.
seoFixed SEO block on every entryEmbed with "$ref": "#/components/SEO".
sectionsDynamic page blocksReference "#/$defs/$ALLOW_ALL_COMPONENTS" or a restricted anyOf.

$ALLOW_ALL_COMPONENTS is generated when you run vtex content generate-schema. Reference it in Content Types — don't create it manually in individual files.

identifierKeys and $singleton

These two properties work together to control how entries are created and fetched:

Pattern$singletonidentifierKeysEditor experienceData Plane lookup
Single page (Home)true[]One entry, edited in placeBy Content Type name
Multi-instance pagefalse["slug"]Many entries, each with a slugBy slug
Multi-key identityfalse["slug", "locale"]Rare. Only when multiple keys identify an entryBy composite key

For most headless pages, use either a singleton with empty identifierKeys or identifierKeys: ["slug"].

Defining a minimal Content Type

The example below defines a singleton About page. It has a sections array only. No slug, no embedded components. Editors add sections from the components you registered in the bundle.

File: cms/pages/cms_content_type__aboutPage.jsonc


_21
{
_21
// Display name in the CMS Admin
_21
"title": "About Page",
_21
_21
// Every Content Type must be an object
_21
"type": "object",
_21
_21
// Only one About entry for the entire store
_21
"$singleton": true,
_21
_21
// No identifier fields — singletons use an empty array
_21
"identifierKeys": [],
_21
_21
"properties": {
_21
"sections": {
_21
"title": "Page sections",
_21
// Generated by `generate-schema` — lists all your components
_21
"$ref": "#/$defs/$ALLOW_ALL_COMPONENTS"
_21
}
_21
}
_21
}

What editors get: a single About entry where they add and reorder sections such as PromoBanner or RichTextBlock.

What your storefront does: fetch the about entry by Content Type name and render each section by componentKey.

Defining a full Content Type with relations and media

The example below defines a multi-instance Landing Page Content Type with:

  • A slug field for routing.
  • An embedded SEO component (relation via $ref).
  • A hero image field using the media gallery widget.
  • A sections array for dynamic page blocks

Prerequisite component: cms/components/cms_component__SEO.jsonc:


_20
{
_20
"$componentKey": "SEO",
_20
"$componentTitle": "SEO",
_20
"type": "object",
_20
"required": ["title", "description"],
_20
"properties": {
_20
"title": {
_20
"title": "Page title",
_20
"type": "string"
_20
},
_20
"description": {
_20
"title": "Meta description",
_20
"type": "string"
_20
},
_20
"canonical": {
_20
"title": "Canonical URL",
_20
"type": "string"
_20
}
_20
}
_20
}

Content Type file: cms/pages/cms_content_type__landingPage.jsonc:


_50
{
_50
"title": "Landing Page",
_50
"type": "object",
_50
"description": "Marketing landing pages with a unique URL slug.",
_50
_50
// Many landing pages allowed
_50
"$singleton": false,
_50
_50
// Slug uniquely identifies each entry
_50
"identifierKeys": ["slug"],
_50
_50
// Top-level fields editors must complete
_50
"required": ["slug"],
_50
_50
"properties": {
_50
"slug": {
_50
"title": "Slug",
_50
"description": "URL path segment for this page (for example, summer-sale).",
_50
"type": "string",
_50
"minLength": 3,
_50
"widget": {
_50
"ui:widget": "slug" // Slug input with / prefix normalization
_50
}
_50
},
_50
_50
"seo": {
_50
"title": "SEO",
_50
// Relation: embeds the SEO component on every landing page entry
_50
"$ref": "#/components/SEO"
_50
},
_50
_50
"heroImage": {
_50
"title": "Hero image",
_50
"description": "Optional full-width image above the page sections.",
_50
"type": "string",
_50
"widget": {
_50
"ui:widget": "media-gallery",
_50
"restrictMediaTypes": {
_50
"image": ["png", "jpeg", "webp"],
_50
"video": false
_50
}
_50
}
_50
},
_50
_50
"sections": {
_50
"title": "Page sections",
_50
"$ref": "#/$defs/$ALLOW_ALL_COMPONENTS"
_50
}
_50
}
_50
}

Understanding relations in this example

PropertyMechanismBehavior
seo$ref to #/components/SEOEvery entry includes one SEO block with title, description, and canonical. Editors complete it as part of the landing page form.
sections$ref to $ALLOW_ALL_COMPONENTSEditors add any registered component. Each item in the array includes a componentKey at publish time.
heroImagemedia-gallery widgetStores a Media Gallery asset URL as a string. Your storefront renders it in the page layout outside the section loop.

Reviewing the published entry shape

After editors save and publish, the Data Plane returns content shaped by this schema:


_17
{
_17
"componentKey": "landingPage",
_17
"slug": "summer-sale",
_17
"seo": {
_17
"componentKey": "SEO",
_17
"title": "Summer Sale",
_17
"description": "Our biggest sale of the season.",
_17
"canonical": "/summer-sale"
_17
},
_17
"heroImage": "https://content.vtexassets.com/assets/…",
_17
"sections": [
_17
{
_17
"componentKey": "PromoBanner",
_17
"title": "Up to 50% off"
_17
}
_17
]
_17
}

Your storefront maps landingPage to its page template, reads slug for routing, and renders seo, heroImage, and each item in sections.

Understanding content modeling and architecture for headless stores
Review modeling concepts, design principles, and recommended page patterns.
Content plugin
Generate and upload schema bundles for your headless store.
Contributors
2
Photo of the contributor
Photo of the contributor
Was this helpful?
Yes
No
Suggest Edits (GitHub)
Contributors
2
Photo of the contributor
Photo of the contributor
Was this helpful?
Suggest edits (GitHub)
On this page