Sample OCP app demonstrating how to build a CMS UI extension for Optimizely CMS. Fork this repository as the starting point for your own extension.
Beta: the CMS UI extension SDK is in beta. APIs may change.
The included demo adds an Unsplash photo search panel to the CMS sidebar. Editors type a query, browse results, view photo metadata, and copy image URLs to the clipboard. It exercises the full extension surface — sidebar UI, backend proxy, settings form, and credential validation — so you can see how the pieces fit together.
┌─────────────────────────┐ invokeFunction({action, params})
│ CMS sidebar (iframe) │ ───────────────────────────────────┐
│ src/cms-ui-extensions │ │
└─────────────────────────┘ ▼
┌──────────────────────────┐
│ Backend function (OCP) │
│ src/backend/functions │
└────────────┬─────────────┘
│ HTTPS + Access Key
▼
┌──────────────────┐
│ Third-party API │
│ (Unsplash) │
└──────────────────┘
- Sidebar UI (
src/cms-ui-extensions/unsplash/UnsplashViewer.sidebar.tsx) — runs inside the CMS iframe. Built with React 19 and the Optimizely Axiom design system. The entry callsregister(factory)from@optimizely/cms-extensibility-sdk; the SDK mounts the returned React tree itself. Talks to the backend viacontext.extension.invokeFunction()and subscribes tocontext.contentto auto-search by the currently-selected contentkey. Swap React/Axiom for any framework by editing the entry and adjustingvite.ui.config.mjsplugins. - Backend function (
src/backend/functions/CmsUiExtension.ts) — runs on the OCP platform. Holds credentials, proxies to the external API. Uses an action-router pattern: the UI sends{action, params}; the function switches onactionand returns a standardized{ok: true, result}/{ok: false, error}envelope. - Unsplash client (
src/backend/lib/unsplash.ts) — thin HTTP wrapper aroundapi.unsplash.com. Replace with your own service client. - Lifecycle hooks (
src/backend/lifecycle/Lifecycle.ts) — validates the Access Key server-side viaonSettingsFormbefore saving. - Settings form (
forms/settings.yml) — per-install credentials.
Why a backend proxy? API keys never reach the browser, no CORS to configure, and auth/rate-limit handling lives in one place.
When forking, replace these (demo-specific to Unsplash):
src/backend/lib/unsplash.ts— swap for your own API client.src/cms-ui-extensions/UnsplashViewer.sidebar.tsx— your sidebar UI.- The
search/trackDownloadactions insrc/backend/functions/CmsUiExtension.ts— your action set. - Credential fields in
forms/settings.yml— your settings shape. - The
APP_ENV_UNSPLASH_ACCESS_KEYenvironment variable inapp.yml.
Keep these (reusable boilerplate any CMS UI extension needs):
- The function envelope + error-handling pattern in
CmsUiExtension.ts. - The lifecycle settings-validation flow in
Lifecycle.ts. - Shared helpers under
src/cms-ui-extensions/common/(e.g.runtime.ts). - Build configs:
vite.backend.config.mjs,vite.ui.config.mjs,ocp-app.config.mjs. - The overall
src/layout (backend/,cms-ui-extensions/,shared/).
- In
app.yml, updatemeta:app_id,display_name,summary,vendor,version,categories. - In
app.yml, rename the function (cms_extension/entry_point: CmsUiExtension) and the sidebar extension (name: unsplash-viewer/entry_point: UnsplashViewer,display_name) to match your domain. - Rename the matching files under
src/backend/functions/andsrc/cms-ui-extensions/to match the newentry_pointvalues. - Update
name(and any other identifying fields) inpackage.json. - Replace the assets in
assets/(icon, logo,directory/overview.md). - Update
forms/settings.ymlwith your own credential fields, and update theenvironment:list inapp.ymlto your env var name. - Replace the Unsplash client and the action handlers; wire your UI to the new actions.
The Unsplash Access Key is resolved in this order:
- The
credentials.accessKeyvalue from the app settings form (per install). - The
APP_ENV_UNSPLASH_ACCESS_KEYenvironment variable (platform-wide default).
Create an Access Key at https://unsplash.com/developers.
yarn install
yarn build # vite build: backend bundle + UI bundle
yarn lint
yarn typecheck
yarn test # placeholder — no tests defined
yarn validate # lint + typecheck + build + ocp-app-sdk validateEnd-to-end loop for getting a change in front of CMS editors.
-
Set up your dev environment. Install the OCP CLI and configure tenant credentials. See Configure your development environment.
-
Build and validate locally.
yarn validate
-
Publish a dev version to OCP. Bumps the dev build number and uploads:
ocp app prepare --bump-dev-version --publish
-
Install the dev app into your sandbox account (one-time per OCP instance — subsequent publishes auto-upgrade to consecutive semver versions):
ocp directory install <app_id>@<app_version> <tracker_id>
-
Enable the extension in CMS. Open the OCP App Directory, authenticate and configure the app (credentials in the settings form), then enable the sidebar extension. The panel appears in the CMS sidebar.
Iterate: edit code → yarn validate → ocp app prepare --bump-dev-version --publish → reload CMS to pick up the new bundle.
@optimizely/ocp-cms-ui-extensions-app-sdk— OCP plugin that wires CMS UI extensions into the app build (ocp-app.config.mjs).@optimizely/cms-extensibility-sdk— runtime SDK used inside the sidebar bundle to talk to the CMS host (context.extension.invokeFunction, etc.).@zaiusinc/app-sdk— base classes for backend functions and lifecycle hooks (App.Function,App.Lifecycle,App.Response).