# Content Manager APIs

> Source: https://docs.strapi.io/cms/plugins-development/content-manager-apis

Content Manager APIs add panels, actions, and custom rich text blocks to the Content Manager through `addEditViewSidePanel`, `addDocumentAction`, `addDocumentHeaderAction`, `addBulkAction`, or `addRichTextBlocks`. Each API accepts component functions with typed contexts, enabling precise control over document-aware UI injections.

Content Manager APIs are part of the [Admin Panel API](/cms/plugins-development/admin-panel-api). They are a way for Strapi plugins to add content or options to the [Content Manager](/cms/features/content-manager). The Content Manager APIs allow you to extend the Content Manager by adding functionality from your own plugin, just like you can do it with [Injection zones](/cms/plugins-development/admin-injection-zones).

:::prerequisites
Before diving deeper into the concepts on this page, please ensure you have:
- [created a Strapi plugin](/cms/plugins-development/create-a-plugin),
- read and understood the basics of the [Admin Panel API](/cms/plugins-development/admin-panel-api)
:::

## General information

Strapi 5 provides 4 Content Manager APIs, all accessible through `app.getPlugin('content-manager').apis`. 
All the Content Manager APIs share the same API shape and must use components.

### Injection zones vs. Content Manager APIs

:::tip tl;dr
For adding panels, actions, or buttons to the Content Manager, the [Content Manager APIs](/cms/plugins-development/content-manager-apis) (`addDocumentAction`, `addEditViewSidePanel`, etc.) are often more robust and better typed than injection zones. Use injection zones when you need to insert components into specific UI areas not covered by the Content Manager APIs.
:::

Content Manager APIs and injection zones are both extension points to customize the admin panel, but they solve different needs:

