Variation #2Continue reading...

Developer Demo

External Content Sync

Push any external data source directly into Optimizely Graph without touching the CMS. Define a schema once via the Content Source sync API, send NdJSON over HTTP, and your data is instantly queryable alongside CMS-managed content — same GraphQL endpoint, same ISR caching.

◎ Demo data — run npx tsx scripts/seed-referrals.tsGraph Content Source APINdJSON · PUT types · POST dataISR · 60s + on-demand tag

Live Example — Referrals #

Each card is a Referral item synced from an external source via the _Item base type. Custom properties (name, comment) hold application data and are queried directly.

Sarah Chen

Switched our content team from Contentful to Optimizely SaaS CMS. The Visual Builder makes it trivial for editors to create new pages without any engineer support.

Marcus Webb

The @recursive GraphQL directive saved us three days of nav implementation work. Fetches 5 levels of nested content in a single round-trip.

Aisha Okafor

Optimizely Graph's ISR plus on-demand revalidation gives us the best of both worlds — fast static pages that update the moment editors publish.

Tom Hartley

We migrated our product catalog to the Graph Content Source API. Data syncs from our PIM in real-time and is immediately queryable via GraphQL.

Priya Sharma

Feature Experimentation and CMS in one platform is a genuine game changer. We A/B test content variations without ever leaving the same toolchain.

Daniel Reeves

Preview mode was clean to implement. Pass a previewToken, swap to no-store caching, and editors see unpublished changes instantly in the live app.

Querying External Content #

Once synced, external content is queried exactly like CMS-managed content — same GraphQL endpoint, same ISR caching. Query your custom properties directly; _itemMetadata fields are search-indexed internals and return null at query time.

query GetReferrals {
    Referral(limit: 100, orderBy: { name: { value: ASC } }) {
      items {
        name
        comment
      }
    }
  }

Sync Paths — Getting Data into Graph #

Four paths exist for pushing external data into Optimizely Graph. All four end up in the same place — data queryable via GraphQL alongside CMS content — but differ in who owns the pipeline, whether scheduling is managed, and what third-party tooling is involved.

Connecting to CMS — same step for every path

Once data is in Graph, wire it to CMS via Admin → Content Types → Create new… → Connect from Graph. Choose the source ID, schema, a CMS base type (Page, Component, Media, Image, or Video), and the fields to use as the content ID and display name. CMS creates a read-only connected content type editors can reference and browse in the Content Manager. Note: the label is Connect from Graph, not "Import from Graph" — a common source of confusion in the UI.

1

Direct to Graph

Content Source API — NdJSON over HTTP

Register a schema via PUT /api/content/v3/types and push NdJSON records via POST /api/content/v2/data. No third-party tools required. You build and run all sync logic.

SchemaRegistered via API
DataPushed directly to Graph
SyncYour code, your schedule
Infra you ownEverything
Best for: Custom or proprietary data with no off-the-shelf integration. Teams comfortable owning scheduling, retries, and rebuilds.

⚠ Two undocumented-but-required fields: "preset": "next" on the schema and displayName___searchable in _itemMetadata. See Base Type Contracts ↓ for full examples. Additional sources may need to be enabled by Optimizely — contact support if the types endpoint returns a source-limit error.

2

OCP — Free Tier

Public apps + Custom Endpoints (real-time)

Included with SaaS CMS. Ships with public apps for common platforms — each app handles auth, schema registration, and field mapping automatically. If no public app exists, Custom Endpoints (real-time) adds a field-mapping UI over what you'd build for Path 1.

SchemaOCP config + Graph
DataWebhook ingestion
SyncReal-time only (free tier)
Infra you ownYour data pipeline

Public apps (free)

BynderBrandfolderCommercetoolsShopifyWordPressCMP
Best for: Customers using a supported public app — this is the unambiguous recommendation for Bynder, Brandfolder, or Commercetools. For custom data with no matching app, the free tier adds little over Path 1 and doesn't support scheduled syncs from your source.
3

OCP — Paid Tier

Managed staging DB + scheduled syncs

Unlocks the OCP database as a hosted staging layer and scheduled outbound syncs to Graph via the Sync Manager. Data reaches OCP via S3 CSV, REST API, webhook, or an OCP app — from there OCP handles the sync to Graph on your chosen interval.

SchemaOCP database + Graph
DataS3 / API / app → OCP DB → Graph
SyncScheduled or real-time (OCP managed)
Infra you ownLess — OCP hosts staging + intervals
Best for: Customers who want OCP to manage the staging layer and scheduling without building their own pipeline. Also the right path if data needs to flow from one source to multiple OCP destinations.

Paid tier required — confirm pricing with the CSM before scoping.

Reference OCP app: joshuaonwezen/ocp-product-catalog — TypeScript app with KV store, REST API, and bulk + real-time Graph sync.

4

CMP DAM → Graph

For existing CMP customers

For customers already on Content Marketing Platform. CMP DAM assets sync to Graph in real-time via OCP, making them queryable via GraphQL and selectable in CMS via the Browse DAM action on content reference and image properties.

SchemaCMP + Graph (via OCP)
DataCMP DAM → Graph (real-time)
SyncReal-time, OCP-managed
Infra you ownNone — fully managed
Best for: CMP customers who want DAM assets queryable in Graph and selectable in CMS without building a custom pipeline. CMP has a self-serve Settings → Organization → Misc → Enable & Sync button that provisions the end-to-end OCP sync.
CMP subscription required. Renditions are not supported in the CMS Browse DAM action — only original assets.

