Skip to content

Commit 13f06e8

Browse files
authored
feat: add Webflow plugin (#116)
* feat: add Webflow plugin * fix: add webflow plugin import and sanitize URL path inputs --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
1 parent 4ca7a2e commit 13f06e8

File tree

8 files changed

+564
-0
lines changed

8 files changed

+564
-0
lines changed

plugins/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import "./slack";
2727
import "./stripe";
2828
import "./superagent";
2929
import "./v0";
30+
import "./webflow";
3031

3132
export type {
3233
ActionConfigField,

plugins/webflow/credentials.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type WebflowCredentials = {
2+
WEBFLOW_API_KEY?: string;
3+
};

plugins/webflow/icon.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function WebflowIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
aria-label="Webflow logo"
5+
className={className}
6+
fill="currentColor"
7+
viewBox="0 0 24 24"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<title>Webflow</title>
11+
<path d="m24 4.515-7.658 14.97H9.149l3.205-6.204h-.144C9.566 16.713 5.621 18.973 0 19.485v-6.118s3.596-.213 5.71-2.435H0V4.515h6.417v5.278l.144-.001 2.622-5.277h4.854v5.244h.144l2.72-5.244H24Z"/>
12+
</svg>
13+
);
14+
}

plugins/webflow/index.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { IntegrationPlugin } from "../registry";
2+
import { registerIntegration } from "../registry";
3+
import { WebflowIcon } from "./icon";
4+
5+
const webflowPlugin: IntegrationPlugin = {
6+
type: "webflow",
7+
label: "Webflow",
8+
description: "Publish and manage Webflow sites",
9+
10+
icon: WebflowIcon,
11+
12+
formFields: [
13+
{
14+
id: "apiKey",
15+
label: "API Token",
16+
type: "password",
17+
placeholder: "your-api-token",
18+
configKey: "apiKey",
19+
envVar: "WEBFLOW_API_KEY",
20+
helpText: "Generate an API token from ",
21+
helpLink: {
22+
text: "Webflow Dashboard",
23+
url: "https://webflow.com/dashboard",
24+
},
25+
},
26+
],
27+
28+
testConfig: {
29+
getTestFunction: async () => {
30+
const { testWebflow } = await import("./test");
31+
return testWebflow;
32+
},
33+
},
34+
35+
actions: [
36+
{
37+
slug: "list-sites",
38+
label: "List Sites",
39+
description: "Get all sites accessible with the API token",
40+
category: "Webflow",
41+
stepFunction: "listSitesStep",
42+
stepImportPath: "list-sites",
43+
outputFields: [
44+
{ field: "sites", description: "Array of site objects" },
45+
{ field: "count", description: "Number of sites returned" },
46+
],
47+
configFields: [],
48+
},
49+
{
50+
slug: "get-site",
51+
label: "Get Site",
52+
description: "Get details of a specific Webflow site",
53+
category: "Webflow",
54+
stepFunction: "getSiteStep",
55+
stepImportPath: "get-site",
56+
outputFields: [
57+
{ field: "id", description: "Site ID" },
58+
{ field: "displayName", description: "Display name of the site" },
59+
{ field: "shortName", description: "Short name (subdomain)" },
60+
{ field: "previewUrl", description: "Preview URL" },
61+
{ field: "lastPublished", description: "Last published timestamp" },
62+
{ field: "customDomains", description: "Array of custom domains" },
63+
],
64+
configFields: [
65+
{
66+
key: "siteId",
67+
label: "Site ID",
68+
type: "template-input",
69+
placeholder: "site-id or {{NodeName.id}}",
70+
example: "580e63e98c9a982ac9b8b741",
71+
required: true,
72+
},
73+
],
74+
},
75+
{
76+
slug: "publish-site",
77+
label: "Publish Site",
78+
description: "Publish a site to one or more domains",
79+
category: "Webflow",
80+
stepFunction: "publishSiteStep",
81+
stepImportPath: "publish-site",
82+
outputFields: [
83+
{ field: "publishedDomains", description: "Array of published domain URLs" },
84+
{ field: "publishedToSubdomain", description: "Whether published to Webflow subdomain" },
85+
],
86+
configFields: [
87+
{
88+
key: "siteId",
89+
label: "Site ID",
90+
type: "template-input",
91+
placeholder: "site-id or {{NodeName.id}}",
92+
example: "580e63e98c9a982ac9b8b741",
93+
required: true,
94+
},
95+
{
96+
key: "publishToWebflowSubdomain",
97+
label: "Publish to Webflow Subdomain",
98+
type: "select",
99+
options: [
100+
{ value: "true", label: "Yes" },
101+
{ value: "false", label: "No" },
102+
],
103+
defaultValue: "true",
104+
},
105+
{
106+
key: "customDomainIds",
107+
label: "Custom Domain IDs (comma-separated)",
108+
type: "template-input",
109+
placeholder: "domain-id-1, domain-id-2",
110+
example: "589a331aa51e760df7ccb89d",
111+
},
112+
],
113+
},
114+
],
115+
};
116+
117+
registerIntegration(webflowPlugin);
118+
119+
export default webflowPlugin;

