Optimizely shipped @optimizely/cms-sdk@2.2.0 and @optimizely/cms-cli@2.2.0 on 23 July 2026, both from the same commit. No breaking changes this time — five changes in the SDK, two in the CLI. The headline items are a GraphQL fragment generation fix for namespaced content types, locale sync from your build config, and a new runtime schema validator. Here’s what each one actually does.
The new features
Languages in build config — locale sync on push (CMS-53673)
Before this release, setting up languages in your CMS was a manual step — you’d log in, navigate to settings, and add each locale by hand. The build config had no concept of languages, so config push synced content types and applications but left locales untouched.
Now buildConfig accepts a languages array:
// optimizely.config.mjs
import { buildConfig } from '@optimizely/cms-sdk';
export default buildConfig({
components: [
'./src/content-types/component/BannerBlockCT.ts',
],
languages: ['en', 'sv', 'de'],
});
When you run cms-cli config push, the CLI reads this array and syncs it with the CMS via the REST API. For each locale it does three things:
- Creates locales that don’t exist yet (derives the display name automatically —
'en-US'becomes'English (United States)') - Enables locales that exist but are disabled
- Skips locales already enabled
The output tells you exactly what happened:
✔ Locales synced: 2 created, 1 enabled, 0 unchanged
This means your language setup is now fully code-first and version-controlled alongside your content types — no manual CMS configuration needed.
The bug fixes
1. GraphQL fragment generation broke on namespaced content types (CMS-54017)
Some content types in Optimizely CMS carry a namespace prefix in their key — graph: for types synced from Optimizely Graph’s built-in schema, and globalcontract: for global contract definitions. A CLI pull against a Graph-enabled instance could produce types like:
const AssetLabelGroupCT = contentType({
key: 'graph:cmp_AssetLabelGroup',
baseType: '_component',
properties: {
Id: { type: 'string', group: 'Content' },
Name: { type: 'string', group: 'Content' },
},
});
The SDK’s GraphClient auto-generates GraphQL fragments from your registered content types. Before this fix, it used the full key — colon included — as the fragment name, ON-type, and field alias prefix:
fragment graph:cmp_AssetLabelGroup on graph:cmp_AssetLabelGroup {
graph:cmp_AssetLabelGroup__Id: Id
}
Colons are illegal in GraphQL identifiers, so this query would fail at parse time.
The fix adds a stripSourcePrefix utility that removes the namespace before any identifier is generated:
// 'graph:cmp_AssetLabelGroup' → 'cmp_AssetLabelGroup'
// 'globalcontract:_Metadata' → '_Metadata'
// 'BannerBlock' → 'BannerBlock' (no-op)
const stripSourcePrefix = (key: string): string =>
key.replace(/^[a-z]+:/i, '');
It’s applied in four places: fragment names, ON-type clauses, field aliases, and contract interface mapping (so globalcontract:_Item correctly resolves to on _IItem). Namespaced types also now skip CMS base fragments since they don’t implement _IContent.
The generated output is now valid:
fragment cmp_AssetLabelGroup on cmp_AssetLabelGroup {
__typename
cmp_AssetLabelGroup__Id: Id
cmp_AssetLabelGroup__Name: Name
}
If all your content types use plain keys (like BannerBlock or ArticlePage), you were never affected. This matters when your CMS instance has Graph enabled or uses global contracts.
2. displaySettings.editor was too strictly typed (CMS-54020)
Display templates let you attach per-instance settings (like color or layout) that editors pick in the CMS UI. Each setting has an editor field controlling which widget renders.
Before this fix, the type was locked to two literal values and was required:
// Before
editor: 'select' | 'checkbox'; // required, only two values
// After
editor?: 'select' | 'checkbox' | string; // optional, any string accepted
If the CMS introduced a new editor type (like selectOne), your code wouldn’t compile. And settings that didn’t need a custom widget couldn’t omit the field. The fix widens the union to accept any string while keeping autocomplete for the known values, and makes the field optional.
3. CLI: duplicate entry point creation and application update detection (CMS-53857)
When cms-cli checkApplications syncs your app manifest with the CMS, each application has an entryPoint — the root content item for that app. Two things were broken:
Duplicate entry points. Missing applications took a separate code path that always created a new content instance for the entry point, bypassing the “does this already exist?” check. Run sync twice, get two root pages. The fix removes that path — all content now goes through a single lookup-then-create flow using deterministic UUIDs.
Phantom updates. The CLI compared entry points as raw strings. Locally you might have a bare key ('startPage'), while the CMS holds a resolved ref ('cms://content/a1b2c3...'). They never match, so every sync flagged a false update. The fix only compares entry points when the local value is already a content reference:
if (
isContentRef(configApp.entryPoint) &&
configApp.entryPoint !== existingApp.entryPoint
)
patch.entryPoint = configApp.entryPoint;
The CLI also no longer skips update detection when all applications already exist — it was previously short-circuiting with “All applications already exist” and returning early.
4. Improved typing for the contentType function (CMS-54088)
The contentType() function is how you define every content model in code. Before this fix, its TypeScript generics were too loose — property definitions, base types, and contract extensions didn’t fully infer through the type system. You could pass invalid combinations without a compile-time error, and the return type didn’t carry enough information for downstream utilities like toSchema() to produce correctly typed output.
The fix tightens the generic signatures so the compiler catches more mistakes at definition time:
// Before: this would compile without complaint
const Broken = contentType({
key: 'Broken',
baseType: '_page',
properties: {
hero: {
type: 'component',
// missing required `contentType` field — no error before the fix
},
},
});
// After: TypeScript flags the missing `contentType` immediately
The return type is also richer. When you pass a content type to toSchema(), the schema’s parse() and safeParse() methods now return ContentProps<T> — a type inferred directly from your property definitions — instead of a generic T that needed manual casting.
No code changes on your end. Existing contentType() calls continue to work; you just get better autocomplete, stricter validation, and end-to-end type inference from definition to schema validation.
Upgrading
Both packages follow same versions and 2.2.0 has no breaking changes. Update with:
npm install @optimizely/cms-sdk@latest @optimizely/cms-cli@latest
If you pin exact versions:
npm install @optimizely/cms-sdk@2.2.0 @optimizely/cms-cli@2.2.0
Verify after install:
npx @optimizely/cms-cli --version
npx @optimizely/cms-sdk --version
If you’re using the new languages feature, add the array to your optimizely.config.mjs and run config push — existing content types and applications are unaffected. Everything else (namespace fix, editor typing, duplicate entry point fix, improved contentType generics) takes effect automatically with no code changes required.
Final Thoughts
2.2.0 is a small release, but it closes real gaps. The namespace fix unblocks anyone working with Graph-synced or global contract types — a category of content types that was silently producing invalid queries. The editor typing and entry point fixes remove friction that forced workarounds. And locale sync is the kind of addition that makes the code-first workflow feel complete: your entire CMS configuration — content types, applications, display templates, and now languages — lives in version control and deploys with a single config push.
If you’re on 2.1.0, the upgrade is safe and immediate. If you’re still on 1.x, the 2.0.0 and 2.1.0 release notes cover the migration path.
Happy Optimizing!!!