Documentation
Feedback
Guides
API Reference

Guides
Guides
CMS
Integrations

Content modeling and architecture for headless stores

Learn how to model and consume CMS content in headless storefronts: JSON Schema structure, Content Types, components, and Data Plane API consumption patterns.

Content modeling defines what can be created on the CMS, and the architecture defines how that content moves from your repository to the CMS Admin and, after publishing, to your storefront.

This guide details both layers for headless integrations, covering core concepts, design principles, JSON Schema rules, consumption patterns, and recommended Content Type structures.

For platform architecture (CQRS, schema upload lifecycle, file organization), see Understanding CMS architecture and schema declarations.

Before you begin

Before modeling content for a headless store, you need three foundations in place.

JSON Schema basics

The CMS uses JSON Schema to declare every Content Type, component, and field. You don't need to master the full specification, but you should be comfortable with:

ConceptRole in the CMS
typeDeclares the data shape (string, number, boolean, object, array).
propertiesLists the fields inside an object.
requiredIndicates which fields are required before content can be saved.
itemsDefines the shape of each element in an array.
$refPoints to another definition in the same schema bundle.

A minimal field declaration looks like this:


_10
"title": {
_10
"title": "Title", // Label shown in the CMS Admin
_10
"type": "string",
_10
"description": "Main heading displayed on the page"
_10
}

Learn more in the JSON Schema instructions guide.

Content plugin (CLI)

Schemas are authored as .jsonc files in your project and uploaded through the Content plugin (@vtex/cli-plugin-content):


_10
vtex plugins install @vtex/cli-plugin-content

Typical workflow:

  1. Author component and Content Type files in your repository.
  2. Run vtex content generate-schema to produce a unified bundle.
  3. Run vtex content upload-schema to publish it to the Schema Registry.

For headless stores, the generate-schema command requires two extra arguments: the paths to your component and Content Type directories, and --base vtex.headless:


_10
vtex content generate-schema <components-dir> <pages-dir> --out <output-file> --base vtex.headless

For example:


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

Headless store setup

RequirementDetails
CMS enabledCMS Admin available in your VTEX account.
Headless storeA store created with storefront type Headless in Storefront > Content.
Frontend integrationAn app that fetches published entries from the Data Plane API and maps componentKey values to UI components.

Your team owns rendering, preview wiring, and CI/CD. The CMS provides schemas, authoring, and published content delivery.

Key concepts

TermDefinition
SchemaA versioned bundle that defines all Content Types and components for your store. Identified as account.name@version (for example, mystore.myheadlessstore@1.0.0) and stored in the Schema Registry. Contains components, content-types, optional $defs, and a $base reference to vtex.headless.
Content TypeA page-level template, for example, Home or Landing Page. Defined under the content-types key. You can create entries from Content Types.
EntryOne content instance of a Content Type (for example, a landing page with slug summer-sale).
ComponentA reusable building block, for example, a banner or navigation bar. Defined under the components key. Can appear on multiple Content Types.
SectionA component that can be added to a page through a Content Type's sections array. See Understanding components and sections.
FieldA single property inside a Component or Content Type (string, number, boolean, object, or array), defined in properties.
Base schemaA shared template in $defs (or inherited via $base). Other definitions reuse it through $extends to avoid duplication.

How Content Types and components relate

A Content Type describes the shape of a page. Components are the building blocks you arrange inside it.

Relationship in practice:

LayerDeclaresExample
Content TypePage structure and which components are availablelandingPage with slug, seo, and sections
ComponentReusable block with its own fieldsPromoBanner with title, image, link
FieldAtomic editable valuetitle (string), image.src (string + media widget)

A Content Type holds fixed fields (always present on every entry) and a dynamic sections array (you choose which components appear and in what order).

Using a Content Type or a component

ScenarioContent TypeComponent
Represents a routable page
Needs multiple instances (many landing pages)
Only one instance store-wide✅ ($singleton: true)
Reusable UI block on one or more pages
Nested inside another component
Needs a slug or identifier field
Shared across the store (header, footer)✅ (singleton Content Type)✅ (component inside it)

If it has a URL, model it as a Content Type. If it renders a block on a page, model it as a component.

Design principles

Well-structured schemas reuse definitions instead of duplicating fields. The CMS supports three complementary mechanisms.

Base schemas

