Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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;
126 changes: 126 additions & 0 deletions plugins/webflow/steps/get-site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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/${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