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
- Understanding content modeling and architecture for headless stores
- Content plugin installed
- Component schemas your Content Types reference (for example,
SEO,PromoBanner) are defined undercms/components/
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:
_11your-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
| Rule | Example | Result in schema bundle |
|---|---|---|
| File prefix | cms_content_type__ | Required. Files without this prefix are ignored. |
| Content Type ID | cms_content_type__landingPage.jsonc | Becomes the key landingPage under content-types. |
| Casing | Use camelCase for the ID segment | landingPage, 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.headlessbase 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:
_10vtex content generate-schema cms/components cms/pages \_10 --out schema.json \_10 --base vtex.headless_10_10vtex 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
| Property | Required | Description |
|---|---|---|
type | ✅ | Must be "object". |
title | ✅ | Display name in the CMS Admin (for example, "Landing Page"). |
$singleton | ✅ | true if only one entry is allowed store-wide (home); false if editors can create many entries. |
identifierKeys | ✅ | Array of field names that identify an entry. Use [] for singletons; ["slug"] for multi-instance pages. |
properties | ✅ | Field and relation definitions for the Content Type. |
$extends | Optional | Inherits structure from a base template (for example, #/$defs/base-page-template). |
description | Optional | Help text shown to editors in the Admin. |
required | Optional | Lists top-level fields editors must complete before saving. |
Common fields inside properties
| Field | Typical use | Notes |
|---|---|---|
slug | Multi-instance pages | Use "ui:widget": "slug". Add "slug" to identifierKeys. |
seo | Fixed SEO block on every entry | Embed with "$ref": "#/components/SEO". |
sections | Dynamic page blocks | Reference "#/$defs/$ALLOW_ALL_COMPONENTS" or a restricted anyOf. |
$ALLOW_ALL_COMPONENTSis generated when you runvtex 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 | $singleton | identifierKeys | Editor experience | Data Plane lookup |
|---|---|---|---|---|
| Single page (Home) | true | [] | One entry, edited in place | By Content Type name |
| Multi-instance page | false | ["slug"] | Many entries, each with a slug | By slug |
| Multi-key identity | false | ["slug", "locale"] | Rare. Only when multiple keys identify an entry | By 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
| Property | Mechanism | Behavior |
|---|---|---|
seo | $ref to #/components/SEO | Every 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_COMPONENTS | Editors add any registered component. Each item in the array includes a componentKey at publish time. |
heroImage | media-gallery widget | Stores 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.