LevelMechanismPurpose
Platform"$base": "vtex.headless"Inherits the headless platform bundle when you upload your store schema.
Component"$extends": ["#/$defs/base-component"]Inherits shared component structure from your base bundle, when available.
Content Type"$extends": ["#/$defs/base-page-template"]Shares common page properties across Content Types when you define a template.

The vtex.headless base provides core platform definitions — not a full page library. You add the components and content-types your storefront needs.


_10
// Store schema bundle (simplified)
_10
{
_10
"$id": "youraccount.yourstore@1.0.0",
_10
"$base": "vtex.headless",
_10
"components": { /* components you define */ },
_10
"content-types": { /* Content Types you define */ }
_10
}

Definitions such as base-component or base-page-template may be provided by your base bundle. Check the merged output of vtex content generate-schema before referencing them with $extends.

Extension ($extends)

$extends inherits properties from one or more definitions in the same bundle. Child properties override parent properties with the same name.


_11
{
_11
"$extends": ["#/$defs/base-component"],
_11
"$componentKey": "PromoBanner",
_11
"$componentTitle": "Promotional Banner",
_11
"properties": {
_11
"discountPercentage": {
_11
"title": "Discount %",
_11
"type": "number"
_11
}
_11
}
_11
}

Use $extends when multiple components share fields (dates, color variants, link objects) or when Content Types share a common page skeleton.

Referencing ($ref)

$ref embeds or reuses a definition without copying it:

PatternExampleUse when
Embed a component"seo": { "$ref": "#/components/SEO" }Every entry of a Content Type needs the same block.
Open section picker"sections": { "$ref": "#/$defs/$ALLOW_ALL_COMPONENTS" }You need to add any component you registered.
Restricted sections"sections": { "type": "array", "items": { "anyOf": [...] } } }Only certain components are allowed on a page.

_10
"seo": {
_10
"$ref": "#/components/SEO"
_10
}
_10
_10
"sections": {
_10
"$ref": "#/$defs/$ALLOW_ALL_COMPONENTS"
_10
}

$ALLOW_ALL_COMPONENTS is generated when you run vtex content generate-schema. It lists every component in your bundle as an anyOf array. Reference it in Content Types. Do not hand-author this definition in individual .jsonc files.

JSON Schema fundamentals

Supported draft

The CMS uses JSON Schema draft 2019-09 with a VTEX-specific vocabulary for CMS keywords and widgets.

CMS-specific keywords

These keywords extend standard JSON Schema. They're resolved when you upload a bundle.

KeywordApplies toPurpose
$baseSchema bundleInherits the vtex.headless platform schema.
$extendsComponent, Content TypeInherits properties from other definitions in the bundle.
$componentKeyComponentUnique identifier used in API responses and frontend mapping.
$componentTitleComponentDisplay name in the CMS Admin.
$singletonContent TypeLimits the Content Type to a single entry (for example, home, globalHeader).
$abstractComponentMarks a template-only component that you can’t add to pages.
identifierKeysContent TypeFields that uniquely identify entries (for example, ["slug"]).
widgetFieldControls the Admin UI control (ui:widget).
enumNamesFieldHuman-readable labels for enum values.

For schema file naming and upload workflow, see Understanding CMS architecture and schema declarations.

Field types and schema mappings

Field typeJSON SchemaDescription
Short text{ "type": "string" }Default text input.
Long textstring + "ui:widget": "text-area"Multi-line input.
URL slugstring + "ui:widget": "slug"Normalized with / prefix.
Number{ "type": "number" } or "integer"Integers for counts and limits.
Toggle{ "type": "boolean" }Checkbox.
Selectenum + optional enumNamesDropdown.
Date and timestring + "ui:widget": "date-time"Stored as ISO 8601 string.
Image or videostring + "ui:widget": "media-gallery"References Media Gallery assets.
Rich textstring + "ui:widget": "draftjs-rich-text"Formatted text stored as a JSON string.
Group of fields{ "type": "object", "properties": {…} }Nested form section.
List of items{ "type": "array", "items": {…} }Repeatable blocks (footer links, nav items).

Validation rules and constraints

Standard JSON Schema validation runs when you save content. Common constraints:

ConstraintExampleEffect
required"required": ["title", "slug"]Named fields must be completed.
minLength / maxLength"minLength": 10 on a slugEnforces string length.
minimum / maximum"maximum": 5 on item countBounds numeric values.
minItems / maxItems"minItems": 1, "maxItems": 8 on a link arrayLimits array size.
enum"enum": ["primary", "secondary"]Restricts to allowed values.
default"default": "primary"Pre-fills new entries.

