From Zero to App Directory: Building an Optimizely SaaS CMS App with OCP

It started with a simple question from an editor:

“I’ve updated all twenty articles under News. Do I really have to publish them one by one?”

It’s a small request, and a perfect fit for something you can build yourself.

Optimizely SaaS CMS can be extended with apps built on Optimizely Connect Platform (OCP). You write a small TypeScript app, Optimizely hosts it, and it appears inside the CMS editor as if it had always been there. No servers to run, no separate admin site: just your feature, where editors already work.

So we built Publish Child Items. You open a page, open the panel, tick the drafts, and click Publish. Twenty clicks become one.

Publish Child Items in the CMS sidebar: child pages with Draft and Live badges, ready to publish together.

This post follows that app from an empty folder to the App Directory. It’s the path we took, what we learned along the way, and what we’d tell anyone building their first SaaS CMS app.

Planning Your App

Before writing code, answer two simple questions.

Where should it appear in the CMS?

A CMS UI extension (beta) can show up in three places:

  • Sidebar: a panel next to the page being edited
  • View: a full page of its own
  • Property editor: a custom input for one field

Our app works on the page you’re looking at, so we chose the sidebar.

What runs in the browser, and what runs on the server?

Your app has two halves:

  • The UI runs in the editor’s browser. It shows things and handles clicks.
  • A backend function runs on OCP. It does the real work, like calling the CMS API, and keeps secrets such as API keys safe.

Here’s how that split works in Publish Child Items:

  • In the browser (UI): when an editor opens a page, the sidebar asks the backend for that page’s child items and shows them with their status, Draft or Live. The editor ticks the drafts and clicks Publish.
  • On OCP (backend): the backend receives the request, uses the app’s CMS API key to fetch the children or publish the selected drafts through the CMS REST API, and sends the result back to the sidebar.

The editor only sees a simple panel. The API key and the CMS calls stay safely on the server.

💡 Tip: if a step needs a password or an API key, it belongs in the backend.

Setting Up Your Environment

A one-time setup, and then you’re ready to build.

What you need

  • OCP developer access: ask your organisation admin for the Developer role. If OCP developer features aren’t enabled yet, Optimizely Support can switch them on.
  • A SaaS CMS instance with UI extensions enabled: the feature is in beta, so ask Optimizely Support to enable it for your instance.
  • Node.js 22 or later and Git on your machine.

Step 1: Create your developer API key

In the OCP Developer Portal, generate an API key. The CLI uses it to act on your behalf.

Don’t see an API Keys section in the portal? It isn’t enabled by default. Raise a request with the Optimizely Support team to enable it for your account, and once it appears, you can create your key.

Step 2: Install the OCP CLI

On Windows, in PowerShell:

iwr -useb https://cli.ocp.optimizely.com/install.ps1 | iex

On macOS or Linux:

curl -fsSL https://cli.ocp.optimizely.com/install.sh | bash

Open a new terminal and check it: ocp --version should show 2.x.

Step 3: Save your API key

The CLI reads the key from ~/.ocp/credentials.json (on Windows, %USERPROFILE%\.ocp\credentials.json):

{ "apiKey": "<your-developer-api-key>" }

Step 4: Check that everything works

ocp accounts whoami

This one command tells you three useful things:

  • your vendor ID, which goes into your app’s app.yml
  • the OCP accounts you can install apps into
  • the CMS instances you can reach, with their IDs, which you’ll need later to install your app

Registering and Creating Your App

Register the app once, then let the CLI create the project for you.

Step 1: Register your app in the Developer Portal

Registering reserves your app’s ID with OCP. In the OCP Developer Portal, open Apps and click Register app:

  • App ID: a unique, lowercase ID with underscores, such as publish_child_items. Choose carefully: it can’t be changed later.
  • Display name: the name people see, such as Publish Child Items.
  • Target product: OCP.

Step 2: Create the project

One command creates the project from Optimizely’s CMS UI Extensions Sample template, a ready-to-run sidebar extension with a backend function already connected to it:

ocp app init --app-id <app_id> --display-name "<Display Name>" --summary "<what your app does>" --support-url <https://your-support-url> --contact-email <you@example.com> --category Productivity --template "CMS UI Extensions Sample" --package-manager npm --directory <project-folder> --no-prompt

Replace the <placeholders> with your own values. --app-id must match the ID you registered.

Prefer to be guided? Run ocp app init on its own and answer the prompts instead.

Step 3: Initialise Git

OCP expects your app to live in a Git repository, so do this straight away:

cd <project-folder>
git init

Step 4: Two small touch-ups

The template gives you a great head start. We made two small adjustments to suit our setup:

  1. Add a version to app.yml, under display_name. We use versioned releases in this guide, and the CLI reads the version from here:
version: 1.0.0-dev.1

2. Using npm? The template’s scripts in package.json call yarn. Change them to npm, for example yarn build → npm run build.

That’s it: you now have a real CMS UI extension app. Next, let’s look at what’s inside.

What’s Inside Your App

The template is small, and most of it you’ll never touch. These are the parts that matter:

app.yml ties it together

The manifest declares the two halves of the app. Here’s ours, trimmed to the important lines:

runtime: node22-cms-ext          # marks this as a CMS UI extension app

functions:
  cms_extension:                 # the name the UI uses to call the backend
    entry_point: PublishChildrenBackend
    accepts: cms_ui_extension    # only our UI can call it, with no public URL

ui_extensions:
  sidebar:
    - name: publish-children
      entry_point: PublishChildren    # → PublishChildren.sidebar.tsx
      display_name: Publish Child Items

Each entry_point points to a file: PublishChildren is the sidebar file PublishChildren.sidebar.tsx, and PublishChildrenBackend is the backend class of the same name.

💡 Tip: the file name tells OCP where the UI appears. MyPanel.sidebar.tsx becomes a sidebar, and MyPage.view.tsx becomes a full page.

So we have a sidebar and a backend function. Next: how do they talk to each other?

How the Sidebar Talks to the Backend

This is the heart of every CMS UI extension app. The sidebar and the backend never call each other directly. The CMS carries the message between them.

The sidebar sends a request

When the CMS loads your panel, register() gives you a context. Its invokeFunction() calls the backend using the function’s key from app.yml:

register((context) => <PublishChildren context={context} />);

// inside the panel
const response = await context.extension.invokeFunction('cms_extension', {
  action: 'listChildren',
  params: { key: contentKey }
});

The backend answers it.

On OCP, your function class receives exactly that JSON in this.request.bodyJSON:

export class PublishChildrenBackend extends App.Function {
  public async perform(): Promise<App.Response> {
    const { action, params } = this.request.bodyJSON;

    switch (action) {
      case 'listChildren': return new App.Response(200, { ok: true, result: /* children */ });
      case 'publish':      return new App.Response(200, { ok: true, result: /* results */ });
    }
    return new App.Response(400, { ok: false, error: 'unknown_action' });
  }
}

Whatever the backend returns arrives in the sidebar as response.data.

Two patterns that made life easy

  • One function, many actions. Rather than one backend function per feature, we send an action name and switch on it. Adding a feature means adding a case.
  • Always return the same shape. Every reply is { ok: true, result } or { ok: false, error }. The sidebar checks ok once and never has to guess.

💡 Tip: only the JSON you send reaches the backend. The editor’s login doesn’t, so if your backend calls the CMS, give it its own API key through the app’s settings form.

Deploy and Try It in Your CMS

Time to see it inside the editor. Four steps take you from code to a working panel.

💻 The complete source code for Publish Child Items is on GitHub: chiragkhanna28/OptimizelyPublishingApp

Step 1: Validate: Lints, builds, tests and runs OCP’s checks. Look for “Looks good to me”.

npm run validate

Step 2: Publish a dev version: OCP builds it in the cloud. Dev versions need no review, which makes them perfect for testing.

ocp app prepare --bump-dev-version --publish

Step 3: Install it on your CMS. Use the CMS instance ID from ocp accounts whoami. --scope is what installs it on that CMS.

ocp directory install <app_id>@<version> --scope <cms-instance-id>

Step 4: Configure and open. Add your settings in the OCP App Directory, enable the extension in the CMS, and open a page.

App Settings:

App Preview:

💡 Tip: to ship a change, run steps 1 and 2 again and reload the CMS. To watch the backend while you test, use ocp app logs --appId=<app_id> --scope=<cms-instance-id> --tail.

Final Thoughts

It began with one editor’s question and ended with a small panel that saves real time every day. Along the way, it showed us how approachable extending Optimizely SaaS CMS has become: a familiar stack, a clear structure, and a platform that takes care of hosting for you.

A few things we’d gently pass on to anyone starting their first app:

  • Begin with the template. It gives you a working sidebar and backend from the very first run.
  • Let each half do its job. The sidebar talks to editors, and the backend takes care of everything secure.
  • Think like an editor. Multiple languages, larger sections and the occasional slow first call are all part of real content work, and planning for them early makes the experience feel effortless.
  • Test in a real CMS, often. Dev versions make every change quick to try.

Publish Child Items is now with the Optimizely team for review. Once approved, it will be available in the App Directory for everyone to install. The review and release journey deserves its own story, and we look forward to sharing it in a follow-up post.

Thank you for reading. If this inspires an idea for your own SaaS CMS app, we’d be glad to hear about it, and happy building!

Happy Optimizing!!!

Leave a Reply