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.

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:

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);
javascript

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

  1. Log in to the Developer Portal.
  2. Navigate to the Your integrations page.
  3. Click Create an app.
  4. In the App name field, enter a name for the app.
  5. Agree to the Canva Developer Terms.
  6. Click Create 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. Enable Display search field.
  5. 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.
  6. 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.

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
bash

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,
});
});
javascript

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

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
bash

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,
},
});
});
javascript

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

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

<base_url>/publish/resources/upload
bash

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),
}),
});
});
javascript

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.

For guidelines on uploading the assets, see Upload the user's design to the destination platform.

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);
javascript