Vipin Banka
Jul 31, 2026
visibility 10
star star star star star
(0 votes)

Flat or Nested? Weighing Your Content Modeling Options in Optimizely SaaS CMS

When building a headless site on Optimizely SaaS CMS, one of the earliest and most critical design milestones your team will face is defining your content model.

Unlike traditional monolithic setups where visual layout often dictates data structure, headless modeling requires you to design a clear, reusable contract between two very different groups of people: your content authors (working in the CMS UI) and your developers (writing front-end components).

Because developers are trained to eliminate duplicate code (the "DRY" principle), they often instinctively design highly complex, nested, or consolidated data structures. Authors, on the other hand, require flat, intuitive, and self-explanatory editing interfaces.

When these two instincts collide, tension arises.

Below, we’ll walk through a common, real-world scenario—modeling standard videos versus gated lead-generation videos—and lay out three valid modeling options, complete with their architectural trade-offs, so your team can make a well-informed choice.


The Scenario: One Concept, Two Realities

Let's say your site needs to support videos in two very different business contexts:

  1. Standard Video: An editorial or informational video with autoplay, looping, and visual player controls.

  2. Gated Video: A lead-generation asset that plays a brief preview, pauses at a specific timestamp, and displays an email capture form synced automatically with an external Marketing Automation Platform (MAP).

Feature

Standard Video

Gated Video

Author sets

Video file, Poster image, Title, Autoplay toggle, Loop toggle, Show controls

Video file, Poster image, Title

Sourced from MAP

Not applicable

Form ID, Gate Timestamp (seconds to trigger form)

Front-end behavior

Continuous playback, native player controls

Pauses playback at Gate time and overlays lead capture form

To model this, your team has three distinct structural approaches to consider. Let's look at the trade-offs of each.


Option 1: The Consolidated "Mega-Block" (All Fields in One Type)

This approach creates a single, highly flexible content type—such as VideoComponent—containing every possible property. You then rely on the author's training (or inline help text) to know which fields to fill.

Consolidated Block: VideoComponent
├── VideoFile
├── PosterImage
├── Title
├── Autoplay         (Fill only for Standard)
├── Loop             (Fill only for Standard)
├── ShowControls     (Fill only for Standard)
├── FormId           (Fill only for Gated)
└── GateTimestamp    (Fill only for Gated)

The Trade-Offs

  • Pros:

    • Simple Setup: Only one content type to define, register, and sync to the CMS.

    • Single Component: Developers maintain just one straightforward React component with simple conditional rendering.

  • Cons:

    • Author Cognitive Load: Authors are forced to look at "dead fields." An editor adding a gated marketing video must mentally ignore Autoplay, Loop, and Controls.

    • Validation Gaps: You cannot easily make fields like FormId required for gated videos without making them required for standard videos too, risking incomplete data.


Option 2: The Nested "Wrapper Block" (Composition via Content Area)

This approach seeks to avoid database field duplication by pulling the shared properties (VideoFile, PosterImage, and Title) into a parent VideoItem block. This block then contains a nested Content Area where the author inserts a secondary "extension block" containing the specific properties.

Parent Block: VideoItem
├── VideoFile
├── PosterImage
├── Title
└── Content Area (Allows only StandardProperties OR GatedProperties)

The Trade-Offs

  • Pros:

    • Strictly "DRY" Database Schema: The base fields are declared exactly once, preventing schema duplication.

    • Modularity: You can reuse the "Standard" or "Gated" property blocks in other components across the system.

    • Configurable guardrails: Content Areas are not a free-for-all. Built-in validation fields let you constrain them declaratively—allowedTypes / restrictedTypes restrict exactly which extension types can be dropped in, and maxLength: 1 caps the area to a single item—so wrong-type and duplicate insertions are preventable without any custom code.

  • Cons:

    • Editing Friction: Authors must execute a two-step process to build a single video. They fill out the parent, click "Create New Block" inside the Content Area, and then fill out the child block—slower than picking one flat type up front.

    • The "empty wrapper" gap: While allowedTypes and maxLength handle type and cardinality, enforcing a strict minimum of one item is not cleanly supported for content areas—so an author can still save an empty wrapper with no extension inside.

    • GraphQL Query Noise: Even when correctly constrained, every query to fetch a video must navigate a nested fragment structure, increasing the payload and query complexity compared to a flat type.


Option 3: The Split-Type "Symmetrical" Pattern (Recommended)

This approach splits the concept into two distinct, flat content types: StandardVideo and GatedVideo. The author's choice of which component to insert in their page layout is the disambiguation.

To keep things clean, we use Optimizely's built-in features to resolve the duplication: Property Groups for authors, and SDK Contracts for developers.

Standard Video (Type 1)
├── [Video Settings Group]   ← via SDK Contract
│     ├── VideoFile
│     ├── PosterImage
│     └── Title
└── [Player Options Group]
      ├── Autoplay, Loop, ShowControls

Gated Video (Type 2)
├── [Video Settings Group]   ← via SDK Contract
│     ├── VideoFile
│     ├── PosterImage
│     └── Title
└── [Lead Capture Group]
      ├── FormId, GateTimestamp