Invalid data is rejected when saving, before the content is published.

Consuming content

After you upload schemas and publish content, your headless storefront reads published entries from the Data Plane API. The schema you defined shapes the JSON your app receives: field names, section structure, and componentKey values.

For the full lifecycle (schema upload, authoring, publishing, sync), see Understanding CMS architecture and schema declarations.

Your storefront owns:

  • Routing: Mapping URLs to Content Types and slugs.
  • Locale: Passing the locale query parameter when fetching entries.
  • Rendering: Mapping each componentKey to a UI component in your framework.

The sections below cover the most common consumption patterns.

Fetch a page by route or slug

Use this pattern when a URL arrives at your frontend, and you need to load the corresponding CMS entry. Pass the slug as a path segment after entries/slug/. Multi-segment slugs (for example, en/promo) are supported.


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/{contentType}/entries/slug/summer-sale

Response shape (simplified):


_11
{
_11
"componentKey": "landingPage",
_11
"slug": "summer-sale",
_11
"sections": [
_11
{
_11
"componentKey": "PromoBanner",
_11
"title": "Summer Sale",
_11
"image": { "src": "https://...", "alt": "Banner" }
_11
}
_11
]
_11
}

To request a localized version, add the locale query parameter:


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/landingPage/entries/slug/summer-sale?locale=pt-BR

Fetch all entries of a content type

Use this pattern to build listing pages (for example, a blog index or a campaign directory), or to pre-render all entries at build time.


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/blogPost/entries

Response shape (simplified):


_17
{
_17
"entries": [
_17
{
_17
"id": "abc123",
_17
"name": "Summer trends",
_17
"createdAt": "2024-06-01T00:00:00Z",
_17
"updatedAt": "2024-06-15T12:00:00Z"
_17
},
_17
{
_17
"id": "def456",
_17
"name": "Back to school guide",
_17
"createdAt": "2024-07-01T00:00:00Z",
_17
"updatedAt": "2024-07-10T09:00:00Z"
_17
}
_17
],
_17
"scroll": "eyJzb3J0IjoidXBkYXRlZEF0Iiw..."
_17
}

By default, only entry metadata is returned. To include the full content blob for each entry, add ?content=all:


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/blogPost/entries?content=all

Paginate through entries

The Data Plane API uses scroll-based pagination. Each response returns a scroll token when more entries exist. Pass it as the scroll query parameter to fetch the next page. Each page contains up to 20 entries.

First request:


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/blogPost/entries


_10
{
_10
"entries": [ /* 20 entries */ ],
_10
"scroll": "eyJzb3J0IjoidXBkYXRlZEF0Iiw..."
_10
}

Next page:


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/blogPost/entries?scroll=eyJzb3J0IjoidXBkYXRlZEF0Iiw...

When scroll is absent from the response, you have reached the last page.

You can also control sort order:


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/blogPost/entries?sort=createdAt&order=asc

Fetch singleton or global content

Singleton Content Types (defined with "$singleton": true) have exactly one entry store-wide — no slug is needed. Fetch them by listing the Content Type's entries and reading the first (and only) result. This is the standard pattern for headers, footers, and global navigation.


_10
GET https://{account}.vtexcommercestable.com.br/api/content-platform/data/{account}/{storeId}/globalHeader/entries?content=all


_20
{
_20
"entries": [
_20
{
_20
"id": "xyz789",
_20
"name": "Global Header",
_20
"blobContent": {
_20
"componentKey": "globalHeader",
_20
"sections": [
_20
{
_20
"componentKey": "SiteHeader",
_20
"logo": { "src": "https://...", "alt": "My Store" },
_20
"links": [
_20
{ "label": "Sale", "href": "/sale" }
_20
]
_20
}
_20
]
_20
}
_20
}
_20
]
_20
}

Map componentKey to UI components

Every section object in an API response includes a componentKey that identifies which component to render. Your frontend maps these keys to actual UI components.

A typical implementation in React:


_23
import { PromoBanner } from '@/components/PromoBanner'
_23
import { SiteHeader } from '@/components/SiteHeader'
_23
import { RichText } from '@/components/RichText'
_23
_23
const COMPONENT_MAP: Record<string, React.ComponentType<unknown>> = {
_23
PromoBanner,
_23
SiteHeader,
_23
RichText,
_23
}
_23
_23
function Sections({ sections }: { sections: { componentKey: string; [key: string]: unknown }[] }) {
_23
return (
_23
\<\>
_23
{sections.map((section, index) => {
_23
const Component = COMPONENT_MAP[section.componentKey]
_23
if (!Component) {
_23
return null
_23
}
_23
return <Component key={index} {...section} />
_23
})}
_23
</>
_23
)
_23
}