| Need | Recommended API | Why |
| --- | --- | --- |
| Add a custom panel in the Edit View side area | Content Manager API ([`addEditViewSidePanel`](/cms/plugins-development/content-manager-apis#addeditviewsidepanel)) | Best for contextual information or controls that stay visible while editing. |
| Add actions in a document action menu | Content Manager API ([`addDocumentAction`](/cms/plugins-development/content-manager-apis#adddocumentaction)) | Best for document-level actions in the Edit View actions menu. |
| Add actions in the Edit View header | Content Manager API ([`addDocumentHeaderAction`](/cms/plugins-development/content-manager-apis#adddocumentheaderaction)) | Best for quick, prominent actions next to the document title. |
| Add actions for selected entries in List View | Content Manager API ([`addBulkAction`](/cms/plugins-development/content-manager-apis#addbulkaction)) | Best for workflows that apply to multiple entries at once. |
| Add UI to a predefined zone in a plugin view (localized visual customization) | Injection Zones API ([`injectComponent`](/cms/plugins-development/admin-injection-zones#injecting-into-content-manager-zones)) | Best when you target a specific zone exposed by a plugin. |

For implementation details and up-to-date API signatures, please refer to the [content-manager](https://github.com/strapi/strapi/blob/develop/packages/core/content-manager/admin/src/content-manager.ts) file in the Strapi codebase.

**Mini examples (inside `bootstrap(app)`)**

```js
// Document action menu item
app.getPlugin('content-manager').apis.addDocumentAction(() => ({
  label: 'Run custom action',
  onClick: ({ documentId }) => runCustomAction(documentId),
}));

// Edit View header action
app.getPlugin('content-manager').apis.addDocumentHeaderAction(() => ({
  label: 'Open preview',
  onClick: ({ document }) => openPreview(document),
}));

// List View bulk action
app.getPlugin('content-manager').apis.addBulkAction(() => ({
  label: 'Bulk publish',
  onClick: ({ documentIds }) => bulkPublish(documentIds),
}));

// Edit View side panel
app.getPlugin('content-manager').apis.addEditViewSidePanel([
  {
    name: 'my-plugin.side-panel',
    Component: MySidePanel,
  },
]);

// Injection zone (plugin-defined zone)
app.getPlugin('content-manager').injectComponent('editView', 'right-links', {
  name: 'my-plugin.custom-link',
  Component: MyCustomLink,
});
```

### API shape

All Content Manager APIs works in the same way: to use them, call them on your plugin's [`bootstrap()`](/cms/plugins-development/admin-panel-api#bootstrap) function, in 2 possible ways:

:::note
When using TypeScript, the `apis` property returned by `app.getPlugin()` is typed as `unknown`. Cast it to `ContentManagerPlugin['config']['apis']` before calling the APIs.
:::

- Passing an array with what you want to add. For example, the following code would add the ReleasesPanel at the end of the current EditViewSidePanels:

  ```js
    const apis = app.getPlugin('content-manager').apis;

    apis.addEditViewSidePanel([ReleasesPanel]);
    ```
  ```tsx
    import type { ContentManagerPlugin } from '@strapi/content-manager/strapi-admin';

    const apis =
      app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis'];

    apis.addEditViewSidePanel([ReleasesPanel]);
    ```

- Passing a function that receives the current elements and return the new ones. This is useful if, for example, you want to add something in a specific position in the list, like in the following code:

  ```js
    const apis = app.getPlugin('content-manager').apis;

    apis.addEditViewSidePanel((panels) => [SuperImportantPanel, ...panels]);
    ```
  ```tsx
    const apis =
      app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis'];

    apis.addEditViewSidePanel((panels) => [SuperImportantPanel, ...panels]);
    ```

### Components

You need to pass components to the API in order to add things to the Content Manager.

Components are functions that receive some properties and return an object with some shape (depending on the function). Each component's return object is different based on the function you're using, but they receive similar properties, depending on whether you use a ListView or EditView API.

Properties include important information about the document(s) you are viewing or editing.

#### ListViewContext

```jsx
interface ListViewContext {
  /**
   * Will be either 'single-types' | 'collection-types'
   */
  collectionType: string;
  /**
   * The current selected documents in the table
   */
  documents: Document[];
  /**
   * The current content-type's model.
   */
  model: string;
}
```

#### EditViewContext

```jsx
interface EditViewContext {
  /**
   * This will only be null if the content-type
   * does not have draft & publish enabled.
   */
  activeTab: 'draft' | 'published' | null;
  /**
   * Will be either 'single-types' | 'collection-types'
   */
  collectionType: string;
  /**
   * Will be undefined if someone is creating an entry.
   */
  document?: Document;
  /**
   * Will be undefined if someone is creating an entry.
   */
  documentId?: string;
  /**
   * Will be undefined if someone is creating an entry.
   */
  meta?: DocumentMetadata;
  /**
   * The current content-type's model.
   */
  model: string;
}
```

:::tip
More information about types and APIs can be found in [Strapi](https://github.com/strapi/strapi/blob/develop/packages/core/content-manager/admin/src/content-manager.ts).
:::

**Example:**

Adding a panel to the sidebar can be done as follows:

<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript" default>

```jsx title="my-plugin/components/my-panel.js"
const Panel = ({ 
  activeTab, 
  collectionType, 
  document, 
  documentId, 
  meta, 
  model 
}) => {
  return {
    title: 'My Panel',
    content: <p>I'm on {activeTab}</p>
  }
}
```

<TabItem value="ts" label="TypeScript">

```tsx title="my-plugin/components/my-panel.ts"

const Panel: PanelComponent = ({ 
  activeTab, 
  collectionType, 
  document, 
  documentId, 
  meta, 
  model 
}: PanelComponentProps) => {
  return {
    title: 'My Panel',
    content: <p>I'm on {activeTab}</p>
  }
}
```

## Available APIs

<br/>

### `addEditViewSidePanel`

Use this to add new panels to the Edit view sidebar, just like in the following example where something is added to the Releases panel:

![addEditViewSidePanel](/img/assets/content-manager-apis/add-edit-view-side-panel.png)

```jsx
addEditViewSidePanel(panels: DescriptionReducer<PanelComponent> | PanelComponent[])
```

#### PanelComponent

A `PanelComponent` receives the properties listed in [EditViewContext](#editviewcontext) and returns an object with the following shape:

```tsx
type PanelComponent = (props: PanelComponentProps) => {
  title: string;
  content: React.ReactNode;
};
```

`PanelComponentProps` extends the [EditViewContext](#editviewcontext).

### `addDocumentAction`

Use this API to add more actions to the Edit view or the List View of the Content Manager. There are 3 positions available:

- `header` of the Edit view:

    ![Header of the Edit view](/img/assets/content-manager-apis/add-document-action-header.png)
- `panel` of the Edit view:

    ![Panel of the Edit View](/img/assets/content-manager-apis/add-document-action-panel.png)
- `table-row` of the List view:

    ![Table-row in the List View](/img/assets/content-manager-apis/add-document-action-tablerow.png)

```jsx
addDocumentAction(actions: DescriptionReducer<DocumentActionComponent> | DocumentActionComponent[])
```

#### DocumentActionDescription

The interface and properties of the API look like the following: 

```jsx
interface DocumentActionDescription {
    label: string;
    onClick?: (event: React.SyntheticEvent) => Promise<boolean | void> | boolean | void;
    icon?: React.ReactNode;
    /**
     * @default false
     */
    disabled?: boolean;
    /**
     * @default 'panel'
     * @description Where the action should be rendered.
     */
    position?: DocumentActionPosition | DocumentActionPosition[];
    dialog?: DialogOptions | NotificationOptions | ModalOptions;
    /**
     * @default 'secondary'
     */
    variant?: ButtonProps['variant'];
    loading?: ButtonProps['loading'];
}

type DocumentActionPosition = 'panel' | 'header' | 'table-row' | 'preview' | 'relation-modal';

interface DialogOptions {
    type: 'dialog';
    title: string;
    content?: React.ReactNode;
    variant?: ButtonProps['variant'];
    onConfirm?: () => void | Promise<void>;
    onCancel?: () => void | Promise<void>;
}
interface NotificationOptions {
    type: 'notification';
    title: string;
    link?: {
        label: string;
        url: string;
        target?: string;
    };
    content?: string;
    onClose?: () => void;
    status?: NotificationConfig['type'];
    timeout?: number;
}
interface ModalOptions {
    type: 'modal';
    title: string;
    content: React.ComponentType<{
        onClose: () => void;
    }> | React.ReactNode;
    footer?: React.ComponentType<{
        onClose: () => void;
    }> | React.ReactNode;
    onClose?: () => void;
}
```

### `addDocumentHeaderAction`

Use this API to add more actions to the header of the Edit view of the Content Manager:

![addEditViewSidePanel](/img/assets/content-manager-apis/add-document-header-action.png)

```jsx
addDocumentHeaderAction(actions: DescriptionReducer<HeaderActionComponent> | HeaderActionComponent[])
```

#### HeaderActionDescription

The interface and properties of the API look like the following:

```jsx
interface HeaderActionDescription {
  disabled?: boolean;
  label: string;
  icon?: React.ReactNode;
  type?: 'icon' | 'default';
  onClick?: (event: React.SyntheticEvent) => Promise<boolean | void> | boolean | void;
  dialog?: DialogOptions;
  options?: Array<{
    disabled?: boolean;
    label: string;
    startIcon?: React.ReactNode;
    textValue?: string;
    value: string;
  }>;
  onSelect?: (value: string) => void;
  value?: string;
}

interface DialogOptions {
  type: 'dialog';
  title: string;
  content?: React.ReactNode;
  footer?: React.ReactNode;
}
```

### `addBulkAction`

Use this API to add buttons that show up when entries are selected on the List View of the Content Manager, just like the "Add to Release" button for instance:

![addEditViewSidePanel](/img/assets/content-manager-apis/add-bulk-action.png)

```jsx
addBulkAction(actions: DescriptionReducer<BulkActionComponent> | BulkActionComponent[])
```

#### BulkActionDescription

The interface and properties of the API look like the following: 

```jsx
interface BulkActionDescription {
  dialog?: DialogOptions | NotificationOptions | ModalOptions;
  disabled?: boolean;
  icon?: React.ReactNode;
  label: string;
  onClick?: (event: React.SyntheticEvent) => void;
  /**
   * @default 'default'
   */
  type?: 'icon' | 'default';
  /**
   * @default 'secondary'
   */
  variant?: ButtonProps['variant'];
}
```

### `addRichTextBlocks`

Use this API to register custom block types in the [Blocks](/cms/features/content-manager) rich text field. Custom blocks appear in the toolbar dropdown alongside built-in ones.

:::note
`addRichTextBlocks` must be called in the `register()` lifecycle function, not `bootstrap()`. The editor initializes its Slate instance during `register`, so blocks must be available at that point.
:::

```jsx
addRichTextBlocks(blocks: RichTextBlocksStore | ((currentBlocks: RichTextBlocksStore) => RichTextBlocksStore))
```

The API accepts 2 call signatures:

- Passing an **object**: the provided blocks are merged into the existing blocks store.

  ```js title="src/admin/app.js"
  export default {
    register(app) {
      app.getPlugin('content-manager').apis.addRichTextBlocks({
        callout: {
          renderElement: (props) => {props.children},
          icon: Information,
          label: { id: 'my-plugin.blocks.callout', defaultMessage: 'Callout' },
          matchNode: (node) => node.type === 'callout',
          isInBlocksSelector: true,
          handleConvert(editor) { /* use Slate Transforms to set node type */ },
          snippets: [':::callout'],
        },
      });
    },
  };
  ```
  ```tsx title="src/admin/app.ts"
  import type { ContentManagerPlugin } from '@strapi/content-manager/strapi-admin';

  export default {
    register(app) {
      const apis =
        app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis'];

      apis.addRichTextBlocks({
        callout: {
          renderElement: (props) => {props.children},
          icon: Information,
          label: { id: 'my-plugin.blocks.callout', defaultMessage: 'Callout' },
          matchNode: (node) => node.type === 'callout',
          isInBlocksSelector: true,
          handleConvert(editor) { /* use Slate Transforms to set node type */ },
          snippets: [':::callout'],
        },
      });
    },
  };
  ```

- Passing a **function**: the function receives the current blocks store and must return the updated store. Use this form to remove or replace built-in blocks.

  ```js title="src/admin/app.js"
  export default {
    register(app) {
      app.getPlugin('content-manager').apis.addRichTextBlocks((currentBlocks) => {
        // Remove the built-in code block
        const { code: _removed, ...rest } = currentBlocks;
        return rest;
      });
    },
  };
  ```
  ```tsx title="src/admin/app.ts"
  import type {
    ContentManagerPlugin,
    RichTextBlocksStore,
  } from '@strapi/content-manager/strapi-admin';

  export default {
    register(app) {
      const apis =
        app.getPlugin('content-manager').apis as ContentManagerPlugin['config']['apis'];

      apis.addRichTextBlocks((currentBlocks: RichTextBlocksStore) => {
        const { code: _removed, ...rest } = currentBlocks;
        return rest;
      });
    },
  };
  ```

#### Block definition

Each entry in the blocks object is a block definition with the following properties:

| Property | Required | Type | Description |
|---|---|---|---|
| `renderElement` | Yes | `React.FC` | React render function. Spread `props.attributes` on the root element and render `props.children`. |
| `matchNode` | Yes | `(node: Node) => boolean` | Returns `true` if a given [Slate](https://docs.slatejs.org/) node belongs to this block type. |
| `isInBlocksSelector` | No | `boolean` | Set to `true` to show the block in the toolbar dropdown. Defaults to `false`. |
| `icon` | No | `React.ComponentType` | Icon component shown in the toolbar dropdown. Required when `isInBlocksSelector` is `true`. |
| `label` | No | `{ id: string, defaultMessage: string }` | `MessageDescriptor` shown in the toolbar dropdown. Required when `isInBlocksSelector` is `true`. |
| `handleConvert` | No | `(editor: Editor) => void \| (() => React.JSX.Element)` | Called when the user selects this block from the dropdown. Use Slate's `Transforms` to set the node type. Can return a React element factory to render a modal. |
| `handleEnterKey` | No | `(editor: Editor) => void` | Custom Enter key behavior inside this block. |
| `handleBackspaceKey` | No | `(editor: Editor, event: React.KeyboardEvent<HTMLElement>) => void` | Custom Backspace key behavior. |
| `handleTab` | No | `(editor: Editor) => void` | Custom Tab key behavior (e.g., indentation). |
| `handleShiftTab` | No | `(editor: Editor) => void` | Custom Shift+Tab key behavior. |
| `snippets` | No | `string[]` | Typing one of these strings followed by Space triggers a conversion to this block type. |
| `dragHandleTopMargin` | No | `string` | Adjusts the vertical position of the drag-to-reorder grip icon. |
| `plugin` | No | `(editor: Editor) => Editor` | A [Slate plugin](https://docs.slatejs.org/) registered when the editor instance is created. Use this for custom normalizers or Slate-level behavior. |
| `isDraggable` | No | `(element: Element) => boolean` | Function returning whether a given element is draggable. Defaults to `() => true`. |

The built-in block keys are: `paragraph`, `heading-one`, `heading-two`, `heading-three`, `heading-four`, `heading-five`, `heading-six`, `list-ordered`, `list-unordered`, `image`, `quote`, `code`, `link`, `list-item`.

**Key handler example**

Key handlers each receive the Slate `editor` instance. Use Slate's `Transforms` API to modify the document:

```js
callout: {
  // ...
  handleEnterKey(editor) {
    // Exit the block on Enter and insert a new paragraph below
    Transforms.insertNodes(editor, { type: 'paragraph', children: [{ text: '' }] });
  },
  handleBackspaceKey(editor, event) {
    // Convert back to paragraph when backspacing in an empty callout
    Transforms.setNodes(editor, { type: 'paragraph' });
  },
  handleTab(editor) {
    // Increase indentation level on Tab
    Transforms.setNodes(editor, { indent: (editor.selection ? 1 : 0) });
  },
  handleShiftTab(editor) {
    // Decrease indentation level on Shift+Tab
    Transforms.setNodes(editor, { indent: 0 });
  },
},
```

:::tip
More information about types can be found in [Strapi](https://github.com/strapi/strapi/blob/develop/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx).
:::
