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 Unsplash photo search to the CMS in two surfaces:
- A sidebar panel scoped to the currently-selected content — auto-searches
by content
key, shows results with metadata, and copies image URLs. - A full-page gallery view — a wider, higher-density grid for browsing search results independent of any single content item.
Both surfaces share the same backend proxy, settings form, and credential validation, so you can see how the full extension surface fits together and how multiple UI extensions reuse one backend function.
┌─────────────────────────┐
│ CMS sidebar (iframe) │──┐
│ *.sidebar.tsx │ │
└─────────────────────────┘ │ invokeFunction({action, params})
├────────────────────────┐
┌─────────────────────────┐ │ │
│ CMS full-page view │──┘ ▼
│ *.view.tsx │ ┌──────────────────────────┐
└─────────────────────────┘ │ 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. - Gallery view (
src/cms-ui-extensions/unsplash/UnsplashGallery.view.tsx) — a full-pageviewextension registered the same way as the sidebar. Because it isn't scoped to a single content item, it doesn't subscribe tocontext.content; instead it drives search from a query box and renders a wider, higher-density grid (more results per page). It calls the same backend function andsearch/trackDownloadactions as the sidebar, demonstrating how multiple UI extensions reuse one backend function. - 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/unsplash/UnsplashViewer.sidebar.tsx— your sidebar UI.src/cms-ui-extensions/unsplash/UnsplashGallery.view.tsx— your full-page view UI (drop it, along with theview:block inapp.yml, if you only need a sidebar).- 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), the sidebar extension (name: unsplash-viewer/entry_point: UnsplashViewer), and the view extension (name: unsplash-gallery/entry_point: UnsplashGallery) — plus theirdisplay_names — to match your domain. Remove theview:block if you don't need a full-page view. - 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 extensions in CMS. Open the OCP App Directory, authenticate and configure the app (credentials in the settings form), then enable the extensions. The sidebar panel appears in the CMS sidebar, and the gallery is available as a full-page view.
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).