Skip to docs content
Including UI Support
Open Source
Free Cloud
Pro
SDK current
Updated 2026-09-04

Including UI Support

Editions: Open Source · Free Cloud · Pro

This is the Cloudinary Console story: sidebar, gallery, settings, and a model field. Official plugins ship a compiled ui.js. Third-party plugins do not execute JS in Console.

Manifest — contributions.ui and fields

ui:
  ui_available: true
  routes:
    - path: /
      title: Cloudinary
  navigation:
    - after: storage
      label: Cloudinary
      path: /console/plugin/hc-cloudinary-plugin
  settings:
    - path: /console/settings/plugins/hc-cloudinary-plugin
      label: Cloudinary
fields:
  - id: cloudinary_asset
    label: Cloudinary Asset
    icon: image
    storage_type: media
    form_component: CloudinaryAssetPicker
    display_component: CloudinaryAssetPreview
    content_form: true

Allowed after: anchors: content, database, model, users, storage, graphql, rest, auth, logic, plugins, settings.

Cloudinary uses after: storage so Files then Cloudinary in the project sidebar.

Files then Cloudinary in the project sidebar

Settings path is /console/settings/plugins/hc-cloudinary-plugin. Field id cloudinary_asset stores as core storage_type: media plus plugin_idnever a new FIELD_TYPE_ENUM.

Model field picker Plugin Fields / Cloudinary Asset

Bundle — ui/ui.js IIFE

esbuild marks react and antd as window.React / window.antd. Those must be the Console-bundled copies. installPluginSDK overwrites any CDN React. Dual React trees = dead Ant Design click handlers.

build.onLoad({ filter: /.*/, namespace: "window-globals" }, (args) => {
  if (args.path === "react") {
    return { contents: "module.exports = window.React;", loader: "js" };
  }
  return { contents: "module.exports = window.antd;", loader: "js" };
});

Register from ui/src/index.tsx:

sdk().register({
  name: PLUGIN_ID,
  version: "0.0.5",
  displayName: "Cloudinary",
  routes: [{ path: "/", component: "LibraryPage", title: "Cloudinary" }],
  components: {
    pages: { LibraryPage },
    forms: {
      CloudinaryAssetPicker: AssetField,
      CloudinaryAssetPreview: AssetField,
    },
  },
  fields: [
    {
      type: "cloudinary_asset",
      label: "Cloudinary Asset",
      icon: "image",
      formComponent: "CloudinaryAssetPicker",
      displayComponent: "CloudinaryAssetPreview",
      pluginId: PLUGIN_ID,
      storageType: "media",
    },
  ],
});

host.rest(PLUGIN_ID).get/post talks to plugin REST (/plugin/hc-cloudinary-plugin/...). Gallery: library.tsx lists GET /assets; cloudinary-upload.ts POST /sign then uploads to Cloudinary. Field picker: asset-field.tsx.

Cloudinary gallery with Upload image and an existing apito-test asset

Hard lesson: file inputs

Do not wrap Ant Design Upload with a display:none file input. Chrome ignores synthetic input.click() on hidden file inputs, so the button looks dead.

Cloudinary overlays an opacity-0 <input type="file"> on the button (ui/src/file-button.tsx):

<span style={{ position: "relative", display: "inline-block" }}>
  <Button type="primary" htmlType="button">{label}</Button>
  <input
    type="file"
    accept="image/*"
    style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }}
    onChange={async (event) => {
      const file = event.target.files?.[0];
      event.target.value = "";
      if (file) await onFile(file);
    }}
  />
</span>

Skip native OS file-dialog screenshots when automating — Playwright intercepts choosers.

Load rules

Engine SealUIBundle hashes ui.js. Console PluginRegistry loads the blob only if all are true:

  • official
  • signed
  • plugin activated on this project
  • SHA-256 matches bundle_sha256 from GET /system/plugin/manifest

Mismatch → declarative fallback (“no signed bundle”). After replacing ui.js locally, Engine must reload so the hash reseals.

Next: How to Publish a Plugin.