Flat list layout

Create a publish extension with the "Flat list" layout.

A publish extension adds a publish destination to Canva. Users can then publish their design to this destination via Canva's Publish menu.

This tutorial explains how to create a publish extension that uses the Flat list layout. This layout is intended for platforms that:

  • Require users to organize content into folders.

  • Don't support nested folders.

To learn more about the available layouts for publish extensions, refer to Choose a layout for a publish extension.

Step 1: Set up a Base URL

In the Developer Portal, publish extensions have a Base URL field. This indicates that the extension must provide the URL of a server that can receive and respond to HTTP requests from Canva.

To set up a Base URL, refer to either of the following guides:

You can use any programming language, framework, or architecture to handle requests from Canva, but the examples in this documentation use Express.js.

Step 2: Set up the required endpoints

A publish extension that uses the Flat list layout must support the following endpoints:

The following snippet demonstrates how to set up these endpoints with Express.js:

const express = require('express');
const fs = require('fs-extra');
const jimp = require('jimp');
const path = require('path');
const url = require('url');
const app = express();
app.use(express.json());
app.use(express.static('public'));
app.post('/publish/resources/find', async (request, response) => {
// code goes here
});
app.post('/publish/resources/get', async (request, response) => {
// code goes here
});
app.post('/publish/resources/upload', async (request, response) => {
// code goes here
});
app.listen(process.env.PORT || 3000);

This snippet also assumes the following Node.js dependencies are installed:

Step 3: Create an app via the Developer Portal

  1. Log in to the Developer Portal.

  2. Navigate to the Your integrations page.

  3. Select Create an app.

  4. In the App name field, enter a name for the app.

  5. Agree to the Canva Developer Terms.

  6. Select Create app.

Step 4: Add a publish extension to the app

  1. Select Publish.

  2. In the Base URL field, enter the URL of the server.

  3. For the Layout option, select Flat list.

  4. For the Output file types option, select the types of files a user can publish to the destination platform. The example in this tutorial requires the file types to be JPG and PNG.

  5. For the Max number of pages field, enter the maximum number of pages that a user can publish of their design. The example in this tutorial requires the number of pages to be 1.

Any changes to the form save automatically.

Step 5: Provide a list of folders

When a user opens a publish extension that uses the Flat list layout, Canva sends a POST request to the following endpoint:

<base_url>/publish/resources/find

This endpoint must respond with an array of "CONTAINER" resources. Canva then renders these resources as folders that the user can select.

The following snippet demonstrates how to create an array of "CONTAINER" resources based on the contents of the public directory:

app.post('/publish/resources/find', async (request, response) => {
const dirPath = path.join(__dirname, 'public');
await fs.ensureDir(dirPath);
const files = await fs.readdir(dirPath, {
withFileTypes: true,
});
const resources = files
.filter((dirent) => dirent.isDirectory())
.map((folder) => {
return {
type: 'CONTAINER',
id: path.join(dirPath, folder.name),
name: folder.name,
isOwner: true,
readOnly: false,
};
});
response.send({
type: 'SUCCESS',
resources,
});
});

In this case, the id of the resource is the path of directory.

Step 6: Verify that the selected folder still exists

When a user is ready to publish their design, they must select a folder.

When the user selects a folder, a Save button appears, and when the user clicks this button, Canva sends a POST request to the following endpoint:

<base_url>/publish/resources/get

The body of this request includes an id property that contains the ID of the selected folder.

The endpoint must verify that:

  • The folder still exists on the destination platform.

  • The user still has access to the folder on the destination platform.

To do this, the endpoint must respond with the details of the selected folder.

The following snippet demonstrates how to verify the existence of a folder in the public directory (and how to respond with an error if the folder doesn't exist):

app.post('/publish/resources/get', async (request, response) => {
const dirPathExists = await fs.pathExists(request.body.id);
if (!dirPathExists) {
response.send({
type: 'ERROR',
errorCode: 'NOT_FOUND',
});
return;
}
response.send({
type: 'SUCCESS',
resource: {
type: 'CONTAINER',
id: request.body.id,
name: path.basename(request.body.id),
isOwner: true,
readOnly: false,
},
});
});

In this case, the id property in the body of the request is the path of the selected folder.

Step 7: Upload the user's design

When the extension verifies that the selected folder exists, Canva immediately sends a POST request to the following endpoint:

<base_url>/publish/resources/upload

The body of the request contains:

  • An array of assets. Each asset has a url property, which the extension must use to download the asset.

  • A parent property, which contains the id of the selected folder.

The endpoint must use this information to download the user's design to the destination platform. The following snippet demonstrates how to download assets to the selected folder:

app.post('/publish/resources/upload', async (request, response) => {
const [asset] = request.body.assets;
const image = await jimp.read(asset.url);
const filePath = path.join(request.body.parent, asset.name);
await image.writeAsync(filePath);
response.send({
type: 'SUCCESS',
url: url.format({
protocol: request.protocol,
host: request.get('host'),
pathname: path.join(request.body.parent, asset.name),
}),
});
});

If the user publishes their design as an image, Canva provides each page of the design as a separate asset. Otherwise, Canva provides the entire design as a single asset.

If you'd like to share your app with Canva's users, the url property must point to a page that lets users view their published design in context on the destination platform. For example, if a user publishes a tweet, the URL should point to the page of that tweet.

Example

const express = require('express');
const jimp = require('jimp');
const path = require('path');
const url = require('url');
const fs = require('fs-extra');
const app = express();
app.use(express.json());
app.use(express.static('public'));
app.post('/publish/resources/find', async (request, response) => {
const dirPath = path.join(__dirname, 'public');
await fs.ensureDir(dirPath);
const files = await fs.readdir(dirPath, {
withFileTypes: true,
});
const resources = files
.filter((dirent) => dirent.isDirectory())
.map((folder) => {
return {
type: 'CONTAINER',
id: path.join(dirPath, folder.name),
name: folder.name,
isOwner: true,
readOnly: false,
};
});
response.send({
type: 'SUCCESS',
resources,
});
});
app.post('/publish/resources/get', async (request, response) => {
const dirPathExists = await fs.pathExists(request.body.id);
if (!dirPathExists) {
response.send({
type: 'ERROR',
errorCode: 'NOT_FOUND',
});
return;
}
response.send({
type: 'SUCCESS',
resource: {
type: 'CONTAINER',
id: request.body.id,
name: path.basename(request.body.id),
isOwner: true,
readOnly: false,
},
});
});
app.post('/publish/resources/upload', async (request, response) => {
const [asset] = request.body.assets;
const image = await jimp.read(asset.url);
const filePath = path.join(request.body.parent, asset.name);
await image.writeAsync(filePath);
response.send({
type: 'SUCCESS',
url: url.format({
protocol: request.protocol,
host: request.get('host'),
pathname: path.join(request.body.parent, asset.name),
}),
});
});
app.listen(process.env.PORT || 3000);