Examples
App elements
Assets and media
Fundamentals
Design interaction
Drag and drop
Design elements
Localization
Content replacement
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 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 { PublishContentRequest } 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 { PreviewUi } from "./preview_ui";import { SettingUi } from "./setting_ui";const root = createRoot(document.getElementById("root") as Element);// Initialize the Content Publisher intent// This configures the app to handle content publishing workflowsprepareContentPublisher({// Render the settings UI where users configure publishing optionsrenderSettingsUi: ({updatePublishSettings,registerOnSettingsUiContextChange,}) => {root.render(<AppUiProvider><SettingUiupdatePublishSettings={updatePublishSettings}registerOnSettingsUiContextChange={registerOnSettingsUiContextChange}/></AppUiProvider>,);},// Render the preview UI showing how the content will appear after publishingrenderPreviewUi: ({ registerOnPreviewChange }) => {root.render(<AppUiProvider><PreviewUi registerOnPreviewChange={registerOnPreviewChange} /></AppUiProvider>,);},// Define the output types (publishing formats) available to users// Canva automatically displays a dropdown selector when more than one output type is definedgetPublishConfiguration: async () => {return {status: "completed",outputTypes: [{id: "post",displayName: "Feed Post",mediaSlots: [{id: "media",displayName: "Media",required: true,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 platformpublishContent: async (request: PublishContentRequest) => {// 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};},});
TYPESCRIPT
/* Main container for the preview UI */.container {display: flex;align-items: center;justify-content: center;flex-direction: column;width: 100%;height: 100%;}/* Scale down preview on mobile devices */@media (max-width: 600px) {.container {transform: scale(0.3);}}/* Wrapper for the social media post preview */.wrapper {display: flex;align-items: center;justify-content: center;width: calc(400px + 32px + 2px); /* Image width + padding + border */}/* User profile section styling */.user {display: flex;align-items: center;gap: 8px;}/* Avatar styling to match social media appearance */.avatar {transform: scale(0.6);width: 24px;height: 24px;transform-origin: top left;}.avatarImage {width: 100%;height: 100%;object-fit: cover;}/* Text placeholder for loading states */.textPlaceholder {min-width: calc(8 * 20);}/* Container for media images */.imageContainer {overflow: hidden; /* Enable border radius */}/* Row containing all images */.imageRow {overflow-y: hidden;overflow-x: auto;height: 400px;display: flex;}/* Aspect ratio helpers for different image formats */.aspect-1-1 {padding-top: 100%;}.aspect-3-4 {padding-top: 133.33%;}/* Individual image and placeholder styling */.imagePlaceholder,.image {width: 400px;height: 400px;object-fit: cover;display: inline-block;position: relative;}/* Icon placeholder styling */.iconPlaceholder {width: 24px;height: 24px;}
CSS
import type { OutputType, PreviewMedia } from "@canva/intents/content";import { useEffect, useState } from "react";import { parsePublishSettings } from "./types";import * as styles from "./preview_ui.css";import {Box,Text,Rows,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 {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 = ({ registerOnPreviewChange }: PreviewUiProps) => {const [previewData, setPreviewData] = useState<{previewMedia: PreviewMedia[];outputType: OutputType;publishRef?: string;} | null>(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}>{outputType?.id === "post" && (<PostPreview previewMedia={previewMedia} settings={publishSettings} />)}</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;return (<BoxclassName={styles.wrapper}background="surface"borderRadius="large"padding="2u"border="standard"><Rows spacing="2u"><UserInfo isLoading={isLoading} /><ImagePreview previewMedia={previewMedia} /><Caption isLoading={isLoading} caption={caption} /></Rows></Box>);};// Renders user profile information with avatar and usernameconst UserInfo = ({ isLoading }: { isLoading: boolean }) => {return (<div className={styles.user}><Box className={styles.avatar}><Avatar name={username} /></Box>{isLoading ? (<div className={styles.textPlaceholder}><TextPlaceholder size="medium" /></div>) : (<Text size="small" variant="bold">{username}</Text>)}</div>);};// 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 (<Box borderRadius="large" className={styles.imageContainer}>{isLoading || !media?.previews.length ? (<div className={styles.imagePlaceholder}><Placeholder shape="rectangle" /></div>) : (<div className={styles.imageRow} style={{ width: fullWidth }}>{media?.previews.map((p) => {return (<div key={p.id} className={styles.image}><PreviewRenderer preview={p} /></div>);})}</div>)}</Box>);};// Renders individual preview based on its type and statusconst PreviewRenderer = ({ preview }: { preview: Preview }) => {// Handle different preview statesif (preview.status === "loading") {return <ImageStatusText text="Loading..." />;}if (preview.status === "error") {return <ImageStatusText text="Error loading preview" />;}// Handle image previews (ready status)if (isImagePreviewReady(preview)) {return (<ImageCardalt={`Image preview ${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">Preview not available</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 {RenderSettingsUiRequest,SettingsUiContext,} from "@canva/intents/content";import { FormField, Rows, Text, TextInput } from "@canva/app-ui-kit";import { useEffect, useState } from "react";import * as styles from "styles/components.css";import type { PublishSettings } from "./types";// Settings UI component for configuring publish settingsexport const SettingUi = ({updatePublishSettings,registerOnSettingsUiContextChange,}: RenderSettingsUiRequest) => {const [settings, setSettings] = useState<PublishSettings>({caption: "",});const [settingsUiContext, setSettingsUiContext] =useState<SettingsUiContext | null>(null);// Listen for settings UI context changes (e.g., when output type changes)useEffect(() => {const dispose = registerOnSettingsUiContextChange((context) => {setSettingsUiContext(context);});return dispose;}, [registerOnSettingsUiContextChange]);// Update publish settings whenever they change// This notifies Canva of the current settings and validity stateuseEffect(() => {updatePublishSettings({publishRef: JSON.stringify(settings),validityState: validatePublishRef(settings),});}, [settings, updatePublishSettings]);return (<div className={styles.scrollContainer}><Rows spacing="2u"><Text>{settingsUiContext?.outputType.displayName}</Text><FormFieldlabel="Caption"control={(props) => (<TextInput{...props}value={settings.caption}onChange={(caption) =>setSettings((prev) => ({ ...prev, 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.- **Localization**: Text content is hardcoded in English. Production apps should implement proper internationalization using the `@canva/app-i18n-kit` package for multi-language support.- **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).
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)