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(opens in a new tab or window). For more information, refer to our Quickstart guide.

  2. 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.

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start content_publisher_intent
    SHELL
  6. 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 workflows
prepareContentPublisher({
// Render the settings UI where users configure publishing options
renderSettingsUi: ({
updatePublishSettings,
registerOnSettingsUiContextChange,
}) => {
root.render(
<AppUiProvider>
<SettingUi
updatePublishSettings={updatePublishSettings}
registerOnSettingsUiContextChange={registerOnSettingsUiContextChange}
/>
</AppUiProvider>,
);
},
// Render the preview UI showing how the content will appear after publishing
renderPreviewUi: ({ 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 defined
getPublishConfiguration: 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 platform
publishContent: 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 post
externalUrl: "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 API
const 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 change
useEffect(() => {
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 caption
export const PostPreview = ({ previewMedia, settings }: PreviewProps) => {
const isLoading = !previewMedia;
const caption = settings?.caption;
return (
<Box
className={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 username
const 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 username
const 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 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 (
<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 status
const PreviewRenderer = ({ preview }: { preview: Preview }) => {
// Handle different preview states
if (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 (
<ImageCard
alt={`Image preview ${preview.id}`}
thumbnailUrl={preview.url}
/>
);
}
// Fallback for unknown preview types
return (
<Box
width="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 states
const ImageStatusText = ({ text }: { text: string }) => (
<Box
width="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 settings
export 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 state
useEffect(() => {
updatePublishSettings({
publishRef: JSON.stringify(settings),
validityState: validatePublishRef(settings),
});
}, [settings, updatePublishSettings]);
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Text>{settingsUiContext?.outputType.displayName}</Text>
<FormField
label="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 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
);
}
TYPESCRIPT
# 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: <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 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.
- **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?