diff --git a/src/pages/config.md b/src/pages/config.md
index 32841bc1..e2e3ada4 100644
--- a/src/pages/config.md
+++ b/src/pages/config.md
@@ -11,6 +11,7 @@
- [Universal Editor](/services/aem-universal-editor/index.md)
- [Adobe Commerce Admin](https://developer.adobe.com/commerce/extensibility/admin-ui-sdk/)
- [AEM Assets View](/services/aem-assets-view/index.md)
+ - [Content Hub](/services/contenthub/index.md)
- [AEM Experience Hub](/services/aem-experience-hub/index.md)
- [Extension Manager](/extension-manager/index.md)
@@ -55,6 +56,15 @@
- [Step-by-step Extension Development](/services/aem-assets-view/extension-development/index.md)
- [Code Generation](/services/aem-assets-view/code-generation/index.md)
- [Troubleshooting](/services/aem-assets-view/debug/index.md)
+ - [Content Hub](/services/contenthub/index.md)
+ - [Extension Points](/services/contenthub/api/index.md)
+ - [Common Concepts](/services/contenthub/api/commons/index.md)
+ - [Card Actions](/services/contenthub/api/card-actions/index.md)
+ - [Asset Details Tab Panels](/services/contenthub/api/asset-details/index.md)
+ - [Selection Bar Actions](/services/contenthub/api/selection-bar/index.md)
+ - [Step-by-step Extension Development](/services/contenthub/extension-development/index.md)
+ - [Code Generation](/services/contenthub/code-generation/index.md)
+ - [Troubleshooting](/services/contenthub/debug/index.md)
- [Extension Manager](/extension-manager/index.md)
- [Feature Highlights](/extension-manager/feature-highlights/index.md)
- [Extensions Developed By Adobe](/extension-manager/extension-developed-by-adobe/index.md)
diff --git a/src/pages/services/contenthub/api/asset-details/asset-details-tab-panel.png b/src/pages/services/contenthub/api/asset-details/asset-details-tab-panel.png
new file mode 100644
index 00000000..e97da4d0
Binary files /dev/null and b/src/pages/services/contenthub/api/asset-details/asset-details-tab-panel.png differ
diff --git a/src/pages/services/contenthub/api/asset-details/index.md b/src/pages/services/contenthub/api/asset-details/index.md
new file mode 100644
index 00000000..b722e0bb
--- /dev/null
+++ b/src/pages/services/contenthub/api/asset-details/index.md
@@ -0,0 +1,234 @@
+---
+title: Asset Details Tab Panels - Content Hub Extensibility
+description: Add custom tab panels to the Asset Details dialog in Content Hub.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Asset Details Tab Panels
+
+Content Hub lets extensions add custom tab panels to the **Asset Details dialog** — the dialog that opens when a user clicks on an asset.
+
+
+
+Custom tabs appear alongside the built-in tabs in the Asset Details dialog. Content Hub manages tab switching and lifecycle; the extension only provides tab metadata and the URL that renders the tab body.
+
+Extensions use the `aem/assets/contenthub/1` extension point and implement the `assetDetails` namespace inside a single `register()` call.
+
+## Host API Reference
+
+In addition to the [Common APIs](../commons/index.md), the `assetDetails` namespace exposes one additional method on `guestConnection.host`:
+
+### `assetDetails.getCurrentAsset()`
+
+**Description:** Returns the identifier of the asset currently open in the Asset Details dialog.
+
+**Returns** (`string`): Asset URN (e.g. `urn:aaid:aem:...`).
+
+**Important:** `getCurrentAsset()` returns a **plain string**, not an object. If your downstream code expects an object, wrap it as `{ id: assetId }`; otherwise use the string directly:
+
+```js
+const assetId = await connection.host.assetDetails.getCurrentAsset();
+// use assetId directly, or wrap: const asset = { id: assetId };
+```
+
+## Extension API Reference
+
+### `assetDetails` namespace
+
+#### `assetDetails.getTabPanels()`
+
+**Description:** Returns the list of custom tab panels to add to the Asset Details dialog.
+
+**Returns** (`array`): Array of tab panel descriptor objects:
+- `id` (`string`): Panel ID, unique within the extension.
+- `tooltip` (`string`): Tooltip shown on the tab icon.
+- `title` (`string`): Tab label text.
+- `icon` (`string`): [React Spectrum workflow icon](https://react-spectrum.adobe.com/react-spectrum/workflow-icons.html#available-icons) name.
+- `contentUrl` (`string`): Hash-relative URL to the panel content (e.g. `/#asset-details-extension-tab`).
+
+## Example
+
+This example adds an **Extension Template** panel that displays the current asset's URN and a button that shows a toast notification.
+
+### `ExtensionRegistration.js` — registration
+
+```js
+import React from 'react';
+import { Text } from '@adobe/react-spectrum';
+import { register } from '@adobe/uix-guest';
+import { extensionId } from './Constants';
+
+function ExtensionRegistration() {
+ const init = async () => {
+ let guestConnection = await register({
+ id: extensionId,
+ methods: {
+ assetDetails: {
+ getTabPanels() {
+ return [
+ {
+ id: 'extension-template',
+ tooltip: 'Extension Template',
+ icon: 'Extension',
+ title: 'Extension Template',
+ contentUrl: '/#asset-details-extension-tab',
+ },
+ ];
+ },
+ },
+ },
+ });
+ };
+
+ init().catch(console.error);
+ return IFrame for integration with Host (Content Hub)...;
+}
+
+export default ExtensionRegistration;
+```
+
+### `App.js` — routing
+
+```js
+import React from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+import { HashRouter as Router, Routes, Route } from 'react-router-dom';
+import ExtensionRegistration from './ExtensionRegistration';
+import PanelAssetDetailsExtensionTab from './PanelAssetDetailsExtensionTab';
+
+function App() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+
+
+
+ );
+
+ function onError(e, componentStack) {}
+ function fallbackComponent({ componentStack, error }) {
+ return (
+
+
Extension rendering error
+
{componentStack + '\n' + error.message}
+
+ );
+ }
+}
+
+export default App;
+```
+
+### `PanelAssetDetailsExtensionTab.js` — panel content
+
+The panel component calls `attach()` to connect to Content Hub and retrieves the current asset. Note that `getCurrentAsset()` is **asynchronous** and returns a plain string — normalize it to `{ id }`.
+
+```js
+import React, { useState, useEffect } from 'react';
+import { attach } from '@adobe/uix-guest';
+import {
+ Provider,
+ defaultTheme,
+ View,
+ Heading,
+ Text,
+ Button,
+ Divider,
+ ProgressCircle,
+} from '@adobe/react-spectrum';
+import { extensionId } from './Constants';
+
+export default function PanelAssetDetailsExtensionTab() {
+ const [guestConnection, setGuestConnection] = useState(null);
+ const [asset, setAsset] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const connection = await attach({ id: extensionId });
+ setGuestConnection(connection);
+
+ // getCurrentAsset() returns the asset id as a plain string (e.g. "urn:aaid:aem:...").
+ const assetId = await connection.host.assetDetails.getCurrentAsset();
+ setAsset({ id: assetId });
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Extension Template
+
+ {asset && (
+
+ Asset ID:
+
+
+ {asset.id}
+
+
+
+ )}
+
+
+
+ );
+}
+```
+
+## Calling a backend web action from the panel
+
+To make AEM API calls from the panel, retrieve auth info from the host and call your Adobe I/O Runtime action:
+
+```js
+// Add this import at the top of PanelAssetDetailsExtensionTab.js alongside the other imports:
+import actions from '../config.json';
+
+// Inside the useEffect, after attach():
+const { accessToken, imsOrg } = await connection.host.auth.getIMSInfo();
+const apiKey = await connection.host.auth.getApiKey();
+const aemHost = await connection.host.discovery.getAemHost();
+
+const response = await fetch(actions['aem-assets-contenthub-1/generic'], {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ assetId: asset.id, aemHost, apiKey, imsOrg }),
+});
+const data = await response.json();
+```
+
+`config.json` is generated by the CLI template and contains the deployed web action URLs keyed by action name.
+
+## Additional resources
+
+- [Common Concepts](../commons/index.md)
+- [Card Actions](../card-actions/index.md)
+- [Selection Bar Actions](../selection-bar/index.md)
+- [Step-by-step Extension Development](../../extension-development/index.md)
+- [Troubleshooting](../../debug/index.md)
diff --git a/src/pages/services/contenthub/api/card-actions/asset-card-actions.png b/src/pages/services/contenthub/api/card-actions/asset-card-actions.png
new file mode 100644
index 00000000..2f342285
Binary files /dev/null and b/src/pages/services/contenthub/api/card-actions/asset-card-actions.png differ
diff --git a/src/pages/services/contenthub/api/card-actions/collection-card-actions.png b/src/pages/services/contenthub/api/card-actions/collection-card-actions.png
new file mode 100644
index 00000000..fc3fa875
Binary files /dev/null and b/src/pages/services/contenthub/api/card-actions/collection-card-actions.png differ
diff --git a/src/pages/services/contenthub/api/card-actions/index.md b/src/pages/services/contenthub/api/card-actions/index.md
new file mode 100644
index 00000000..5972695f
--- /dev/null
+++ b/src/pages/services/contenthub/api/card-actions/index.md
@@ -0,0 +1,231 @@
+---
+title: Card Actions - Content Hub Extensibility
+description: Add custom action buttons to asset cards and collection tiles in Content Hub.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Card Actions
+
+Content Hub lets extensions add custom action buttons to the **asset card menu** and the **collection tile menu** using a single shared `card` namespace.
+
+
+
+
+
+Card actions appear wherever asset cards or collection tiles are rendered. Use the `context` value passed to each method to differentiate the surface:
+
+| `context` value | Surface |
+|---|---|
+| `'assets'` | Main Assets browse grid |
+| `'collection'` | Asset card inside an open collection |
+| `'collections'` | Collection tile on the Collections grid |
+| `'share'` | Asset card in a link-share view |
+
+Extensions use the `aem/assets/contenthub/1` extension point and implement the `card` namespace inside a single `register()` call.
+
+## Extension API Reference
+
+### `card` namespace
+
+#### `card.getActionButtons(actionContext)`
+
+**Description:** Returns the list of custom buttons to add to the card menu for the given context.
+
+**Parameters:**
+- `actionContext` (`object`):
+ - `context` (`string`): The surface where the card is rendered — `'assets'`, `'collection'`, `'collections'`, or `'share'`.
+
+**Returns** (`array`): An array of button descriptor objects. Each object contains:
+- `id` (`string`): Unique identifier for the button within the extension.
+- `label` (`string`): Button label shown in the menu.
+- `icon` (`string`): [React Spectrum workflow icon](https://react-spectrum.adobe.com/react-spectrum/workflow-icons.html#available-icons) name.
+
+Return an empty array if no buttons should be shown for the given context.
+
+#### `card.onActionClick(resourceType, buttonId, resourceId, actionContext)`
+
+**Description:** Called by Content Hub when the user clicks a custom card button.
+
+**Parameters:**
+- `resourceType` (`string`): `'asset'` for asset cards; `'collection'` for collection tiles.
+- `buttonId` (`string`): The `id` of the button that was clicked.
+- `resourceId` (`string`): The URN or ID of the asset or collection the card represents.
+- `actionContext` (`object`): Same context object passed to `getActionButtons`.
+
+## Example
+
+This example adds a custom button to asset cards (main Assets grid, inside collections) and to collection tiles (Collections grid). A single `card` registration block and a single modal component serve both surfaces — `resourceType` tells the modal which kind of resource was clicked.
+
+### `App.js` — routing
+
+```js
+import React from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+import { HashRouter as Router, Routes, Route } from 'react-router-dom';
+import ExtensionRegistration from './ExtensionRegistration';
+import CardActionModal from './CardActionModal';
+
+function App() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+
+
+
+ );
+
+ function onError(e, componentStack) {}
+ function fallbackComponent({ componentStack, error }) {
+ return (
+
+
Extension rendering error
+
{componentStack + '\n' + error.message}
+
+ );
+ }
+}
+
+export default App;
+```
+
+### `ExtensionRegistration.js` — registration
+
+```js
+import React from 'react';
+import { Text } from '@adobe/react-spectrum';
+import { register } from '@adobe/uix-guest';
+import { extensionId } from './Constants';
+
+function ExtensionRegistration() {
+ const init = async () => {
+ let guestConnection = await register({
+ id: extensionId,
+ methods: {
+ card: {
+ getActionButtons(actionContext) {
+ const { context } = actionContext || {};
+ // Show the button on asset cards and on collection tiles
+ if (context !== 'assets' && context !== 'collection' && context !== 'collections') {
+ return [];
+ }
+ return [
+ {
+ id: 'customId',
+ label: 'Custom label',
+ icon: 'Form',
+ },
+ ];
+ },
+ async onActionClick(resourceType, buttonId, resourceId, actionContext) {
+ if (buttonId === 'customId') {
+ await guestConnection.host.modal.openDialog({
+ title: 'Custom Dialog',
+ contentUrl: `/#card-action-modal?resourceId=${encodeURIComponent(resourceId)}&resourceType=${encodeURIComponent(resourceType)}`,
+ type: 'modal',
+ size: 'M',
+ });
+ }
+ },
+ },
+ },
+ });
+ };
+
+ init().catch(console.error);
+ return IFrame for integration with Host (Content Hub)...;
+}
+
+export default ExtensionRegistration;
+```
+
+### `CardActionModal.js` — dialog content
+
+```js
+import React, { useState, useEffect } from 'react';
+import { attach } from '@adobe/uix-guest';
+import {
+ Provider,
+ defaultTheme,
+ View,
+ Heading,
+ Text,
+ Button,
+ ButtonGroup,
+ Divider,
+ ProgressCircle,
+} from '@adobe/react-spectrum';
+import { extensionId } from './Constants';
+
+export default function CardActionModal() {
+ const [guestConnection, setGuestConnection] = useState(null);
+ const [payload, setPayload] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ const connection = await attach({ id: extensionId });
+ setGuestConnection(connection);
+
+ // Read data passed via URL query parameters
+ const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
+ setPayload({
+ resourceId: params.get('resourceId'),
+ resourceType: params.get('resourceType'),
+ });
+ })();
+ }, []);
+
+ if (!payload) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const handleExport = async () => {
+ // Add your export logic here
+ await guestConnection?.host.toast.display({
+ variant: 'positive',
+ message: `Custom action on ${payload.resourceType}: ${payload.resourceId}`,
+ });
+ guestConnection?.host.modal.closeDialog();
+ };
+
+ return (
+
+
+ Custom Dialog
+
+
+ Type: {payload.resourceType}
+
+
+ {payload.resourceId}
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+## Additional resources
+
+- [Common Concepts](../commons/index.md)
+- [Asset Details Tab Panels](../asset-details/index.md)
+- [Selection Bar Actions](../selection-bar/index.md)
+- [Step-by-step Extension Development](../../extension-development/index.md)
+- [Troubleshooting](../../debug/index.md)
diff --git a/src/pages/services/contenthub/api/commons/index.md b/src/pages/services/contenthub/api/commons/index.md
new file mode 100644
index 00000000..33e69126
--- /dev/null
+++ b/src/pages/services/contenthub/api/commons/index.md
@@ -0,0 +1,291 @@
+---
+title: Common Concepts - Content Hub Extensibility
+description: Learn about extension registration and common Host APIs available to every Content Hub extension.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Common Concepts in Creating Extensions
+
+This page explains the fundamentals shared by every Content Hub extension: the extension point, how to register an extension, and the Host APIs that are available to all surfaces.
+
+## Extension Point
+
+All Content Hub UI extensions use the `aem/assets/contenthub/1` extension point.
+
+Declare it in your `app.config.yaml`:
+
+```yaml
+extensions:
+ aem/assets/contenthub/1:
+ $include: src/aem-assets-contenthub-1/ext.config.yaml
+```
+
+A typical `ext.config.yaml`:
+
+```yaml
+operations:
+ view:
+ - type: web
+ impl: index.html
+actions: actions
+web: web-src
+```
+
+## Extension Registration
+
+An extension announces its capabilities to Content Hub by calling `register()` from `@adobe/uix-guest`. This function is asynchronous and returns a connection object used to interact with the host.
+
+All three namespaces (`assetDetails`, `card`, `selectionBar`) are declared in a single `register()` call.
+
+Use `let` for `guestConnection` so the `card` and `selectionBar` click handlers can close over it after `register()` resolves.
+
+```js
+import { register } from '@adobe/uix-guest';
+
+const init = async () => {
+ let guestConnection = await register({
+ id: extensionId,
+ methods: {
+ assetDetails: {
+ getTabPanels() { /* ... */ },
+ },
+ card: {
+ getActionButtons(actionContext) { /* ... */ },
+ async onActionClick(resourceType, buttonId, resourceId, actionContext) { /* ... */ },
+ },
+ selectionBar: {
+ getActionButtons(actionContext) { /* ... */ },
+ async onActionClick(buttonId, assetIds) { /* ... */ },
+ },
+ },
+ });
+};
+init().catch(console.error);
+```
+
+`register()` must be called from the extension's initialization page after it loads.
+
+The object passed to `register()` must include:
+- `id` — a unique string identifier for the extension (used for debugging and for `attach()` calls from panel/modal components).
+- `methods` — namespaces that correspond to Content Hub extension surfaces.
+
+## Restricting extensions to specific repositories
+
+For production extensions you should restrict registration to specific Content Hub repositories. Content Hub injects the current repository ID as a `repo` query parameter on the extension URL.
+
+```js
+// Populate before deploying to Production. Empty = allow any repo (safe for dev).
+const allowedRepos = [
+ 'delivery-p12345-e167890.adobeaemcloud.com',
+];
+
+function getRepo() {
+ const search = new URLSearchParams(window.location.search);
+ return search.get('repo');
+}
+
+function shouldSkipRegistration(repo) {
+ return allowedRepos.length > 0 && !allowedRepos.includes(repo);
+}
+
+function ExtensionRegistration() {
+ const repo = getRepo();
+ if (shouldSkipRegistration(repo)) {
+ return Skipped registration: repo not in allowedRepos;
+ }
+ // proceed with init() and register() ...
+}
+```
+
+## Building Extension UI
+
+Extension panels and modal dialogs are rendered inside iframes. A panel or dialog component calls `attach()` to get the connection object:
+
+```js
+import { attach } from '@adobe/uix-guest';
+
+const guestConnection = await attach({ id: extensionId });
+```
+
+After attaching, the component can call Host APIs on `guestConnection.host`.
+
+## Passing data to modal dialogs
+
+The `modal.openDialog()` `contentUrl` is a hash-relative URL rendered inside an iframe. Pass data to a dialog via **URL query parameters** embedded in `contentUrl`:
+
+```js
+// In ExtensionRegistration.js — opening the dialog:
+await guestConnection.host.modal.openDialog({
+ title: 'Custom Dialog',
+ contentUrl: `/#my-dialog?assetId=${encodeURIComponent(assetId)}`,
+ type: 'modal',
+ size: 'M',
+});
+
+// In the dialog component — reading the data:
+const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
+const assetId = params.get('assetId');
+```
+
+## Common APIs exposed by Content Hub to all extensions
+
+The APIs below are available to every Content Hub extension regardless of which surface it extends. All API calls are asynchronous and return a `Promise`.
+
+---
+
+### Authentication API (`auth`)
+
+#### `auth.getIMSInfo()`
+
+**Description:** Returns IMS organization information and the current access token.
+
+**Returns:**
+- `imsOrg` (`string`): IMS organization identifier.
+- `imsOrgName` (`string`): Human-readable IMS organization name.
+- `accessToken` (`string`): Current IMS access token.
+
+**Example:**
+
+```js
+const { imsOrg, imsOrgName, accessToken } = await guestConnection.host.auth.getIMSInfo();
+```
+
+#### `auth.getApiKey()`
+
+**Description:** Returns the API key used by Content Hub.
+
+**Returns** (`string`): API key.
+
+**Example:**
+
+```js
+const apiKey = await guestConnection.host.auth.getApiKey();
+```
+
+---
+
+### Discovery API (`discovery`)
+
+#### `discovery.getAemHost()`
+
+**Description:** Returns the AEM host URL of the delivery repository connected to this Content Hub instance.
+
+**Returns** (`string`): Full AEM host URL including protocol and trailing slash (e.g. `https://delivery-p12345-e123456.adobeaemcloud.com/`).
+
+**Example:**
+
+```js
+const aemHost = await guestConnection.host.discovery.getAemHost();
+// e.g. "https://delivery-p12345-e123456.adobeaemcloud.com/"
+```
+
+---
+
+### Toast API (`toast`)
+
+#### `toast.display({ variant, message })`
+
+**Description:** Shows a toast notification in Content Hub.
+
+**Parameters:**
+- `variant` (`string`, optional): `'neutral'`, `'positive'`, `'negative'`, or `'info'`. Defaults to `'info'`.
+- `message` (`string`, required): Text to display.
+
+**Example:**
+
+```js
+guestConnection.host.toast.display({ variant: 'positive', message: 'Asset exported successfully' });
+```
+
+---
+
+### Internationalization API (`i18n`)
+
+#### `i18n.getLocalizationInfo()`
+
+**Description:** Returns the locale currently active in Content Hub.
+
+**Returns:**
+- `locale` (`string`): BCP 47 locale tag (e.g. `en-US`, `fr-FR`).
+
+**Example:**
+
+```js
+const { locale } = await guestConnection.host.i18n.getLocalizationInfo();
+```
+
+---
+
+### Modal API (`modal`)
+
+#### `modal.openDialog(options)`
+
+**Description:** Opens a dialog. The dialog content is loaded from `contentUrl`.
+
+**Parameters (`options` object):**
+- `title` (`string`, required): Dialog heading.
+- `contentUrl` (`string`, required): Hash-relative URL to the dialog content page within the extension (e.g. `/#my-dialog`). To pass data, either embed it as URL query parameters (e.g. `/#my-dialog?assetId=...`) or use the `payload` field below.
+- `type` (`string`, optional): `'modal'` (default) or `'fullscreen'`.
+- `size` (`string`, optional): `'S'`, `'M'` (default), or `'L'`. Ignored when `type` is `'fullscreen'`.
+- `payload` (`any`, optional): Arbitrary data object the dialog component can retrieve with `modal.getPayload()`.
+
+**Recommended pattern — pass data via URL query parameters** (simpler, synchronous, works without async `getPayload()` round-trip):
+
+```js
+await guestConnection.host.modal.openDialog({
+ title: 'Custom Dialog',
+ contentUrl: `/#export-dialog?assetId=${encodeURIComponent(assetId)}`,
+ type: 'modal',
+ size: 'M',
+});
+
+// Inside the dialog component — read from the URL:
+const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
+const assetId = params.get('assetId');
+```
+
+**Alternative — pass data via `payload`:**
+
+```js
+// In ExtensionRegistration.js — opening the dialog with a payload:
+await guestConnection.host.modal.openDialog({
+ title: 'Custom Dialog',
+ contentUrl: '/#export-dialog',
+ type: 'modal',
+ size: 'M',
+ payload: { assetId },
+});
+```
+
+```js
+// Inside the dialog component — attach() first, then read the payload:
+const connection = await attach({ id: extensionId });
+const { assetId } = await connection.host.modal.getPayload();
+```
+
+#### `modal.closeDialog()`
+
+**Description:** Closes the currently active dialog.
+
+**Example:**
+
+```js
+guestConnection.host.modal.closeDialog();
+// or using optional chaining from a dialog component:
+guestConnection?.host.modal.closeDialog();
+```
+
+#### `modal.getPayload()`
+
+**Description:** Returns the payload object passed when `modal.openDialog()` was called. Call this from the dialog component after `attach()`.
+
+**Returns:** The payload value passed in `openDialog({ payload })`, or `undefined` if no payload was provided.
+
+**Example:**
+
+```js
+// In the dialog component:
+const connection = await attach({ id: extensionId });
+const payload = await connection.host.modal.getPayload();
+```
diff --git a/src/pages/services/contenthub/api/index.md b/src/pages/services/contenthub/api/index.md
new file mode 100644
index 00000000..d7642d7d
--- /dev/null
+++ b/src/pages/services/contenthub/api/index.md
@@ -0,0 +1,36 @@
+---
+title: Content Hub Extensibility - Extension Points
+description: Browse all available extension surfaces in Content Hub.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Content Hub Extension Points
+
+This section covers the available extension surfaces, extension registration, and common methods available to every Content Hub extension.
+
+All Content Hub extensions use the **`aem/assets/contenthub/1`** extension point.
+
+
+
+[Common Concepts in Creating Extensions](commons/index.md)
+
+Learn about extension registration, the Host API, and APIs available to every extension (auth, discovery, toast, i18n, modal)
+
+
+
+[Card Actions](card-actions/index.md)
+
+Add custom action buttons to asset cards and collection tiles in the Assets grid, inside Collections, and in-app Link Share
+
+
+
+[Asset Details Tab Panels](asset-details/index.md)
+
+Add custom tab panels to the Asset Details dialog
+
+
+
+[Selection Bar](selection-bar/index.md)
+
+Add custom bulk-action buttons to the blue Selection Bar that appears when one or more assets are selected
diff --git a/src/pages/services/contenthub/api/selection-bar/index.md b/src/pages/services/contenthub/api/selection-bar/index.md
new file mode 100644
index 00000000..9d0236c6
--- /dev/null
+++ b/src/pages/services/contenthub/api/selection-bar/index.md
@@ -0,0 +1,229 @@
+---
+title: Selection Bar Actions - Content Hub Extensibility
+description: Add custom bulk-action buttons to the Content Hub Selection Bar.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Selection Bar Actions
+
+Content Hub lets extensions add custom buttons to the **Selection Bar** — the bar that appears at the top of the screen when one or more assets are selected.
+
+
+
+Selection Bar actions allow bulk operations: the extension receives the IDs of all selected assets when the button is clicked.
+
+Extensions use the `aem/assets/contenthub/1` extension point and implement the `selectionBar` namespace inside a single `register()` call.
+
+## Extension API Reference
+
+### `selectionBar` namespace
+
+#### `selectionBar.getActionButtons(actionContext)`
+
+**Description:** Returns the list of custom buttons to add to the Selection Bar.
+
+**Parameters:**
+- `actionContext` (`object`):
+ - `context` (`string`): The surface where assets are selected — `'assets'`, `'collection'`, `'collections'`, or `'share'`.
+ - `resourceSelection` (`object`):
+ - `resources` (`array`): Array of selected resource objects, each with an `id` property.
+
+**Returns** (`array`): An array of button descriptor objects:
+- `id` (`string`): Unique identifier for the button within the extension.
+- `label` (`string`): Button label shown in the Selection Bar.
+- `icon` (`string`): [React Spectrum workflow icon](https://react-spectrum.adobe.com/react-spectrum/workflow-icons.html#available-icons) name.
+
+Return an empty array if no buttons should be shown.
+
+#### `selectionBar.onActionClick(buttonId, assetIds)`
+
+**Description:** Called by Content Hub when the user clicks a custom Selection Bar button.
+
+**Parameters:**
+- `buttonId` (`string`): The `id` of the button that was clicked.
+- `assetIds` (`array`): Array of asset ID strings for all currently selected assets.
+
+## Example
+
+This example adds a custom button to the Selection Bar that opens a dialog showing all selected asset IDs.
+
+### `App.js` — routing
+
+```js
+import React from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+import { HashRouter as Router, Routes, Route } from 'react-router-dom';
+import ExtensionRegistration from './ExtensionRegistration';
+import SelectionBarModal from './SelectionBarModal';
+
+function App() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+
+
+
+ );
+
+ function onError(e, componentStack) {}
+ function fallbackComponent({ componentStack, error }) {
+ return (
+
+
Extension rendering error
+
{componentStack + '\n' + error.message}
+
+ );
+ }
+}
+
+export default App;
+```
+
+### `ExtensionRegistration.js` — registration
+
+```js
+import React from 'react';
+import { Text } from '@adobe/react-spectrum';
+import { register } from '@adobe/uix-guest';
+import { extensionId } from './Constants';
+
+function ExtensionRegistration() {
+ const init = async () => {
+ let guestConnection = await register({
+ id: extensionId,
+ methods: {
+ selectionBar: {
+ getActionButtons(actionContext) {
+ const { context } = actionContext || {};
+ // Show button only on the main assets grid
+ if (context !== 'assets') {
+ return [];
+ }
+ return [
+ {
+ id: 'customId',
+ label: 'Custom label',
+ icon: 'Form',
+ },
+ ];
+ },
+ async onActionClick(buttonId, assetIds) {
+ if (buttonId === 'customId') {
+ await guestConnection.host.modal.openDialog({
+ title: `Custom Dialog (${assetIds.length} asset${assetIds.length !== 1 ? 's' : ''} selected)`,
+ contentUrl: `/#selection-bar-modal?assetIds=${encodeURIComponent(JSON.stringify(assetIds))}`,
+ type: 'modal',
+ size: 'M',
+ });
+ }
+ },
+ },
+ },
+ });
+ };
+
+ init().catch(console.error);
+ return IFrame for integration with Host (Content Hub)...;
+}
+
+export default ExtensionRegistration;
+```
+
+### `SelectionBarModal.js` — dialog content
+
+The assetIds are passed as a JSON-encoded URL parameter and decoded inside the dialog component.
+
+```js
+import React, { useState, useEffect } from 'react';
+import { attach } from '@adobe/uix-guest';
+import {
+ Provider,
+ defaultTheme,
+ View,
+ Heading,
+ Text,
+ Button,
+ ButtonGroup,
+ Divider,
+ ListView,
+ Item,
+ ProgressCircle,
+} from '@adobe/react-spectrum';
+import { extensionId } from './Constants';
+
+export default function SelectionBarModal() {
+ const [guestConnection, setGuestConnection] = useState(null);
+ const [payload, setPayload] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ const connection = await attach({ id: extensionId });
+ setGuestConnection(connection);
+
+ // assetIds are passed as a JSON-encoded URL parameter
+ const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
+ const raw = params.get('assetIds');
+ setPayload({
+ assetIds: raw ? JSON.parse(raw) : [],
+ });
+ })();
+ }, []);
+
+ if (!payload) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const handleExport = async () => {
+ // Add your bulk export logic here
+ await guestConnection?.host.toast.display({
+ variant: 'positive',
+ message: `Custom action on ${payload.assetIds.length} asset(s)`,
+ });
+ guestConnection?.host.modal.closeDialog();
+ };
+
+ return (
+
+
+ Custom Dialog — {payload.assetIds.length} asset(s) selected
+
+ ({ id, name: id }))}
+ height="size-2400"
+ aria-label="Selected assets"
+ >
+ {(item) => (
+
+ {item.name}
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+```
+
+## Additional resources
+
+- [Common Concepts](../commons/index.md)
+- [Card Actions](../card-actions/index.md)
+- [Asset Details Tab Panels](../asset-details/index.md)
+- [Step-by-step Extension Development](../../extension-development/index.md)
+- [Troubleshooting](../../debug/index.md)
diff --git a/src/pages/services/contenthub/api/selection-bar/selection-bar.png b/src/pages/services/contenthub/api/selection-bar/selection-bar.png
new file mode 100644
index 00000000..ba6d2987
Binary files /dev/null and b/src/pages/services/contenthub/api/selection-bar/selection-bar.png differ
diff --git a/src/pages/services/contenthub/code-generation/index.md b/src/pages/services/contenthub/code-generation/index.md
new file mode 100644
index 00000000..f676961e
--- /dev/null
+++ b/src/pages/services/contenthub/code-generation/index.md
@@ -0,0 +1,153 @@
+---
+title: Sample Extension - Content Hub Extensibility
+description: Get started quickly by cloning the Content Hub sample extension from adobe/aem-uix-examples.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Content Hub Sample Extension
+
+The quickest way to start building a Content Hub extension is to clone the official sample from [adobe/aem-uix-examples](https://github.com/adobe/aem-uix-examples). The sample is a working App Builder project that demonstrates all three extension surfaces — Asset Details tab panels, asset card actions, and Selection Bar bulk actions — in a single extension.
+
+Clone it, run it locally, then delete the parts you don't need and replace the rest with your business logic.
+
+## Prerequisites
+
+- Node.js 18 or higher
+- [Adobe I/O CLI](https://developer.adobe.com/runtime/docs/guides/tools/cli_install/): `npm install -g @adobe/aio-cli`
+- An Adobe Developer Console account with an App Builder project (I/O Runtime must be enabled on the project)
+- Access to a Content Hub environment with the extensibility feature flag enabled
+
+## Clone the sample
+
+```shell
+git clone https://github.com/adobe/aem-uix-examples.git
+cd aem-uix-examples/aem-contenthub-sample
+```
+
+## Project structure
+
+```text
+aem-contenthub-sample/
+├── app.config.yaml
+├── extension-manifest.json
+├── hooks/
+│ └── post-deploy.js
+├── package.json
+└── src/
+ └── aem-assets-contenthub-1/
+ ├── actions/
+ │ └── generic/
+ │ └── index.js ← server-side web action (optional)
+ ├── ext.config.yaml
+ └── web-src/
+ └── src/
+ └── components/
+ ├── App.js ← routing
+ ├── Constants.js ← extension ID
+ ├── ExtensionRegistration.js ← registers all namespaces
+ ├── PanelAssetDetailsExtensionTab.js ← assetDetails tab panel
+ ├── CardActionModal.js ← card action modal
+ └── SelectionBarModal.js ← selection bar modal
+```
+
+```yaml
+# app.config.yaml — declares the extension point
+extensions:
+ aem/assets/contenthub/1:
+ $include: src/aem-assets-contenthub-1/ext.config.yaml
+```
+
+`ExtensionRegistration.js` is the entry point — it registers all three namespaces with Content Hub. `App.js` maps URL routes to each panel or modal component.
+
+## Setup and run locally
+
+```shell
+# 1. Install dependencies
+npm install
+
+# 2. Log in to Adobe I/O
+aio login
+
+# 3. Select your org, project, and workspace
+aio console org select
+aio console project select
+aio console workspace select
+
+# 4. Link the app to the selected project and populate .env
+aio app use -g
+
+# 5. Start the local dev server (https://localhost:9080)
+aio app run
+```
+
+## Accept the self-signed certificate
+
+The first time you run the dev server, open `https://localhost:9080` in your browser and accept the certificate (click **Advanced → Proceed to localhost**, or type `thisisunsafe` on the warning page).
+
+You only need to do this once per browser session.
+
+## Load the extension in Content Hub
+
+Open Content Hub with `devMode=true` and `ext=` pointing to your local server:
+
+```text
+https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/
+```
+
+## Verify each surface
+
+**Asset Details tab panel (`assetDetails`)**
+1. Click any asset to open the Asset Details dialog
+2. Look for the **"Asset Details Tab"** in the side rail
+3. Click it — the panel loads and displays the asset's URN
+
+**Asset card action (`card`)**
+1. Hover over any asset card — a **"Card Action"** button appears
+2. Click it — a modal opens showing the resource type and ID
+
+**Collection tile action (`card`)**
+1. Navigate to the Collections grid
+2. Hover over a collection tile — a **"Collection Action"** button appears
+3. Click it — a modal opens showing `resourceType: collection`
+
+**Selection Bar bulk action (`selectionBar`)**
+1. Select one or more assets
+2. The Selection Bar appears — click **"Bulk Action"**
+3. A modal opens listing all selected asset URNs
+
+## Customize the sample
+
+Open `ExtensionRegistration.js` and edit the three namespace blocks to implement your logic. Remove any namespace block you don't need, and delete the matching route from `App.js` and the matching component file.
+
+For a detailed walkthrough of each surface and its API, see the [Extension Development guide](../extension-development/index.md) and the [API reference](../api/index.md).
+
+## Restrict to specific repositories
+
+`ExtensionRegistration.js` has an `allowedRepos` array that is empty by default — the extension registers for any Content Hub repository, which is correct for local development. Before deploying to Production, add your delivery repository IDs:
+
+```js
+const allowedRepos = [
+ 'delivery-p12345-e167890.adobeaemcloud.com',
+];
+```
+
+## Deploy
+
+```shell
+# Deploy to Stage
+aio app use -w Stage
+aio app deploy
+
+# Deploy to Production
+aio app use -w Production
+aio app deploy
+```
+
+After deploying to Production, approve the extension in [Extension Manager](https://experience.adobe.com/aem/extension-manager) to make it visible to all users without the `ext=` parameter.
+
+## Additional resources
+
+- [Step-by-step Extension Development](../extension-development/index.md)
+- [API Reference](../api/index.md)
+- [Troubleshooting](../debug/index.md)
diff --git a/src/pages/services/contenthub/debug/cert-1.png b/src/pages/services/contenthub/debug/cert-1.png
new file mode 100644
index 00000000..efcd5307
Binary files /dev/null and b/src/pages/services/contenthub/debug/cert-1.png differ
diff --git a/src/pages/services/contenthub/debug/cert-2.png b/src/pages/services/contenthub/debug/cert-2.png
new file mode 100644
index 00000000..7fe58e3a
Binary files /dev/null and b/src/pages/services/contenthub/debug/cert-2.png differ
diff --git a/src/pages/services/contenthub/debug/index.md b/src/pages/services/contenthub/debug/index.md
new file mode 100644
index 00000000..6b7a43a6
--- /dev/null
+++ b/src/pages/services/contenthub/debug/index.md
@@ -0,0 +1,143 @@
+---
+title: Troubleshooting - Content Hub Extensibility
+description: Connect a locally running extension to Content Hub and resolve common issues.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Troubleshooting - Content Hub Extensibility
+
+Fast feedback is essential for development. You can connect a locally running Content Hub extension to the production Content Hub environment to see your changes immediately without deploying.
+
+## Running in a local environment
+
+There are two ways to run an extension locally:
+
+### Option 1: Complete local isolation
+
+Both the extension UI and serverless actions run on your machine.
+
+```shell
+➜ my-contenthub-extension % aio app dev
+```
+
+```shell
+To view your local application:
+ -> https://localhost:9080
+To view your deployed application in the Experience Cloud shell:
+ -> https://experience.adobe.com/?devMode=true#/custom-apps/?localDevUrl=https://localhost:9080
+Your actions:
+web actions:
+ -> https://localhost:9080/api/v1/web/aem-assets-contenthub-1/my-action
+press CTRL+C to terminate the dev environment
+```
+
+### Option 2: Local UI, cloud actions
+
+The extension UI runs locally; serverless actions are deployed to Adobe I/O Runtime.
+
+```shell
+➜ my-contenthub-extension % aio app run
+```
+
+```shell
+For a developer preview of your UI extension in the Content Hub environment, follow the URL:
+ -> https://experience.adobe.com/aem/extension-manager/preview/
+
+To view your local application:
+ -> https://localhost:9080
+To view your deployed application in the Experience Cloud shell:
+ -> https://experience.adobe.com/?devMode=true#/custom-apps/?localDevUrl=https://localhost:9080
+press CTRL+C to terminate dev environment
+```
+
+### Extension endpoint
+
+The local extension is served at the URL shown next to `To view your local application` — typically `https://localhost:9080`. You will use this URL to load the extension in Content Hub.
+
+## Accept the Certificate
+
+The first time you run the extension locally, you will see:
+
+```shell
+success: generated certificate
+A self signed development certificate has been generated, you will need to accept it in your browser in order to use it.
+Waiting for the certificate to be accepted....
+```
+
+1. Navigate to `https://localhost:9080` in Google Chrome.
+
+
+
+2. Click **Advanced**, then click **Proceed to localhost (unsafe)**.
+
+
+
+In Chrome you can also type `thisisunsafe` on the warning page to bypass it. Refer to your browser's documentation for other browsers.
+
+
+## Load UI Extension
+
+Once the extension is running locally, embed it in Content Hub:
+
+1. Navigate to Content Hub: `https://experience.adobe.com/#/assets/contenthub/`
+2. Append the following query parameters to the URL:
+ - `devMode=true` — tells Adobe Experience Shell to allow content from localhost.
+ - `ext=` — the full URL of your local extension.
+3. Press Enter to reload Content Hub with the extension loaded.
+
+**Example URL:**
+
+```text
+https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/
+```
+
+You can specify multiple `ext=` parameters to load more than one extension simultaneously:
+
+```text
+https://experience.adobe.com/?devMode=true&ext=https://localhost:9080&ext=https://localhost:9081#/assets/contenthub/
+```
+
+### `ext=` query parameter syntax
+
+The full syntax of the `ext=` parameter is:
+
+```text
+ext.=[,]
+```
+
+For Content Hub, the extension point ID is `aem/assets/contenthub/1`. URL-encoded:
+
+```text
+ext.aem%2fassets%2fcontenthub%2f1=https://localhost:9080
+```
+
+Using the shorthand `ext=` (without specifying the extension point) applies the extension to all available extension points:
+
+```text
+ext=https://localhost:9080
+```
+
+## Common issues
+
+### The extension panel or action does not appear
+
+- Verify you accepted the self-signed certificate at `https://localhost:9080`.
+- Check that `devMode=true` is in the URL.
+- Open the browser DevTools console and look for errors from the UIX SDK (messages prefixed with `[uix]`).
+- Confirm your `register()` call specifies the correct namespace and method names (e.g. `assetDetails.getTabPanels`).
+
+### Toast or modal does not appear
+
+- Ensure the component called `attach()` to connect to Content Hub.
+- All `host.*` API calls are asynchronous; make sure you are awaiting the promises where required.
+
+### `register()` is never called
+
+- Check your `App.js` routes — the route that renders `ExtensionRegistration` must match the URL loaded in the iframe (typically the index route).
+
+## Additional resources
+
+- [Step-by-step Extension Development](../extension-development/index.md)
+- [UI Extensions Development Flow](../../../guides/development-flow/index.md)
+- [FAQ](../../../getting-started/faq/index.md)
diff --git a/src/pages/services/contenthub/extension-development/extension-on-stage.png b/src/pages/services/contenthub/extension-development/extension-on-stage.png
new file mode 100644
index 00000000..99ba333b
Binary files /dev/null and b/src/pages/services/contenthub/extension-development/extension-on-stage.png differ
diff --git a/src/pages/services/contenthub/extension-development/index.md b/src/pages/services/contenthub/extension-development/index.md
new file mode 100644
index 00000000..0d76ba13
--- /dev/null
+++ b/src/pages/services/contenthub/extension-development/index.md
@@ -0,0 +1,581 @@
+---
+title: Step-by-step Extension Development - Content Hub Extensibility
+description: Build and deploy a complete Content Hub UI extension from scratch.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Step-by-step Extension Development
+
+This guide walks through building a complete Content Hub extension that implements all three extension surfaces — **Asset Details tab panel**, **asset card actions**, and **Selection Bar actions** — in a single extension. By the end you will have a working extension running locally, tested against a live Content Hub environment, and deployed to Stage.
+
+## About the extension
+
+The extension built in this guide adds:
+- A custom **tab panel** (Extension Template) to the Asset Details dialog showing the current asset's ID
+- A custom button on asset cards and collection tiles (shown in the Assets grid, inside collections, and on the Collections grid)
+- A custom button in the Selection Bar for bulk operations on selected assets
+
+The extension demonstrates how all three surfaces are registered in a single `register()` call and how modal data is passed via URL query parameters.
+
+## Create a project in Adobe Developer Console
+
+UI Extensions are represented as projects in [Adobe Developer Console](https://developer.adobe.com/developer-console/docs/guides/).
+
+
+
+If you don't have access to Adobe Developer Console, refer to the [How to Get Access](../../../guides/get-access/index.md) guide.
+
+1. Sign in to [Adobe Developer Console](https://developer.adobe.com/console) with your Adobe ID.
+
+
+
+2. Choose your account.
+
+
+
+3. Choose your profile or organization.
+
+
+
+4. Make sure you are in the correct organization (a switcher is in the top right corner).
+
+
+
+5. Click **Create new project** → **Project from template**:
+
+
+
+And choose **App Builder**:
+
+
+
+6. Fill in **Project Title** (display name) and **App Name** (unique identifier — cannot be changed after creation).
+
+
+
+After creating, you will see a project with two default workspaces: **Production** and **Stage**. Use **Stage** for development and testing before pushing to Production.
+
+
+
+## Set up local environment
+
+Make sure you have the correct Node.js version and the latest AIO CLI installed.
+
+```shell
+$ node -v
+v20.9.0
+```
+
+Check your AIO CLI version:
+
+```shell
+aio -v
+```
+
+Compare it with the latest published version:
+
+```shell
+npm show @adobe/aio-cli version
+```
+
+If outdated, update:
+
+```shell
+npm install -g @adobe/aio-cli
+```
+
+More details: [Local Environment Set Up](../../../guides/local-environment/index.md).
+
+## Step 1: Get the sample project
+
+The quickest way to start is to clone the official Content Hub sample from [adobe/aem-uix-examples](https://github.com/adobe/aem-uix-examples):
+
+```shell
+git clone https://github.com/adobe/aem-uix-examples.git
+cd aem-uix-examples/aem-contenthub-sample
+npm install
+```
+
+Then log in to Adobe I/O and link the project to your Console workspace:
+
+```shell
+aio login
+aio console org select
+aio console project select
+aio console workspace select
+aio app use -g
+```
+
+See [Code Generation](../code-generation/index.md) for the full setup walkthrough.
+
+## Step 2: Project structure
+
+After scaffolding, the relevant files are:
+
+```text
+app.config.yaml
+src/
+ aem-assets-contenthub-1/
+ ext.config.yaml
+ web-src/
+ src/
+ components/
+ App.js
+ Constants.js
+ ExtensionRegistration.js
+ PanelAssetDetailsExtensionTab.js
+ CardActionModal.js
+ SelectionBarModal.js
+```
+
+```yaml
+# app.config.yaml
+extensions:
+ aem/assets/contenthub/1:
+ $include: src/aem-assets-contenthub-1/ext.config.yaml
+```
+
+## Step 3: Wire up routing in `App.js`
+
+`App.js` maps URL hash paths to React components. Each modal or panel is a separate route. Content Hub loads the extension in an iframe and navigates between routes to render panels and dialogs.
+
+```js
+import React from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+import { HashRouter as Router, Routes, Route } from 'react-router-dom';
+import ExtensionRegistration from './ExtensionRegistration';
+import PanelAssetDetailsExtensionTab from './PanelAssetDetailsExtensionTab';
+import CardActionModal from './CardActionModal';
+import SelectionBarModal from './SelectionBarModal';
+
+function App() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* YOUR CUSTOM ROUTES SHOULD BE HERE */}
+
+
+
+ );
+
+ function onError(e, componentStack) {}
+ function fallbackComponent({ componentStack, error }) {
+ return (
+
+
Extension rendering error
+
{componentStack + '\n' + error.message}
+
+ );
+ }
+}
+
+export default App;
+```
+
+## Step 4: Register all surfaces in `ExtensionRegistration.js`
+
+This is the core of the extension. `ExtensionRegistration.js` calls `register()` from `@adobe/uix-guest`, which connects to Content Hub and announces the extension's capabilities.
+
+All three namespaces (`assetDetails`, `card`, `selectionBar`) are declared in a single `register()` call. Use `let` for `guestConnection` so that click handlers defined inside `card` and `selectionBar` can close over it after `register()` resolves.
+
+For production, populate `allowedRepos` to restrict the extension to your specific Content Hub repository. Leave it empty during development to allow any repository.
+
+```js
+import React from 'react';
+import { Text } from '@adobe/react-spectrum';
+import { register } from '@adobe/uix-guest';
+import { extensionId } from './Constants';
+
+// Populate before deploying to Production. Empty = allow any repo (safe for dev).
+const allowedRepos = [];
+
+function getRepo() {
+ return new URLSearchParams(window.location.search).get('repo');
+}
+
+function shouldSkipRegistration(repo) {
+ return allowedRepos.length > 0 && !allowedRepos.includes(repo);
+}
+
+function ExtensionRegistration() {
+ const repo = getRepo();
+ if (shouldSkipRegistration(repo)) {
+ return Skipped registration: repo not in allowedRepos;
+ }
+
+ const init = async () => {
+ let guestConnection = await register({
+ id: extensionId,
+ methods: {
+
+ // ── Asset Details tab panel ──────────────────────────────────────
+ assetDetails: {
+ getTabPanels() {
+ return [
+ {
+ id: 'extension-template',
+ tooltip: 'Extension Template',
+ icon: 'Extension',
+ title: 'Extension Template',
+ contentUrl: '/#asset-details-extension-tab',
+ },
+ ];
+ },
+ },
+
+ // ── Asset card & collection tile actions ─────────────────────────
+ // A single button configuration and a single modal serve both asset
+ // cards and collection tiles — `resourceType` tells the modal which
+ // kind of resource was clicked.
+ card: {
+ getActionButtons(actionContext) {
+ const { context } = actionContext || {};
+ if (context !== 'assets' && context !== 'collection' && context !== 'collections') {
+ return [];
+ }
+ return [{ id: 'customId', label: 'Custom label', icon: 'Form' }];
+ },
+ async onActionClick(resourceType, buttonId, resourceId, actionContext) {
+ if (buttonId === 'customId') {
+ await guestConnection.host.modal.openDialog({
+ title: 'Custom Dialog',
+ contentUrl: `/#card-action-modal?resourceId=${encodeURIComponent(resourceId)}&resourceType=${encodeURIComponent(resourceType)}`,
+ type: 'modal',
+ size: 'M',
+ });
+ }
+ },
+ },
+
+ // ── Selection Bar bulk actions ────────────────────────────────────
+ selectionBar: {
+ getActionButtons(actionContext) {
+ const { context } = actionContext || {};
+ if (context !== 'assets') return [];
+ return [{ id: 'customId', label: 'Custom label', icon: 'Form' }];
+ },
+ async onActionClick(buttonId, assetIds) {
+ if (buttonId === 'customId') {
+ await guestConnection.host.modal.openDialog({
+ title: `Custom Dialog (${assetIds.length} asset${assetIds.length !== 1 ? 's' : ''} selected)`,
+ contentUrl: `/#selection-bar-modal?assetIds=${encodeURIComponent(JSON.stringify(assetIds))}`,
+ type: 'modal',
+ size: 'M',
+ });
+ }
+ },
+ },
+
+ },
+ });
+ };
+
+ init().catch(console.error);
+ return IFrame for integration with Host (Content Hub)...;
+}
+
+export default ExtensionRegistration;
+```
+
+## Step 5: Build the tab panel — `PanelAssetDetailsExtensionTab.js`
+
+The tab panel is a separate component rendered inside an iframe when the user clicks the custom tab. It calls `attach()` to connect to Content Hub and retrieves the current asset.
+
+`getCurrentAsset()` is asynchronous and returns the asset id as a plain string (the asset URN). Wrap it as `{ id: assetId }` if your downstream code expects an object; otherwise use it directly.
+
+```js
+import React, { useState, useEffect } from 'react';
+import { attach } from '@adobe/uix-guest';
+import {
+ Provider, defaultTheme, View, Heading, Text, Button, Divider, ProgressCircle,
+} from '@adobe/react-spectrum';
+import { extensionId } from './Constants';
+
+export default function PanelAssetDetailsExtensionTab() {
+ const [guestConnection, setGuestConnection] = useState(null);
+ const [asset, setAsset] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const connection = await attach({ id: extensionId });
+ setGuestConnection(connection);
+
+ // getCurrentAsset() returns the asset id as a plain string (e.g. "urn:aaid:aem:...").
+ const assetId = await connection.host.assetDetails.getCurrentAsset();
+ setAsset({ id: assetId });
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Extension Template
+
+ {asset && (
+
+ Asset ID:
+
+
+ {asset.id}
+
+
+
+ )}
+
+
+
+ );
+}
+```
+
+## Step 6: Build the card action modal — `CardActionModal.js`
+
+The card action modal is opened when the user clicks a custom card button. Data (the asset or collection ID and resource type) is passed via URL query parameters embedded in `contentUrl` and read from the URL hash inside the dialog component.
+
+Note that `payload` will be `null` on the very first render and set on the second render after the URL params are parsed — show a spinner until it's ready.
+
+```js
+import React, { useState, useEffect } from 'react';
+import { attach } from '@adobe/uix-guest';
+import {
+ Provider, defaultTheme, View, Heading, Text, Button, ButtonGroup, Divider, ProgressCircle,
+} from '@adobe/react-spectrum';
+import { extensionId } from './Constants';
+
+export default function CardActionModal() {
+ const [guestConnection, setGuestConnection] = useState(null);
+ const [payload, setPayload] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ const connection = await attach({ id: extensionId });
+ setGuestConnection(connection);
+
+ // Data is passed via URL query parameters embedded in contentUrl.
+ const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
+ setPayload({
+ resourceId: params.get('resourceId'),
+ resourceType: params.get('resourceType'),
+ });
+ })();
+ }, []);
+
+ if (!payload) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Custom Dialog
+
+
+ Type: {payload.resourceType}
+
+
+ {payload.resourceId}
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+## Step 7: Build the selection bar modal — `SelectionBarModal.js`
+
+The selection bar modal receives a list of selected asset IDs passed as a JSON-encoded URL parameter. Decode and parse them inside the component.
+
+```js
+import React, { useState, useEffect } from 'react';
+import { attach } from '@adobe/uix-guest';
+import {
+ Provider, defaultTheme, View, Heading, Text, Button, ButtonGroup, Divider,
+ ListView, Item, ProgressCircle,
+} from '@adobe/react-spectrum';
+import { extensionId } from './Constants';
+
+export default function SelectionBarModal() {
+ const [guestConnection, setGuestConnection] = useState(null);
+ const [payload, setPayload] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ const connection = await attach({ id: extensionId });
+ setGuestConnection(connection);
+
+ // assetIds are passed as a JSON-encoded URL parameter.
+ const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
+ const raw = params.get('assetIds');
+ setPayload({ assetIds: raw ? JSON.parse(raw) : [] });
+ })();
+ }, []);
+
+ if (!payload) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Custom Dialog — {payload.assetIds.length} asset(s) selected
+
+ ({ id, name: id }))}
+ height="size-2400"
+ aria-label="Selected assets"
+ >
+ {(item) => (
+
+ {item.name}
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+```
+
+## Step 8: Test locally
+
+Run the extension and load it in Content Hub. See [Troubleshooting](../debug/index.md) for certificate setup and common issues.
+
+```shell
+aio app run
+```
+
+```shell
+For a developer preview of your UI extension in the Content Hub environment, follow the URL:
+ -> https://experience.adobe.com/aem/extension-manager/preview/
+
+To view your local application:
+ -> https://localhost:9080
+press CTRL+C to terminate dev environment
+```
+
+Open Content Hub with the `ext=` parameter to load your locally running extension:
+
+```text
+https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/
+```
+
+You may need to accept the self-signed certificate first — see [Accept the Certificate](../debug/index.md#accept-the-certificate).
+
+## Step 9: Run on Stage
+
+After development is complete, test on Stage before deploying to Production. First, ensure you are logged into the correct org and using the Stage workspace:
+
+```shell
+$ aio where
+
+You are currently in:
+1. Org: My Org
+2. Project: my-contenthub-extension
+3. Workspace: Stage
+```
+
+Then deploy:
+
+```shell
+aio app deploy
+✔ Building web assets for 'aem/assets/contenthub/1'
+✔ Deploying web assets for 'aem/assets/contenthub/1'
+To view your deployed application:
+ -> https://123456-yournamespace-stage.adobeio-static.net/index.html
+For a developer preview of your UI extension in the Content Hub environment, follow the URL:
+ -> https://experience.adobe.com/aem/extension-manager/preview/
+New Extension Point(s) in Workspace 'Stage': 'aem/assets/contenthub/1'
+Successful deployment 🏄
+```
+
+Use the staging deployment URL with the `ext=` parameter to verify the extension in Content Hub:
+
+```text
+https://experience.adobe.com/?devMode=true&ext=https://123456-yournamespace-stage.adobeio-static.net/index.html#/assets/contenthub/
+```
+
+
+
+## Step 10: Deploy to Production
+
+After testing on Stage, populate `allowedRepos` in `ExtensionRegistration.js` with your production Content Hub repository hostname:
+
+```js
+const allowedRepos = [
+ 'delivery-p12345-e167890.adobeaemcloud.com',
+];
+```
+
+Then redeploy using the Production workspace and publish through the Extension Manager.
+
+Refer to [UI Extensions Development Flow](../../../guides/development-flow/index.md#deploy-on-production) for the full publication and approval process.
+
+## Additional resources
+
+- [Common Concepts](../api/commons/index.md)
+- [Card Actions](../api/card-actions/index.md)
+- [Asset Details Tab Panels](../api/asset-details/index.md)
+- [Selection Bar Actions](../api/selection-bar/index.md)
+- [Code Generation](../code-generation/index.md)
+- [Troubleshooting](../debug/index.md)
+- [UI Extensions Development Flow](../../../guides/development-flow/index.md)
+- [UI Extensions Management](../../../guides/publication/index.md)
diff --git a/src/pages/services/contenthub/index.md b/src/pages/services/contenthub/index.md
new file mode 100644
index 00000000..52260eb6
--- /dev/null
+++ b/src/pages/services/contenthub/index.md
@@ -0,0 +1,14 @@
+---
+title: Content Hub Extensibility
+description: Extend Content Hub with custom UI panels, tab panels, card actions, and bulk-action buttons using Adobe App Builder and the UIX SDK.
+contributors:
+ - https://github.com/AdobeDocs/uix
+---
+
+# Content Hub Extensibility
+
+Content Hub supports UI extensibility through [Adobe App Builder](https://developer.adobe.com/app-builder/docs/overview/) and the [UIX SDK](https://developer.adobe.com/uix/docs/). Extensions add custom surfaces to Content Hub without modifying its source code.
+
+All Content Hub extensions use the **`aem/assets/contenthub/1`** extension point.
+
+In this section, you will find the available [extension points](api/index.md) and examples of how to utilize them.