plugins/webflow/steps/get-site.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import "server-only";
2+
3+
import { fetchCredentials } from "@/lib/credential-fetcher";
4+
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
5+
import { getErrorMessage } from "@/lib/utils";
6+
import type { WebflowCredentials } from "../credentials";
7+
8+
const WEBFLOW_API_URL = "https://api.webflow.com/v2";
9+
10+
type WebflowSiteResponse = {
11+
id: string;
12+
workspaceId: string;
13+
createdOn: string;
14+
displayName: string;
15+
shortName: string;
16+
lastPublished?: string;
17+
lastUpdated: string;
18+
previewUrl: string;
19+
timeZone: string;
20+
customDomains?: Array<{
21+
id: string;
22+
url: string;
23+
lastPublished?: string;
24+
}>;
25+
};
26+
27+
type GetSiteResult =
28+
| {
29+
success: true;
30+
id: string;
31+
displayName: string;
32+
shortName: string;
33+
previewUrl: string;
34+
lastPublished?: string;
35+
lastUpdated: string;
36+
timeZone: string;
37+
customDomains: Array<{
38+
id: string;
39+
url: string;
40+
lastPublished?: string;
41+
}>;
42+
}
43+
| { success: false; error: string };
44+
45+
export type GetSiteCoreInput = {
46+
siteId: string;
47+
};
48+
49+
export type GetSiteInput = StepInput &
50+
GetSiteCoreInput & {
51+
integrationId?: string;
52+
};
53+
54+
async function stepHandler(
55+
input: GetSiteCoreInput,
56+
credentials: WebflowCredentials
57+
): Promise<GetSiteResult> {
58+
const apiKey = credentials.WEBFLOW_API_KEY;
59+
60+
if (!apiKey) {
61+
return {
62+
success: false,
63+
error:
64+
"WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.",
65+
};
66+
}
67+
68+
if (!input.siteId) {
69+
return {
70+
success: false,
71+
error: "Site ID is required",
72+
};
73+
}
74+
75+
try {
76+
const response = await fetch(
77+
`${WEBFLOW_API_URL}/sites/${encodeURIComponent(input.siteId)}`,
78+
{
79+
method: "GET",
80+
headers: {
81+
Accept: "application/json",
82+
Authorization: `Bearer ${apiKey}`,
83+
},
84+
});
85+
86+
if (!response.ok) {
87+
const errorData = (await response.json()) as { message?: string };
88+
return {
89+
success: false,
90+
error: errorData.message || `HTTP ${response.status}`,
91+
};
92+
}
93+
94+
const site = (await response.json()) as WebflowSiteResponse;
95+
96+
return {
97+
success: true,
98+
id: site.id,
99+
displayName: site.displayName,
100+
shortName: site.shortName,
101+
previewUrl: site.previewUrl,
102+
lastPublished: site.lastPublished,
103+
lastUpdated: site.lastUpdated,
104+
timeZone: site.timeZone,
105+
customDomains: site.customDomains || [],
106+
};
107+
} catch (error) {
108+
return {
109+
success: false,
110+
error: `Failed to get site: ${getErrorMessage(error)}`,
111+
};
112+
}
113+
}
114+
115+
export async function getSiteStep(
116+
input: GetSiteInput
117+
): Promise<GetSiteResult> {
118+
"use step";
119+
120+
const credentials = input.integrationId
121+
? await fetchCredentials(input.integrationId)
122+
: {};
123+
124+
return withStepLogging(input, () => stepHandler(input, credentials));
125+
}
126+
getSiteStep.maxRetries = 0;
127+
128+
export const _integrationType = "webflow";
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import "server-only";
2+
3+
import { fetchCredentials } from "@/lib/credential-fetcher";
4+
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
5+
import { getErrorMessage } from "@/lib/utils";
6+
import type { WebflowCredentials } from "../credentials";
7+
8+
const WEBFLOW_API_URL = "https://api.webflow.com/v2";
9+
10+
type WebflowSite = {
11+
id: string;
12+
workspaceId: string;
13+
createdOn: string;
14+
displayName: string;
15+
shortName: string;
16+
lastPublished?: string;
17+
lastUpdated: string;
18+
previewUrl: string;
19+
timeZone: string;
20+
customDomains?: Array<{
21+
id: string;
22+
url: string;
23+
lastPublished?: string;
24+
}>;
25+
};
26+
27+
type ListSitesResult =
28+
| {
29+
success: true;
30+
sites: Array<{
31+
id: string;
32+
displayName: string;
33+
shortName: string;
34+
previewUrl: string;
35+
lastPublished?: string;
36+
lastUpdated: string;
37+
customDomains: string[];
38+
}>;
39+
count: number;
40+
}
41+
| { success: false; error: string };
42+
43+
export type ListSitesCoreInput = Record<string, never>;
44+
45+
export type ListSitesInput = StepInput &
46+
ListSitesCoreInput & {
47+
integrationId?: string;
48+
};
49+
50+
async function stepHandler(
51+
_input: ListSitesCoreInput,
52+
credentials: WebflowCredentials
53+
): Promise<ListSitesResult> {
54+
const apiKey = credentials.WEBFLOW_API_KEY;
55+
56+
if (!apiKey) {
57+
return {
58+
success: false,
59+
error:
60+
"WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.",
61+
};
62+
}
63+
64+
try {
65+
const response = await fetch(`${WEBFLOW_API_URL}/sites`, {
66+
method: "GET",
67+
headers: {
68+
Accept: "application/json",
69+
Authorization: `Bearer ${apiKey}`,
70+
},
71+
});
72+
73+
if (!response.ok) {
74+
const errorData = (await response.json()) as { message?: string };
75+
return {
76+
success: false,
77+
error: errorData.message || `HTTP ${response.status}`,
78+
};
79+
}
80+
81+
const data = (await response.json()) as { sites: WebflowSite[] };
82+
83+
const sites = data.sites.map((site) => ({
84+
id: site.id,
85+
displayName: site.displayName,
86+
shortName: site.shortName,
87+
previewUrl: site.previewUrl,
88+
lastPublished: site.lastPublished,
89+
lastUpdated: site.lastUpdated,
90+
customDomains: site.customDomains?.map((d) => d.url) || [],
91+
}));
92+
93+
return {
94+
success: true,
95+
sites,
96+
count: sites.length,
97+
};
98+
} catch (error) {
99+
return {
100+
success: false,
101+
error: `Failed to list sites: ${getErrorMessage(error)}`,
102+
};
103+
}
104+
}
105+
106+
export async function listSitesStep(
107+
input: ListSitesInput
108+
): Promise<ListSitesResult> {
109+
"use step";
110+
111+
const credentials = input.integrationId
112+
? await fetchCredentials(input.integrationId)
113+
: {};
114+
115+
return withStepLogging(input, () => stepHandler(input, credentials));
116+
}
117+
listSitesStep.maxRetries = 0;
118+
119+
export const _integrationType = "webflow";

0 commit comments

Comments
 (0)