Recommendation Matrix

Custom data, full control, no managed layer needed

Path 1

Using Bynder, Brandfolder, Commercetools, Shopify, or WordPress

Public app handles auth, schema, and field mapping

Path 2

Already on CMP, want DAM assets in Graph and selectable in CMS

Path 4

Want managed scheduling + staging layer, paid tier acceptable

Confirm pricing with CSM before scoping

Path 3

Free OCP tier + scheduled (not real-time) syncs from source

Free tier only supports real-time push — scheduled source syncs are paid-only

Path 1 or 3

Base Type Contracts #

Graph ships three built-in base type contracts. Inherit from one when registering a content type — it adds the metadata property Graph needs to identify, index, and surface your items. All registrations require "preset": "next" and "useTypedFieldNames": true.

When pushing data, the displayName field inside any metadata object must be written as displayName___searchable. This is because displayName is marked searchable: true in the contract definition — with useTypedFieldNames enabled, Graph appends ___searchable to distinguish full-text indexed fields from plain stored fields in the payload. Fields in _assetMetadata and _imageMetadata are not searchable and keep their original names.

_Item

→ adds _itemMetadata

The base contract for all external items. Use this for structured data without a file attachment — testimonials, product catalog entries, CRM records, referrals.

Type Registration

PUT https://cg.optimizely.com/api/content/v3/types?id=rfl
Content-Type: application/json
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{
  "contentTypes": {
    "Referral": {
      "contentType": ["_Item"],
      "properties": {
        "name":    { "type": "String" },
        "comment": { "type": "String" }
      }
    }
  },
  "preset": "next",
  "useTypedFieldNames": true
}

Data Payload (NdJSON)

POST https://cg.optimizely.com/api/content/v2/data?id=rfl
Content-Type: text/plain
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{"index": {"_id": 1, "language_routing": "en"}}
{
  "_itemMetadata": {
    "key": "ref-1",
    "displayName___searchable": "Referral - Sarah Chen",
    "lastModified": "2026-05-26T00:00:00.000Z",
    "type": "Referral"
  },
  "name": "Sarah Chen",
  "comment": "Switched our content team to Optimizely SaaS CMS...",
  "ContentType": ["Referral"],
  "Status": "Published",
  "Language": { "DisplayName": "English", "Name": "en" },
  "_rbac": { "read": ["Everyone"] }
}

_AssetItem

→ adds _itemMetadata + _assetMetadata

Extends _Item with _assetMetadata — adds fileSize, mimeType, and url. Use for PDFs, videos, audio, or any binary-backed asset from a DAM or CDN.

Type Registration

PUT https://cg.optimizely.com/api/content/v3/types?id=docs
Content-Type: application/json
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{
  "contentTypes": {
    "Document": {
      "contentType": ["_AssetItem"],
      "properties": {
        "title":       { "type": "String" },
        "description": { "type": "String" }
      }
    }
  },
  "preset": "next",
  "useTypedFieldNames": true
}

Data Payload (NdJSON)

POST https://cg.optimizely.com/api/content/v2/data?id=docs
Content-Type: text/plain
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{"index": {"_id": 1, "language_routing": "en"}}
{
  "_itemMetadata": {
    "key": "doc-1",
    "displayName___searchable": "Product Datasheet",
    "lastModified": "2026-05-26T00:00:00.000Z",
    "type": "Document"
  },
  "_assetMetadata": {
    "fileSize": 245760,
    "mimeType": "application/pdf",
    "url": "https://example.com/docs/product-datasheet.pdf"
  },
  "title": "Product Datasheet",
  "description": "Full technical specifications for the Enterprise plan.",
  "ContentType": ["Document"],
  "Status": "Published",
  "Language": { "DisplayName": "English", "Name": "en" },
  "_rbac": { "read": ["Everyone"] }
}

_ImageItem

→ adds _itemMetadata + _assetMetadata + _imageMetadata

Extends _AssetItem with _imageMetadata — adds width and height. Use for images from a DAM or media library where you need dimensions queryable at render time — for example to compute aspect ratios or avoid layout shift. All three metadata objects are required in the payload.

Type Registration

PUT https://cg.optimizely.com/api/content/v3/types?id=photos
Content-Type: application/json
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{
  "contentTypes": {
    "Photo": {
      "contentType": ["_ImageItem"],
      "properties": {
        "altText": { "type": "String" },
        "caption": { "type": "String" }
      }
    }
  },
  "preset": "next",
  "useTypedFieldNames": true
}

Data Payload (NdJSON)

POST https://cg.optimizely.com/api/content/v2/data?id=photos
Content-Type: text/plain
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{"index": {"_id": 1, "language_routing": "en"}}
{
  "_itemMetadata": {
    "key": "photo-1",
    "displayName___searchable": "Product Hero Image",
    "lastModified": "2026-05-26T00:00:00.000Z",
    "type": "Photo"
  },
  "_assetMetadata": {
    "fileSize": 1048576,
    "mimeType": "image/jpeg",
    "url": "https://example.com/images/product-hero.jpg"
  },
  "_imageMetadata": {
    "width": 1920,
    "height": 1080
  },
  "altText": "Optimizely platform dashboard screenshot",
  "caption": "The Visual Builder interface showing a live page edit.",
  "ContentType": ["Photo"],
  "Status": "Published",
  "Language": { "DisplayName": "English", "Name": "en" },
  "_rbac": { "read": ["Everyone"] }
}