The Trade-Offs

  • Pros:

    • Excellent Author UX: Editors see only the fields that are directly relevant to what they are creating. No noise, no confusion, and the choice happens up front in a single step.

    • Precise Validation: You can make FormId strictly required on Gated Videos and VideoFile required on both, with no overlaps—and no "empty wrapper" edge case to guard against.

    • Evolvable Schemas: If you need to add an "External Tracker ID" to Gated Videos later, you add it to the Gated type. The Standard Video type remains completely untouched, preventing regressions.

    • Flat GraphQL queries: Each type resolves directly, with no nested content-area fragment to traverse.

  • Cons:

    • Minor Code Duplication: In some architectures, this might mean registering two types instead of one (though Optimizely's SDK makes this incredibly clean, as shown below).


Implementing Option 3 Cleanly with the Content JS SDK

⚠️ Implementation Disclaimer: The following code snippets are illustrative implementations demonstrating the structure and APIs of the @optimizely/cms-sdk. Depending on your active SDK version, environment settings, and target packages, always verify the exact properties, imports, and field parameters in your sandbox prior to deploying content types to production.

1. Define a Shared Contract in Code

The SDK's contract() function lets you declare the shared fields once. It keeps your codebase DRY, but merges the properties directly into each schema under the hood so authors get a flat, non-nested editor form:

// src/contracts/VideoBaseContract.ts
import { contract } from '@optimizely/cms-sdk';

export const VideoBaseContract = contract({
  key: 'VideoBase',
  properties: {
    videoFile: { type: 'contentReference', allowedTypes: ['_video'], required: true },
    posterImage: { type: 'contentReference', allowedTypes: ['_image'], required: true },
    title: { type: 'string', required: true },
  },
});

2. Extend the Contract in Your Content Types

Both content types simply extend our base contract:

// src/components/StandardVideo.ts
export const StandardVideoContentType = contentType({
  key: 'StandardVideo',
  baseType: '_component',
  extends: VideoBaseContract, // <-- Merges shared fields
  properties: {
    autoplay: { type: 'boolean', group: 'playerOptions' },
    loop: { type: 'boolean', group: 'playerOptions' },
    showControls: { type: 'boolean', group: 'playerOptions' },
  },
});

// src/components/GatedVideo.ts
export const GatedVideoContentType = contentType({
  key: 'GatedVideo',
  baseType: '_component',
  extends: VideoBaseContract, // <-- Merges shared fields
  properties: {
    formId: { type: 'string', group: 'leadCapture' },
    gateTimestamp: { type: 'integer', group: 'leadCapture' },
  },
});

3. Resolve to a Single React Component

Here is the developer's favorite trick: even though we have two schemas in the CMS, we can map them both to a single React component.

In our registry setup, we point both type keys to our <VideoItem /> component. The component inspects __typename (always supplied by Optimizely Graph) to decide how to render:

// src/app/layout.tsx — Map both keys to the same component
initReactComponentRegistry({
  resolver: {
    StandardVideo: VideoItem,
    GatedVideo:    VideoItem,
  },
});

// src/components/VideoItem.tsx — One entry, two layouts
export default function VideoItem({ content }: Props) {
  if (content.__typename === 'GatedVideo') {
    return <GatedVideoView content={content} />; // Typed as GatedVideoProps
  }
  return <StandardVideoView content={content} />; // Typed as StandardVideoProps
}

Conclusion: Choosing What's Right for Your Team

There is rarely a single "correct" answer in software architecture—only choices and their consequences.

  • If your team is very small, your content needs are highly fluid, and you prioritize a rapid setup above all else, Option 1 (The Consolidated Block) might serve you well.

  • If your design system relies heavily on composable, modular sub-assemblies and your editors are comfortable with multi-step nesting, Option 2 (The Nested Wrapper) offers high structural reuse.

  • If you prioritize editor productivity, robust validation, and clean GraphQL queries, Option 3 (The Split-Type Pattern) combined with the SDK's single component routing gives you the best of both worlds: flat schemas for authors, and type-safe, DRY code for developers.

By laying these options on the table, your team can align on a modeling philosophy that matches your technical maturity and authoring workflows.


Resources: Content JS SDK documentation · Optimizely content modeling principles · Content base types

Jul 31, 2026

Comments

error Please login to comment.
Latest blogs
From SDK to Core: Modernizing Optimizely Configured Commerce for .NET 8 and .NET 10

The .NET Framework 4.8 clock is running out for Configured Commerce customizations. Handled well, this deadline is not a chore it is the cleanest...

Vaibhav | Jul 29, 2026

Parallel Development in Optimizely CMS SaaS: Shifting to a Schema Migration Mindset

  Part 3 of the Parallel Development series. The branch-scoped push script from Part 1 works. Run it on a feature branch where you've touched a...

Vipin Banka | Jul 25, 2026

From AI Agents to AI Workflow with Opal

Introduction In the first article in this series , we talked about AI agents in Optimizely Opal and walked through the process of creating a...

Igor Safonov | Jul 23, 2026