📌 Scope: This post covers Optimizely CMS (SaaS) only — using the @optimizely/cms-sdk toolchain. CMS 13 (PaaS) handles shared structure differently via .NET interfaces.
When you’re building a multi-content-type site, you quickly run into a problem: Blog Articles, Press Releases, and News Pages all need category and tags — but without a shared structure, you end up defining those fields three times and writing three separate Graph queries to retrieve them.
Contracts solve this. Define shared properties once on a Contract, apply it to any number of content types, and Optimizely Graph automatically exposes a unified query interface across all of them.
What is a Contract?
A Contract (also called an interface) defines a set of properties that any content type can implement. Once a content type implements a Contract, it’s guaranteed to have those fields — and Optimizely Graph automatically exposes a unified GraphQL interface for querying all implementing types in one call.

Why use Contracts?
Three concrete benefits:
1. Guaranteed consistency across content types. Define Category and Tags on a Categorizable contract. Apply it to Blog Article, Press Release, and News Page. Every one of those content types will always have those fields — editors can’t skip them, and developers don’t need to check for them per type.
2. One GraphQL query for all implementing types. Instead of writing three separate Graph queries and merging results client-side, you write one query targeting the Categorizable contract and get all content back in a single call. Inline fragments (... on BlogArticle) let you pull type-specific fields on top.
3. Front-end decoupling. Your React/Next.js components can be written against a contract interface rather than a specific content type. A <CategoryFilter /> component that works with any Categorizable content doesn’t need to know whether it’s rendering a blog post or a press release.
How: Define a Contract in code
The cleanest way is in your codebase using @optimizely/cms-sdk, then push it to CMS with the CLI. This keeps your contracts in version control alongside your content types.
Define the contract:
// src/contracts/Categorizable.tsimport { contentType } from '@optimizely/cms-sdk'export const CategorizableContract = contentType({ key: 'Categorizable', baseType: '_component', // contracts use _component as their base properties: { category: { type: 'string', displayName: 'Category', indexingType: 'queryable', }, tags: { type: 'array', items: { type: 'string' }, displayName: 'Tags', indexingType: 'queryable', }, },})
Apply it when defining a content type:
// src/content-types/BlogArticle.tsimport { contentType } from '@optimizely/cms-sdk'import { CategorizableContract } from '../contracts/Categorizable'export const BlogArticleContentType = contentType({ key: 'BlogArticle', baseType: '_page', contracts: [CategorizableContract], // ← implements the contract (IMP NOTE : A content type can extend multiple contracts by passing an array) properties: { heading: { type: 'string', displayName: 'Heading', indexingType: 'searchable', }, body: { type: 'richText', displayName: 'Body', }, // category and tags come from the contract — no need to repeat them here },})
Push both to CMS in one command:
npx @optimizely/cms-cli@latest config push optimizely.config.mjs
After pushing, run CMS → Settings → Scheduled Jobs → Graph Reindex to update the GraphQL schema. The Categorizable interface will appear as a queryable type in Graph within minutes.
How: Create a Contract in the CMS UI
If you prefer the no-code route: Settings → Content Types → Create New → Contract. Fill in the Name (programmatic key), Display Name, and Description, then add properties.

Once the contract exists, open any content type (e.g. Blog Article), go to its Contracts tab, and select the contract to implement it.

Note: Create the properties on the content type before applying the contract — the CMS binds existing properties to contract properties during assignment.
How: Query via Optimizely Graph
Once a contract is defined and content types implement it, Graph exposes the contract as a queryable type. Query all categorizable content with a single call, and use inline fragments for type-specific fields:
query GetCategorizableContent { Categorizable(where: { category: { eq: "Product Launch" } }) { items { _metadata { key displayName url { default } } category tags # Type-specific fields via inline fragments ... on BlogArticle { heading body { html } } ... on PressRelease { headline publishDate } ... on NewsPage { summary } } }}
One query. Multiple content types. No client-side merging. This is the power of contracts over trying to union separate Graph queries in your front-end code.
Practical contract patterns
| Contract name | Properties | Who implements it |
|---|---|---|
Categorizable | category, tags | Blog, News, Press Release |
SEOMetadata | metaTitle, metaDescription, canonicalUrl | All page types |
Publishable | publishDate, expiryDate, author | Blog, Article, Event |
HeroBlock | heroImage, heroHeadline, heroCTA | Landing Page, Campaign Page, Home Page |
Note : A content type can extend multiple contracts by passing an array e.g. extends: [SEOContract, TrackingContract], // Multiple contracts. When a content type extends contracts:
- Properties defined directly on the content type override any inherited properties with the same key
- All contract properties are merged into the content type
- If multiple contracts define the same property key, the rightmost contract wins
Gotchas
Properties must exist on the content type before you apply the contract. In Optimizely SaaS you define the properties on the content type first, then bind them to the contract. If you use the code-first SDK approach with the contracts array, this is handled automatically on push — no manual binding needed.
Always reindex Graph after changes. New contracts and updated implementations don’t appear in the GraphQL schema until you run a Graph Reindex job. In development this is easy to forget — build it into your CLI push workflow.
Set indexingType: 'queryable' on contract properties you’ll filter by. The default is searchable (full-text), which is fine for body text. But for fields like category or publishDate where you’re filtering/sorting — not full-text searching — use queryable for better Graph performance.
Quick checklist
- ☐ Contract defined with
contentType()in code (or via CMS UI) - ☐ Pushed to CMS with
cms-cli config push - ☐ Content types have
contracts: [MyContract](or assigned via UI) - ☐ Graph Reindex run after push
- ☐ Contract properties use
indexingType: 'queryable'for filter fields - ☐ Front-end queries target the contract type, not individual content types