> ## Documentation Index > Fetch the complete documentation index at: https://www.canva.dev/docs/apps/llms.txt > Use this file to discover all available pages before exploring further. # Content publisher intent Content publisher integration for publishing designs to external platforms. ## Running this example To run this example locally: 1. If you haven't already, create a new app in the [Developer Portal](https://www.canva.com/developers/apps). For more information, refer to our [Quickstart guide](https://www.canva.dev/docs/apps/quickstart/). 2. In your app's configuration on the [Developer Portal](https://www.canva.com/developers/apps), ensure the "Development URL" is set to `http://localhost:8080`. 3. Clone the starter kit: ```shell git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git cd canva-apps-sdk-starter-kit ``` 4. Install dependencies: ```shell npm install ``` 5. Run the example: ```shell npm run start:example content_publisher_intent ``` 6. Click the **Preview URL** link shown in the terminal to open the example in the Canva editor. ## Example app source code ```typescript import type { ContentPublisherIntent, GetPublishConfigurationResponse, PublishContentRequest, PublishContentResponse, RenderPreviewUiRequest, RenderSettingsUiRequest, } from "@canva/intents/content"; import { prepareContentPublisher } from "@canva/intents/content"; import { createRoot } from "react-dom/client"; import "@canva/app-ui-kit/styles.css"; import { AppUiProvider } from "@canva/app-ui-kit"; import { AppI18nProvider, initIntl } from "@canva/app-i18n-kit"; import { PreviewUi } from "./preview_ui"; import { SettingUi } from "./setting_ui"; const intl = initIntl(); // Render the settings UI where users configure publishing options function renderSettingsUi(request: RenderSettingsUiRequest) { const root = createRoot(document.getElementById("root") as Element); root.render( , ); } // Render the preview UI showing how the content will appear after publishing function renderPreviewUi(request: RenderPreviewUiRequest) { const root = createRoot(document.getElementById("root") as Element); root.render( , ); } // Define the output types (publishing formats) available to users // Canva automatically displays a dropdown selector when more than one output type is defined async function getPublishConfiguration(): Promise { return { status: "completed", outputTypes: [ { id: "post", displayName: intl.formatMessage({ defaultMessage: "Feed Post", description: "Label for publishing format shown in the output type dropdown", }), mediaSlots: [ { id: "media", displayName: intl.formatMessage({ defaultMessage: "Media", description: "Label for the media upload slot", }), fileCount: { exact: 1 }, accepts: { image: { format: "png", // Social media post aspect ratio range (portrait to landscape) aspectRatio: { min: 4 / 5, max: 1.91 / 1 }, }, }, }, ], }, ], }; } // Handle the actual publishing when the user clicks the publish button // In production, this should make API calls to your platform async function publishContent( request: PublishContentRequest, ): Promise { // Replace this with your actual API integration // Example: Upload media to your platform and create a post // const uploadedMedia = await uploadToYourPlatform(params.outputMedia); // const post = await createPostOnYourPlatform({ // media: uploadedMedia, // caption: JSON.parse(params.publishRef).caption // }); return { status: "completed", externalId: "1234567890", // Your platform's unique identifier for this post externalUrl: "https://example.com/posts/1234567890", // Link to view the published content }; } const contentPublisher: ContentPublisherIntent = { renderSettingsUi, renderPreviewUi, getPublishConfiguration, publishContent, }; // Initialize the Content Publisher intent // This configures the app to handle content publishing workflows prepareContentPublisher(contentPublisher); ``` ```css .container { width: 100%; height: 100%; } /* Scale down preview on mobile devices */ @media (max-width: 600px) { .container { transform: scale(0.3); } } /* Avatar styling to match social media appearance */ .avatar { transform: scale(0.6); width: 24px; height: 24px; transform-origin: top left; } /* Text placeholder for loading states */ .textPlaceholder { min-width: calc(8 * 20); } /* Container for media images */ .imageContainer { border-radius: var(--ui-kit-radius-element-standard); overflow: hidden; /* Enable border radius */ } /* Row containing all images */ .imageRow { overflow-y: hidden; overflow-x: auto; height: 400px; display: flex; } /* Individual image and placeholder styling */ .imagePlaceholder, .image { width: 400px; height: 400px; object-fit: cover; display: inline-block; position: relative; } ``` ```typescript import type { OutputType, PreviewMedia, RenderPreviewUiInvocationContext, } from "@canva/intents/content"; import { useEffect, useState } from "react"; import { useIntl } from "react-intl"; import { parsePublishSettings } from "./types"; import * as styles from "./preview_ui.css"; import { Box, Text, Rows, Columns, Column, Avatar, Placeholder, TextPlaceholder, ImageCard, } from "@canva/app-ui-kit"; import type { Preview } from "@canva/intents/content"; import { isImagePreviewReady, type PublishSettings } from "./types"; // Static user data for demo purposes // In production, fetch real user data from your platform's API const username = "username"; const IMAGE_WIDTH = 400; interface PreviewUiProps { invocationContext: RenderPreviewUiInvocationContext; registerOnPreviewChange: ( callback: (opts: { previewMedia: PreviewMedia[]; outputType: OutputType; publishRef?: string; }) => void, ) => () => void; } // Main preview UI component that receives preview updates when settings or pages change. // preview UI is more flexible to align with your platform's design system, so it is not constrained to the Canva design system. export const PreviewUi = ({ invocationContext, registerOnPreviewChange, }: PreviewUiProps) => { const [previewData, setPreviewData] = useState<{ previewMedia?: PreviewMedia[]; outputType?: OutputType; publishRef?: string; } | null>( invocationContext ? { previewMedia: (invocationContext?.previewMedia as PreviewMedia[]) || [], outputType: (invocationContext?.outputType as OutputType) || undefined, publishRef: invocationContext?.publishRef, } : null, ); // Register to receive preview updates whenever settings or pages change useEffect(() => { const dispose = registerOnPreviewChange((data) => { setPreviewData(data); }); return dispose; }, [registerOnPreviewChange]); const { previewMedia, publishRef, outputType } = previewData ?? {}; const publishSettings = parsePublishSettings(publishRef); return (
{outputType?.id === "post" && ( )}
); }; interface PreviewProps { previewMedia: PreviewMedia[] | undefined; settings: PublishSettings | undefined; } // Renders a social media post preview with user info, media, and caption export const PostPreview = ({ previewMedia, settings }: PreviewProps) => { const isLoading = !previewMedia; const caption = settings?.caption; // TODO: You should update this value to match the visuals you're hoping to achieve with your export preview // Image width + padding + border const previewWidth = 400 + 32 + 2; return (
); }; // Renders user profile information with avatar and username const UserInfo = ({ isLoading }: { isLoading: boolean }) => { return (
{isLoading ? (
) : ( {username} )}
); }; // Renders the post caption with username const Caption = ({ isLoading, caption, }: { isLoading: boolean; caption: string | undefined; }) => { return ( <> {isLoading ? (
) : ( caption && ( {caption} ) )} ); }; // Renders a single image post preview const ImagePreview = ({ previewMedia, }: { previewMedia: PreviewMedia[] | undefined; }) => { const isLoading = !previewMedia; const media = previewMedia?.find((media) => media.mediaSlotId === "media"); const fullWidth = (media?.previews.length ?? 1) * IMAGE_WIDTH; return (
{isLoading || !media?.previews.length ? (
) : (
{media?.previews .filter((p) => p.kind !== "email") .map((p) => { return (
); })}
)}
); }; // Renders individual preview based on its type and status const PreviewRenderer = ({ preview }: { preview: Preview }) => { if (preview.kind === "email") { return null; } const intl = useIntl(); // Handle different preview states if (preview.status === "loading") { return ( ); } if (preview.status === "error") { return ( ); } // Handle image previews (ready status) if (isImagePreviewReady(preview)) { return ( ); } // Fallback for unknown preview types return ( {intl.formatMessage({ defaultMessage: "Preview not available", description: "Fallback text shown when preview type is not supported", })} ); }; // Helper component to display status text for loading/error states const ImageStatusText = ({ text }: { text: string }) => ( {text} ); ```
```typescript import type { PublishSettingsSettingsUiContext, RenderSettingsUiRequest, } from "@canva/intents/content"; import { FormField, Rows, Text, TextInput } from "@canva/app-ui-kit"; import { useCallback, useEffect, useState } from "react"; import { useIntl } from "react-intl"; import * as styles from "styles/components.css"; import type { PublishSettings } from "./types"; import { parsePublishSettings } from "./types"; // Settings UI component for configuring publish settings export const SettingUi = ({ invocationContext, updatePublishSettings, registerOnContextChange, }: RenderSettingsUiRequest) => { const intl = useIntl(); const [settings, setSettings] = useState( parsePublishSettings(invocationContext?.publishRef) ?? ({ caption: "" } as PublishSettings), ); const [settingsUiContext, setSettingsUiContext] = useState(null); // Listen for settings UI context changes (e.g., when output type changes) useEffect(() => { const dispose = registerOnContextChange({ onContextChange: (context) => { if (context.reason !== "publish_settings") return; setSettingsUiContext(context); }, }); return dispose; }, [registerOnContextChange]); // Helper function to both set the settings locally and propagate them to Canva const setAndPropagateSettings = useCallback( (updatedSettings: PublishSettings) => { setSettings(updatedSettings); updatePublishSettings({ publishRef: JSON.stringify(updatedSettings), validityState: validatePublishRef(updatedSettings), }); }, [updatePublishSettings], ); return (
{settingsUiContext?.outputType.displayName} ( setAndPropagateSettings({ ...settings, caption }) } /> )} />
); }; // Validates the publish settings to enable/disable the publish button // Returns "valid" when all required fields are filled const validatePublishRef = (publishRef: PublishSettings) => { // caption is required if (publishRef.caption.length === 0) { return "invalid_missing_required_fields"; } return "valid"; }; ```
```typescript import type { Preview } from "@canva/intents/content"; // Type definition for publish settings // In production, extend this to include all platform-specific settings export interface PublishSettings { caption: string; } // Utility function to safely parse publish settings export function parsePublishSettings( publishRef?: string, ): PublishSettings | undefined { if (!publishRef) return undefined; try { return JSON.parse(publishRef) as PublishSettings; } catch { return undefined; } } // Type guard to check if a preview is an image preview that's ready to display export function isImagePreviewReady(preview: Preview): preview is Preview & { kind: "image"; status: "ready"; url: string; } { return ( preview.kind === "image" && preview.status === "ready" && "url" in preview ); } ``` ```markdown # Content publisher intent This example demonstrates how to use the Content Publisher intent to publish Canva designs to external platforms. It shows a social media publishing use case with posts. For API reference docs and instructions on running this example, see: . ## What this example demonstrates - **App Settings UI**: Platform-specific publishing settings (caption configuration) - **App Preview UI**: Visual preview showing how the design will appear on your platform - **Output types**: Configuring different publishing formats (social media posts) - **Publish content**: Example implementation of the `publishContent` callback ## Implementation notes NOTE: This example differs from what is expected for public apps to pass a Canva review: - **Static user data**: This example uses hardcoded usernames and avatar data. Production apps should fetch real user data from your platform's API. - **API integration**: This example uses mock data. Production apps need to implement proper API authentication, rate limiting, and error handling for the `publishContent` callback. - **Error handling**: Production apps should have comprehensive error handling for network failures, API errors, and edge cases. - **Validation**: Production apps should implement platform-specific validation (e.g., caption length limits, aspect ratio requirements). - **Code structure**: The code structure is simplified. Production apps using [intents](https://www.canva.dev/docs/apps/intents/) are recommended to call the prepareContentPublisher function from src/intents/content_publisher/index.tsx ```
## API reference * [App UI Kit](https://www.canva.dev/docs/apps/app-ui-kit/) * [`prepareContentPublisher`](https://www.canva.dev/docs/apps/api/latest/intents-content-prepare-content-publisher/) ## Need help? * Join our [Community Forum](https://community.canva.dev/) * Report issues with this example on [GitHub](https://github.com/canva-sdks/canva-apps-sdk-starter-kit/issues)