Unified Content

Unified Content Model (UCM)

A standardized JSON schema abstraction for content models across CMS platforms.

Define once. Deploy anywhere.

Supported platforms: Contentful, ContentStack, Sanity, Storyblok, Umbraco Planned: Optimizely, Sitecore

The Problem

Content management systems vary significantly in their data models, APIs, and capabilities.

  • Migrating content between CMS platforms is painful
  • Building omnichannel experiences across multiple systems is complex
  • Maintaining consistent content structures requires duplication of effort
  • Vendor lock-in limits flexibility and future-proofing

The UCM Ecosystem

Spec Name Purpose
UCM Unified Content Model Define content types (schemas)
UCF Unified Content Format Store content instances (data)
UCR Unified Content Retrieval Fetch content with resolution (API)
UCN Unified Content Notifications React to content events (webhooks)

Key benefits:

  • Portability — Define content models once, deploy anywhere
  • Migration — Automated content and schema migration between CMS platforms
  • Consistency — Unified content modeling approach across teams and projects
  • Future-Proofing — Adapt to new CMS platforms without rewriting schemas

Configuration — .cmsconfig

A single config file switches between CMS platforms. Just change CMS_PLATFORM and provide the matching credentials:

CMS_PLATFORM=contentful
MODELS_PATH=./models
DEFAULT_LOCALE=en-US

# Contentful
CONTENTFUL_SPACE_ID=a1b2c3d4e5
CONTENTFUL_ACCESS_TOKEN=CFPAT-xxxxxxxxxxxxxxxxxxxx
CONTENTFUL_ENVIRONMENT=master

Switch to another CMS by changing the platform and keys:

CMS_PLATFORM=umbraco
UMBRACO_BASE_URL=https://your-instance.euwest01.umbraco.io
UMBRACO_API_KEY=xxxxxxxxxxxxxxxxxxxx

All CLI commands (ucm sync, ucm pull, ucm content push, etc.) read from .cmsconfig automatically.

UCM Schema Structure

Every unified content type is a JSON Schema (draft-07) file:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "id": "content-card",
  "title": "Content Card",
  "description": "A polymorphic card component",
  "metadata": {
    "localized": true,
    "version": "1.0.0",
    "displayField": "headline"
  },
  "structure": {
    "allowedAsRoot": true,
    "folder": "Cards"
  },
  "type": "object",
  "properties": {
    "headline": {
      "type": "string",
      "title": "Headline"
    },
    "summary": {
      "type": "text",
      "title": "Summary"
    },
    "category": {
      "type": "string",
      "title": "Category"
    },
    "data": {
      "type": "object",
      "title": "Card Data",
      "ui": { "widget": "web-config-card" }
    }
  }
}

Unified Type System

Core Types

Unified Type Description Example Use
string Single-line text Titles, names
text Multi-line text Descriptions, notes
number Numeric values Quantities, ratings
boolean True/false Enable/disable flags
date Date-only Publication dates
datetime Date and time Event timestamps
url URL strings Links
richtext Formatted text Content bodies
media File references Images, documents
reference Content links Related articles
array Ordered lists Multiple items
object Arbitrary JSON Settings, config
select Choice options Dropdowns

Specialized Types

Unified Type Description CMS Support
geopoint Geographic coordinates 6/7 platforms
slug URL-safe identifiers 7/7 platforms
color Color values (hex, RGB) 7/7 platforms
tags Tag collections 7/7 platforms

Geopoint Example

{
  "type": "geopoint",
  "title": "Location",
  "description": "Geographic coordinates for mapping",
  "validation": { "required": false }
}

Adapter maps to Location (Contentful), geopoint (Sanity), Coordinate (Sitecore), or JSON object (others).

Content Organization with structure

The structure property controls how content types appear and behave in CMS interfaces:

{
  "structure": {
    "allowedAsRoot": true,
    "folder": "Layouts",
    "allowedChildContentTypes": ["content-card"],
    "layout": [
      {
        "area": "main",
        "type": "group",
        "name": "Identity & Business",
        "description": "Basic site configuration",
        "collapse": false,
        "fields": ["site_name", "site_url", "contact_email"]
      },
      {
        "area": "main",
        "type": "group",
        "name": "SEO & Social",
        "collapse": true,
        "fields": ["og_image", "twitter_card", "twitter_site"]
      }
    ]
  }
}
  • Contentful: Maps folder to content type groups, layout to editor interface tabs
  • Umbraco: Maps folder to document type containers, layout to property groups/tabs

Widget Registry

Widgets map custom CMS editors to unified schema fields via widget-registry.json:

{
  "widgets": {
    "web-config-card":     { "contentful": "1BNE...", "umbraco": "BrowserStyle.WebConfigCard" },
    "web-config-csp":      { "contentful": "13dq...", "umbraco": "BrowserStyle.WebConfigCSP" },
    "web-config-manifest": { "contentful": "7bFH...", "umbraco": "BrowserStyle.WebConfigManifest" },
    "web-config-robots":   { "contentful": "7Brx...", "umbraco": "BrowserStyle.WebConfigRobots" },
    "web-config-security": { "contentful": "1pEB...", "umbraco": "BrowserStyle.WebConfigSecurity" },
    "web-config-alt":      {                          "umbraco": "BrowserStyle.WebConfigAlt" }
  }
}

Usage in Schema

{
  "csp": {
    "type": "object",
    "title": "Content Security Policy",
    "ui": { "widget": "web-config-csp" }
  }
}

The adapter reads the widget name, looks up the CMS-specific editor ID, and configures the property editor accordingly.

CLI: Schema Operations

Validate Schemas

ucm validate                          # Validate all schemas
ucm validate models/page.schema.json  # Validate specific file

Sync to CMS

ucm sync                                      # Push all schemas to CMS
ucm sync --file models/content-card.schema.json  # Push single schema

Pull from CMS

ucm pull                    # Pull schemas from CMS
ucm pull --output ./backup  # Pull to custom directory

CLI: More Schema Commands

Diff (Compare Local vs CMS)

ucm diff                    # Show differences between local and remote

Clean (Remove from CMS)

ucm clean                   # Delete content types from CMS
ucm clean --inverse         # Delete types NOT in local models

Backup

ucm backup                  # Backup CMS content types locally

Common Flags

Flag Description
-m, --model <model> Filter by content model
-d, --dry-run Preview without applying
-v, --verbose Detailed output

Unified Content Format (UCF)

While UCM defines content types (schemas), UCF defines content instances (the actual data).

UCF enables:

  • Cross-platform content migration — Move content between CMS platforms
  • Content backup and restore — Version-controlled content outside the CMS
  • Multi-environment sync — Push the same content to dev, staging, and production
  • CMS-agnostic workflows — Edit content locally, deploy anywhere

UCF File Structure

{
  "$schema": "../../models/article.schema.json",
  "id": "getting-started-with-ucm",
  "model": "article",
  "meta": {
    "createdAt": "2026-01-10T09:00:00Z",
    "updatedAt": "2026-01-12T14:30:00Z",
    "locale": "en-US",
    "status": "published",
    "contentful": {
      "entryId": "3Kt1Th8vZ4TrBNzwFjMUXW",
      "version": 3
    }
  },
  "fields": {
    "title": "Getting Started with UCM",
    "slug": "getting-started-with-ucm",
    "summary": "Learn how to use the Unified Content Model.",
    "body": {
      "$richtext": true,
      "content": "<h2>Introduction</h2><p>Welcome to UCM...</p>",
      "format": "html"
    },
    "author": { "$ref": "author/jane-doe" },
    "heroImage": { "$asset": "assets/ucm-hero", "assetId": "ucm-hero" },
    "tags": ["tutorial", "getting-started"],
    "featured": true
  }
}

UCF Special Syntax

Content References ($ref)

{
  "author": { "$ref": "author/jane-doe" },
  "relatedArticles": [
    { "$ref": "article/getting-started" },
    { "$ref": "article/advanced-usage" }
  ]
}

Composite key format: {model}/{content-id} -- resolved via manifest during push.

Media References, Rich Text

{
  "heroImage": { "$asset": "assets/hero-banner", "assetId": "hero-banner" }
}
{
  "body": {
    "$richtext": true,
    "content": "<p>This is <strong>rich</strong> text.</p>",
    "format": "html"
  }
}

UCF: Manifest & Reference Resolution

The manifest tracks CMS-specific IDs and content dependencies:

{
  "environments": {
    "umbraco:production": {
      "article/getting-started": "e9a2c384-7d2f-...",
      "author/jane-doe": "4b63de2b-f7fc-..."
    },
    "contentful:master": {
      "article/getting-started": "3Kt1Th8vZ4TrBNzwFjMUXW",
      "author/jane-doe": "6eQyQIsfPhaKAiwoJasEE6"
    }
  },
  "dependencies": {
    "article/getting-started": ["author/jane-doe"]
  },
  "restoreOrder": ["author/jane-doe", "article/getting-started"]
}

References are resolved to CMS-native formats automatically:

  • Contentful: { "sys": { "type": "Link", "linkType": "Entry", "id": "..." } }
  • Umbraco: { "udi": "umb://document/..." }

UCF: Locale Handling

Multi-locale content through locale-suffixed filenames:

.ucm/content/article/
  getting-started.json        <- en-US (default)
  getting-started.da.json     <- Danish
  getting-started.de-DE.json  <- German

Multiple locale files map to the same CMS entry -- just different locale slots.

CLI Commands