Each component receives the full section object as props, so field names in your JSON Schema map directly to the props your component expects. If a componentKey isn't in your map, the section is silently skipped — this is a safe default during incremental rollout.

The patterns below are common conventions for headless commerce storefronts. Names, components, and routes are yours to define. Nothing in this section exists until you add it to your schema bundle.

Partial and reusable Content Types

Some content applies across every page rather than to a single route.

PatternSuggested Content Type nameComponents you might definePurpose
HeaderglobalHeaderSiteHeader, AnnouncementBarTop navigation, logo, utility links.
FooterglobalFooterSiteFooterLinks, social icons, copyright.
NavigationInside a header componentLink arrays, menu itemsMain menu is maintained once.
BannersOn page Content TypesPromoBanner, TextBannerPromotional blocks per page.

_12
// cms/pages/cms_content_type__globalHeader.jsonc
_12
{
_12
"title": "Global Header",
_12
"type": "object",
_12
"$singleton": true,
_12
"identifierKeys": [],
_12
"properties": {
_12
"sections": {
_12
"$ref": "#/$defs/$ALLOW_ALL_COMPONENTS"
_12
}
_12
}
_12
}

Fetch singleton entries by Content Type name (no slug) and wrap every page layout with the returned sections.

Navigation is usually a component inside a header singleton, not its own page Content Type. Banners are components on page-level Content Types. Each page chooses its own banner stack.

Page Content Types

Page type$singletonidentifierKeysComponents you might useExample route
Hometrue[]Promo banners, featured collections./
Landing pagefalse["slug"]Banners, rich text, CTA./{slug}
PLPfalse["slug"]Breadcrumb, layout config./category/{slug}
PDPfalse["slug"]Product layout, cross-sell blocks./product/{slug}
Blog categoryfalse["slug"]Category header, article list./blog/{slug}
Blog postfalse["slug"]Article body, related content./blog/post/{slug}

Home: One entry, no slug:


_10
{
_10
"title": "Home",
_10
"type": "object",
_10
"$singleton": true,
_10
"identifierKeys": [],
_10
"properties": {
_10
"seo": { "$ref": "#/components/SEO" },
_10
"sections": { "$ref": "#/$defs/$ALLOW_ALL_COMPONENTS" }
_10
}
_10
}

Landing page: Many entries, slug required:


_14
{
_14
"title": "Landing Page",
_14
"type": "object",
_14
"$singleton": false,
_14
"identifierKeys": ["slug"],
_14
"properties": {
_14
"slug": {
_14
"title": "Slug",
_14
"type": "string",
_14
"widget": { "ui:widget": "slug" }
_14
},
_14
"sections": { "$ref": "#/$defs/$ALLOW_ALL_COMPONENTS" }
_14
}
_14
}

PLP and PDP: The CMS entry controls layout and display configuration. Product data (catalog, prices, stock) still comes from VTEX commerce APIs.

Blog: Define blogCategory and blogPost Content Types when your storefront needs editorial content:


_29
// cms/pages/cms_content_type__blogPost.jsonc
_29
{
_29
"title": "Blog Post",
_29
"type": "object",
_29
"$singleton": false,
_29
"identifierKeys": ["slug"],
_29
"properties": {
_29
"slug": {
_29
"title": "Slug",
_29
"type": "string",
_29
"widget": { "ui:widget": "slug" }
_29
},
_29
"title": { "title": "Title", "type": "string" },
_29
"body": {
_29
"title": "Body",
_29
"type": "string",
_29
"widget": { "ui:widget": "draftjs-rich-text" }
_29
},
_29
"sections": {
_29
"type": "array",
_29
"items": {
_29
"anyOf": [
_29
{ "$ref": "#/components/RelatedArticles" },
_29
{ "$ref": "#/components/AuthorBio" }
_29
]
_29
}
_29
}
_29
}
_29
}

Restricting sections with a custom anyOf (instead of $ALLOW_ALL_COMPONENTS) keeps commerce components off article pages.

Understanding CMS architecture and schema declarations
Learn about CQRS, schema file organization, and the content lifecycle.
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