Skip to content

Repository files navigation

Unsplash Viewer — Sample CMS UI Extension

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.

What this app does

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.

Architecture

┌─────────────────────────┐
│  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 calls register(factory) from @optimizely/cms-extensibility-sdk; the SDK mounts the returned React tree itself. Talks to the backend via context.extension.invokeFunction() and subscribes to context.content to auto-search by the currently-selected content key. Swap React/Axiom for any framework by editing the entry and adjusting vite.ui.config.mjs plugins.
  • Gallery view (src/cms-ui-extensions/unsplash/UnsplashGallery.view.tsx) — a full-page view extension registered the same way as the sidebar. Because it isn't scoped to a single content item, it doesn't subscribe to context.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 and search / trackDownload actions 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 on action and returns a standardized {ok: true, result} / {ok: false, error} envelope.
  • Unsplash client (src/backend/lib/unsplash.ts) — thin HTTP wrapper around api.unsplash.com. Replace with your own service client.
  • Lifecycle hooks (src/backend/lifecycle/Lifecycle.ts) — validates the Access Key server-side via onSettingsForm before 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.

Demo-specific vs. boilerplate

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 the view: block in app.yml, if you only need a sidebar).
  • The search / trackDownload actions in src/backend/functions/CmsUiExtension.ts — your action set.
  • Credential fields in forms/settings.yml — your settings shape.
  • The APP_ENV_UNSPLASH_ACCESS_KEY environment variable in app.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/).

Fork checklist

  1. In app.yml, update meta: app_id, display_name, summary, vendor, version, categories.
  2. 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 their display_names — to match your domain. Remove the view: block if you don't need a full-page view.
  3. Rename the matching files under src/backend/functions/ and src/cms-ui-extensions/ to match the new entry_point values.
  4. Update name (and any other identifying fields) in package.json.
  5. Replace the assets in assets/ (icon, logo, directory/overview.md).
  6. Update forms/settings.yml with your own credential fields, and update the environment: list in app.yml to your env var name.
  7. Replace the Unsplash client and the action handlers; wire your UI to the new actions.

Configuration

The Unsplash Access Key is resolved in this order:

  1. The credentials.accessKey value from the app settings form (per install).
  2. The APP_ENV_UNSPLASH_ACCESS_KEY environment variable (platform-wide default).

Create an Access Key at https://unsplash.com/developers.

Commands

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 validate

Development workflow

End-to-end loop for getting a change in front of CMS editors.

  1. Set up your dev environment. Install the OCP CLI and configure tenant credentials. See Configure your development environment.

  2. Build and validate locally.

    yarn validate
  3. Publish a dev version to OCP. Bumps the dev build number and uploads:

    ocp app prepare --bump-dev-version --publish
  4. 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>
  5. 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 validateocp app prepare --bump-dev-version --publish → reload CMS to pick up the new bundle.

Further reading

  • @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).

About

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.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages