Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import "./slack";
import "./stripe";
import "./superagent";
import "./v0";
import "./webflow";

export type {
ActionConfigField,
Expand Down
3 changes: 3 additions & 0 deletions plugins/webflow/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type WebflowCredentials = {
WEBFLOW_API_KEY?: string;
};
14 changes: 14 additions & 0 deletions plugins/webflow/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function WebflowIcon({ className }: { className?: string }) {
return (
<svg
aria-label="Webflow logo"
className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Webflow</title>
<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"/>
</svg>
);
}
119 changes: 119 additions & 0 deletions plugins/webflow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { IntegrationPlugin } from "../registry";
import { registerIntegration } from "../registry";
import { WebflowIcon } from "./icon";

const webflowPlugin: IntegrationPlugin = {
type: "webflow",
label: "Webflow",
description: "Publish and manage Webflow sites",

icon: WebflowIcon,

formFields: [
{
id: "apiKey",
label: "API Token",
type: "password",
placeholder: "your-api-token",
configKey: "apiKey",
envVar: "WEBFLOW_API_KEY",
helpText: "Generate an API token from ",
helpLink: {
text: "Webflow Dashboard",
url: "https://webflow.com/dashboard",
},
},
],

testConfig: {
getTestFunction: async () => {
const { testWebflow } = await import("./test");
return testWebflow;
},
},

actions: [
{
slug: "list-sites",
label: "List Sites",
description: "Get all sites accessible with the API token",
category: "Webflow",
stepFunction: "listSitesStep",
stepImportPath: "list-sites",
outputFields: [
{ field: "sites", description: "Array of site objects" },
{ field: "count", description: "Number of sites returned" },
],
configFields: [],
},
{
slug: "get-site",
label: "Get Site",
description: "Get details of a specific Webflow site",
category: "Webflow",
stepFunction: "getSiteStep",
stepImportPath: "get-site",
outputFields: [
{ field: "id", description: "Site ID" },
{ field: "displayName", description: "Display name of the site" },
{ field: "shortName", description: "Short name (subdomain)" },
{ field: "previewUrl", description: "Preview URL" },
{ field: "lastPublished", description: "Last published timestamp" },
{ field: "customDomains", description: "Array of custom domains" },
],
configFields: [
{
key: "siteId",
label: "Site ID",
type: "template-input",
placeholder: "site-id or {{NodeName.id}}",
example: "580e63e98c9a982ac9b8b741",
required: true,
},
],
},
{
slug: "publish-site",
label: "Publish Site",
description: "Publish a site to one or more domains",
category: "Webflow",
stepFunction: "publishSiteStep",
stepImportPath: "publish-site",
outputFields: [
{ field: "publishedDomains", description: "Array of published domain URLs" },
{ field: "publishedToSubdomain", description: "Whether published to Webflow subdomain" },
],
configFields: [
{
key: "siteId",
label: "Site ID",
type: "template-input",
placeholder: "site-id or {{NodeName.id}}",
example: "580e63e98c9a982ac9b8b741",
required: true,
},
{
key: "publishToWebflowSubdomain",
label: "Publish to Webflow Subdomain",
type: "select",
options: [
{ value: "true", label: "Yes" },
{ value: "false", label: "No" },
],
defaultValue: "true",
},
{
key: "customDomainIds",
label: "Custom Domain IDs (comma-separated)",
type: "template-input",
placeholder: "domain-id-1, domain-id-2",
example: "589a331aa51e760df7ccb89d",
},
],
},
],
};

registerIntegration(webflowPlugin);

export default webflowPlugin;
128 changes: 128 additions & 0 deletions plugins/webflow/steps/get-site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import "server-only";

import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessage } from "@/lib/utils";
import type { WebflowCredentials } from "../credentials";

const WEBFLOW_API_URL = "https://api.webflow.com/v2";

type WebflowSiteResponse = {
id: string;
workspaceId: string;
createdOn: string;
displayName: string;
shortName: string;
lastPublished?: string;
lastUpdated: string;
previewUrl: string;
timeZone: string;
customDomains?: Array<{
id: string;
url: string;
lastPublished?: string;
}>;
};

