Content publisher intent
Content publisher integration for publishing designs to external platforms.
Running this example
To run this example locally:
-
If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.
-
In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to
http://localhost:8080. -
Clone the starter kit:
git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.gitcd canva-apps-sdk-starter-kitSHELL -
Install dependencies:
npm installSHELL -
Run the example:
npm run start:example content_publisher_intentSHELL -
Click the Preview URL link shown in the terminal to open the example in the Canva editor.
Example app source code
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 optionsfunction renderSettingsUi(request: RenderSettingsUiRequest) {const root = createRoot(document.getElementById("root") as Element);root.render(<AppI18nProvider><AppUiProvider><SettingUi {...request} /></AppUiProvider></AppI18nProvider>,);}// Render the preview UI showing how the content will appear after publishingfunction renderPreviewUi(request: RenderPreviewUiRequest) {const root = createRoot(document.getElementById("root") as Element);root.render(<AppI18nProvider><AppUiProvider><PreviewUi {...request} /></AppUiProvider></AppI18nProvider>,);}// Define the output types (publishing formats) available to users// Canva automatically displays a dropdown selector when more than one output type is definedasync function getPublishConfiguration(): Promise<GetPublishConfigurationResponse> {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 platformasync function publishContent(request: PublishContentRequest,): Promise<PublishContentResponse> {// 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 postexternalUrl: "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 workflowsprepareContentPublisher(contentPublisher);
TYPESCRIPT
.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;}
CSS
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 APIconst 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 changeuseEffect(() => {const dispose = registerOnPreviewChange((data) => {setPreviewData(data);});return dispose;}, [registerOnPreviewChange]);const { previewMedia, publishRef, outputType } = previewData ?? {};const publishSettings = parsePublishSettings(publishRef);return (<div className={styles.container}><Boxdisplay="flex"alignItems="center"justifyContent="center"flexDirection="column"width="full"height="full">{outputType?.id === "post" && (<PostPreview previewMedia={previewMedia} settings={publishSettings} />)}</Box></div>);};interface PreviewProps {previewMedia: PreviewMedia[] | undefined;settings: PublishSettings | undefined;}// Renders a social media post preview with user info, media, and captionexport 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 + borderconst previewWidth = 400 + 32 + 2;return (<div style={{ width: previewWidth }}><Boxdisplay="flex"alignItems="center"justifyContent="center"background="surface"borderRadius="large"padding="2u"border="standard"><Rows spacing="2u"><UserInfo isLoading={isLoading} /><ImagePreview previewMedia={previewMedia} /><Caption isLoading={isLoading} caption={caption} /></Rows></Box></div>);};// Renders user profile information with avatar and usernameconst UserInfo = ({ isLoading }: { isLoading: boolean }) => {return (<Columns spacing="1u" alignY="center"><Column width="content"><div className={styles.avatar}><Avatar name={username} /></div></Column><Column width="content">{isLoading ? (<div className={styles.textPlaceholder}><TextPlaceholder size="medium" /></div>) : (<Text size="small" variant="bold">{username}</Text>)}</Column></Columns>);};// Renders the post caption with usernameconst Caption = ({isLoading,caption,}: {isLoading: boolean;caption: string | undefined;}) => {return (<>{isLoading ? (<div className={styles.textPlaceholder}><TextPlaceholder size="medium" /></div>) : (caption && (<Text lineClamp={2} size="small">{caption}</Text>))}</>);};// Renders a single image post previewconst 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 (<div className={styles.imageContainer}>{isLoading || !media?.previews.length ? (<div className={styles.imagePlaceholder}><Placeholder shape="rectangle" /></div>) : (<div className={styles.imageRow} style={{ width: fullWidth }}>{media?.previews.filter((p) => p.kind !== "email").map((p) => {return (<div key={p.id} className={styles.image}><PreviewRenderer preview={p} /></div>);})}</div>)}</div>);};// Renders individual preview based on its type and statusconst PreviewRenderer = ({ preview }: { preview: Preview }) => {if (preview.kind === "email") {return null;}const intl = useIntl();// Handle different preview statesif (preview.status === "loading") {return (<ImageStatusTexttext={intl.formatMessage({defaultMessage: "Loading...",description:"Loading state text shown while image preview is loading",})}/>);}if (preview.status === "error") {return (<ImageStatusTexttext={intl.formatMessage({defaultMessage: "Error loading preview",description: "Error message shown when image preview fails to load",})}/>);}// Handle image previews (ready status)if (isImagePreviewReady(preview)) {return (<ImageCardalt={intl.formatMessage({defaultMessage: "Image preview {id}",description: "Alt text for image preview thumbnails",},{ id: preview.id },)}thumbnailUrl={preview.url}/>);}// Fallback for unknown preview typesreturn (<Boxwidth="full"height="full"padding="2u"display="flex"alignItems="center"justifyContent="center"><Text size="medium" tone="tertiary" alignment="center">{intl.formatMessage({defaultMessage: "Preview not available",description: "Fallback text shown when preview type is not supported",})}</Text></Box>);};// Helper component to display status text for loading/error statesconst ImageStatusText = ({ text }: { text: string }) => (<Boxwidth="full"height="full"padding="2u"display="flex"alignItems="center"justifyContent="center"><Text size="medium" tone="tertiary" alignment="center">{text}</Text></Box>);
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 settingsexport const SettingUi = ({invocationContext,updatePublishSettings,registerOnContextChange,}: RenderSettingsUiRequest) => {const intl = useIntl();const [settings, setSettings] = useState<PublishSettings>(parsePublishSettings(invocationContext?.publishRef) ??({ caption: "" } as PublishSettings),);const [settingsUiContext, setSettingsUiContext] =useState<PublishSettingsSettingsUiContext | null>(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 Canvaconst setAndPropagateSettings = useCallback((updatedSettings: PublishSettings) => {setSettings(updatedSettings);updatePublishSettings({publishRef: JSON.stringify(updatedSettings),validityState: validatePublishRef(updatedSettings),});},[updatePublishSettings],);return (<div className={styles.scrollContainer}><Rows spacing="2u"><Text>{settingsUiContext?.outputType.displayName}</Text><FormFieldlabel={intl.formatMessage({defaultMessage: "Caption",description:"Label for the caption input field in publish settings",})}control={(props) => (<TextInput{...props}value={settings.caption}onChange={(caption) =>setAndPropagateSettings({ ...settings, caption })}/>)}/></Rows></div>);};// Validates the publish settings to enable/disable the publish button// Returns "valid" when all required fields are filledconst validatePublishRef = (publishRef: PublishSettings) => {// caption is requiredif (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 settingsexport interface PublishSettings {caption: string;}// Utility function to safely parse publish settingsexport 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 displayexport function isImagePreviewReady(preview: Preview): preview is Preview & {kind: "image";status: "ready";url: string;} {return (preview.kind === "image" && preview.status === "ready" && "url" in preview);}
TYPESCRIPT
# Content publisher intentThis 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: <https://www.canva.dev/docs/apps/content-publisher/>.## 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 notesNOTE: 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
MARKDOWN
API reference
Need help?
- Join our Community Forum(opens in a new tab or window)
- Report issues with this example on GitHub(opens in a new tab or window)