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 {
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({
updatePublishSettings,
registerOnSettingsUiContextChange,
}: RenderSettingsUiRequest) {
const root = createRoot(document.getElementById("root") as Element);
root.render(
<AppI18nProvider>
<AppUiProvider>
<SettingUi
updatePublishSettings={updatePublishSettings}
registerOnSettingsUiContextChange={registerOnSettingsUiContextChange}
/>
</AppUiProvider>
</AppI18nProvider>,
);
}
// Render the preview UI showing how the content will appear after publishing
function renderPreviewUi({ registerOnPreviewChange }: RenderPreviewUiRequest) {
const root = createRoot(document.getElementById("root") as Element);
root.render(
<AppI18nProvider>
<AppUiProvider>
<PreviewUi registerOnPreviewChange={registerOnPreviewChange} />
</AppUiProvider>
</AppI18nProvider>,
);
}
// 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<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 platform
async 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 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);
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 { useIntl } from "react-intl";
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 }) => {
const intl = useIntl();
// Handle different preview states
if (preview.status === "loading") {
return (
<ImageStatusText
text={intl.formatMessage({
defaultMessage: "Loading...",
description:
"Loading state text shown while image preview is loading",
})}
/>
);
}
if (preview.status === "error") {
return (
<ImageStatusText
text={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 (
<ImageCard
alt={intl.formatMessage(
{
defaultMessage: "Image preview {id}",
description: "Alt text for image preview thumbnails",
},
{ id: 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">
{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 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 { useIntl } from "react-intl";
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 intl = useIntl();
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={intl.formatMessage({
defaultMessage: "Caption",
description:
"Label for the caption input field in publish settings",
})}
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.
- **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?