ucm content pull                           # Pull default locale
ucm content pull --locale da               # Pull specific locale
ucm content pull --all-locales             # Pull all locales
ucm content push                           # Push all locale files
ucm content push --dry-run                 # Preview without applying

UCF: Content CLI Commands

Pull Content

ucm content pull                           # Pull all content
ucm content pull --model article           # Pull specific model
ucm content pull --model article --id home # Pull specific item
ucm content pull --with-assets             # Include media files

Push Content

ucm content push                           # Push all content
ucm content push --model article           # Push specific model
ucm content push --dry-run                 # Preview changes
ucm content push --yes                     # Skip confirmation

Cross-CMS Migration

CMS_PLATFORM=umbraco    ucm content pull --output ./migration/from-umbraco
CMS_PLATFORM=contentful ucm content push --input  ./migration/from-umbraco

Unified Content Retrieval (UCR)

A platform-agnostic API for fetching content from any CMS with automatic reference resolution.

const content = await fetchContent(model, id, depth);
  • model -- Any content type (e.g. page, layout, content-card)
  • id -- Content identifier (slug, ID, or path)
  • depth -- false returns reference IDs, true resolves all nested content

Supported Platforms

Platform Status
Contentful Full
Umbraco Full
Storyblok Full
Contentstack Full
Sanity Planned

UCR: The depth Attribute

Without depth (default) -- references as IDs:

{
  "id": "home", "model": "page",
  "title": "Home Page",
  "layouts": [
    { "$ref": "layout/hero-layout" },
    { "$ref": "layout/features-layout" }
  ]
}

With depth -- all references resolved inline:

{
  "id": "home", "model": "page",
  "title": "Home Page",
  "layouts": [{
    "id": "hero-layout", "model": "layout",
    "title": "Hero Section",
    "content": [{
      "id": "news-1", "model": "content-card",
      "headline": "The Future of Quantum Computing",
      "data": { "type": "news" }
    }]
  }]
}

UCR: Same Content, Different CMS

Contentful and Umbraco return identical normalized content:

From Contentful:

{
  "id": "perfect-chocolate-chip-cookies",
  "model": "content-card",
  "headline": "Perfect Chocolate Chip Cookies",
  "summary": "The ultimate guide to baking soft, chewy...",
  "category": "Recipe",
  "data": {
    "type": "recipe",
    "content": { "readingTime": "Prep time: 45 min" },
    "ribbon": { "text": "Popular", "style": "popular" },
    "engagement": { "reactions": [{ "type": "like", "count": 530 }] }
  }
}

From Umbraco:

{
  "id": "recipe/recipe-3",
  "model": "content-card",
  "headline": "Perfect Chocolate Chip Cookies",
  "summary": "The ultimate guide to baking soft, chewy...",
  "category": "Recipe",
  "data": {
    "type": "recipe",
    "content": { "readingTime": "Prep time: 45 min" },
    "ribbon": { "text": "Popular", "style": "popular" },
    "engagement": { "reactions": [{ "type": "like", "count": 530 }] }
  }
}

Same shape. Same data. Different CMS. One API.

Unified Content Notifications (UCN)

A platform-agnostic webhook listener that normalizes CMS events into a unified format.

CMS Webhook --> UCN Server --> Normalizer --> Unified Event --> Handlers

Unified Event Format

{
  "event": "content.published",
  "platform": "contentful",
  "model": "content-card",
  "entryId": "abc-456",
  "locale": "en-US",
  "timestamp": "2026-02-16T12:00:00Z"
}

Event Types

Event Typical Use
content.published Vectorize for AI search, rebuild sitemap
content.unpublished Remove from sitemap and search
content.deleted Remove from search index
content.draft Preview builds
media.uploaded Generate alt text, optimize images
schema.updated Update search mappings

UCN: Use Cases

AI Search Vectorization

// handlers/vectorize.js
export default async function handle(event) {
  if (event.event === 'content.deleted') {
    await searchIndex.remove(event.model, event.entryId);
    return;
  }
  const content = await fetchContent(event.model, event.entryId, true);
  await searchIndex.upsert(content);
}

Sitemap Regeneration

// handlers/sitemap.js
export default async function handle(event) {
  if (!event.event.startsWith('content.')) return;
  await fetch(SITEMAP_REBUILD_URL, {
    method: 'POST',
    body: JSON.stringify({ reason: event.event, model: event.model })
  });
}

Configuration

{
  "handlers": {
    "*": ["./handlers/logger.js"],
    "content.published": ["./handlers/vectorize.js", "./handlers/sitemap.js"],
    "content.deleted": ["./handlers/vectorize.js", "./handlers/sitemap.js"]
  }
}

Summary

Component What it does
UCM Define content types once -- deploy to any CMS
UCF Store and migrate content instances across platforms
UCR Fetch content with a single API, regardless of CMS
UCN React to content events from any CMS via webhooks

One schema. Any CMS. Full portability.