type GetSiteResult =
| {
success: true;
id: string;
displayName: string;
shortName: string;
previewUrl: string;
lastPublished?: string;
lastUpdated: string;
timeZone: string;
customDomains: Array<{
id: string;
url: string;
lastPublished?: string;
}>;
}
| { success: false; error: string };

export type GetSiteCoreInput = {
siteId: string;
};

export type GetSiteInput = StepInput &
GetSiteCoreInput & {
integrationId?: string;
};

async function stepHandler(
input: GetSiteCoreInput,
credentials: WebflowCredentials
): Promise<GetSiteResult> {
const apiKey = credentials.WEBFLOW_API_KEY;

if (!apiKey) {
return {
success: false,
error:
"WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.",
};
}

if (!input.siteId) {
return {
success: false,
error: "Site ID is required",
};
}

try {
const response = await fetch(
`${WEBFLOW_API_URL}/sites/${encodeURIComponent(input.siteId)}`,
{
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
});

if (!response.ok) {
const errorData = (await response.json()) as { message?: string };
return {
success: false,
error: errorData.message || `HTTP ${response.status}`,
};
}

const site = (await response.json()) as WebflowSiteResponse;

return {
success: true,
id: site.id,
displayName: site.displayName,
shortName: site.shortName,
previewUrl: site.previewUrl,
lastPublished: site.lastPublished,
lastUpdated: site.lastUpdated,
timeZone: site.timeZone,
customDomains: site.customDomains || [],
};
} catch (error) {
return {
success: false,
error: `Failed to get site: ${getErrorMessage(error)}`,
};
}
}

export async function getSiteStep(
input: GetSiteInput
): Promise<GetSiteResult> {
"use step";

const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};

return withStepLogging(input, () => stepHandler(input, credentials));
}
getSiteStep.maxRetries = 0;

export const _integrationType = "webflow";
119 changes: 119 additions & 0 deletions plugins/webflow/steps/list-sites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import "server-only";

import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessage } from "@/lib/utils";
import type { WebflowCredentials } from "../credentials";

const WEBFLOW_API_URL = "https://api.webflow.com/v2";

type WebflowSite = {
id: string;
workspaceId: string;
createdOn: string;
displayName: string;
shortName: string;
lastPublished?: string;
lastUpdated: string;
previewUrl: string;
timeZone: string;
customDomains?: Array<{
id: string;
url: string;
lastPublished?: string;
}>;
};

type ListSitesResult =
| {
success: true;
sites: Array<{
id: string;
displayName: string;
shortName: string;
previewUrl: string;
lastPublished?: string;
lastUpdated: string;
customDomains: string[];
}>;
count: number;
}
| { success: false; error: string };

export type ListSitesCoreInput = Record<string, never>;

export type ListSitesInput = StepInput &
ListSitesCoreInput & {
integrationId?: string;
};

async function stepHandler(
_input: ListSitesCoreInput,
credentials: WebflowCredentials
): Promise<ListSitesResult> {
const apiKey = credentials.WEBFLOW_API_KEY;

if (!apiKey) {
return {
success: false,
error:
"WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.",
};
}

try {
const response = await fetch(`${WEBFLOW_API_URL}/sites`, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
});

if (!response.ok) {
const errorData = (await response.json()) as { message?: string };
return {
success: false,
error: errorData.message || `HTTP ${response.status}`,
};
}

const data = (await response.json()) as { sites: WebflowSite[] };

const sites = data.sites.map((site) => ({
id: site.id,
displayName: site.displayName,
shortName: site.shortName,
previewUrl: site.previewUrl,
lastPublished: site.lastPublished,
lastUpdated: site.lastUpdated,
customDomains: site.customDomains?.map((d) => d.url) || [],
}));

return {
success: true,
sites,
count: sites.length,
};
} catch (error) {
return {
success: false,
error: `Failed to list sites: ${getErrorMessage(error)}`,
};
}
}

export async function listSitesStep(
input: ListSitesInput
): Promise<ListSitesResult> {
"use step";

const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};

return withStepLogging(input, () => stepHandler(input, credentials));
}
listSitesStep.maxRetries = 0;

export const _integrationType = "webflow";
Loading