From webflow-pack
Applies production Webflow API SDK patterns: singleton client, typed error handling, pagination helpers, raw responses. For integrations, refactoring, team standards.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin webflow-packThis skill is limited to using the following tools:
Production-ready patterns for the `webflow-api` SDK (v3.x). Covers singleton client,
Implements layered TypeScript architecture for Webflow Data API v2 with client wrapper, CMS sync, webhook handlers, Express routes, and Redis caching.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Production-ready patterns for the webflow-api SDK (v3.x). Covers singleton client,
typed error handling, pagination, raw response headers, and multi-tenant factory.
webflow-api v3.x installed// src/webflow/client.ts
import { WebflowClient } from "webflow-api";
interface WebflowConfig {
accessToken: string;
timeout?: number; // Request timeout in ms (default: SDK default)
maxRetries?: number; // Auto-retry on 429/5xx (default: 2)
}
let instance: WebflowClient | null = null;
export function getWebflowClient(config?: Partial<WebflowConfig>): WebflowClient {
if (!instance) {
const token = config?.accessToken || process.env.WEBFLOW_API_TOKEN;
if (!token) throw new Error("WEBFLOW_API_TOKEN required");
instance = new WebflowClient({
accessToken: token,
// The SDK supports timeout and maxRetries natively
...(config?.timeout && { timeout: config.timeout }),
...(config?.maxRetries !== undefined && { maxRetries: config.maxRetries }),
});
}
return instance;
}
// Reset client (useful for token rotation)
export function resetWebflowClient(): void {
instance = null;
}
The SDK throws WebflowError subclasses. Handle them by type:
import { WebflowClient } from "webflow-api";
// SDK errors are subclasses of WebflowError
// Common HTTP status codes: 400, 401, 403, 404, 409, 429, 500
async function safeWebflowCall<T>(
operation: () => Promise<T>,
context: string
): Promise<{ data: T | null; error: string | null }> {
try {
const data = await operation();
return { data, error: null };
} catch (err: any) {
const statusCode = err.statusCode || err.status;
const message = err.message || String(err);
// Log with context for debugging
console.error(`[Webflow] ${context} failed:`, {
status: statusCode,
message,
body: err.body,
});
// Classify the error
switch (statusCode) {
case 401:
console.error("Token invalid or revoked. Rotate token.");
break;
case 403:
console.error("Missing required scope. Check token scopes.");
break;
case 404:
console.error("Resource not found. Verify IDs.");
break;
case 409:
console.error("Conflict — item may already exist with this slug.");
break;
case 429:
console.error("Rate limited. SDK will auto-retry with backoff.");
break;
default:
if (statusCode >= 500) {
console.error("Webflow server error. Retry later.");
}
}
return { data: null, error: message };
}
}
// Usage
const { data: sites, error } = await safeWebflowCall(
() => webflow.sites.list(),
"sites.list"
);
Webflow v2 uses offset-based pagination. Iterate through all pages:
interface PaginatedResult<T> {
items: T[];
pagination: { limit: number; offset: number; total: number };
}
async function fetchAllItems<T>(
fetcher: (offset: number, limit: number) => Promise<PaginatedResult<T>>,
pageSize = 100
): Promise<T[]> {
const allItems: T[] = [];
let offset = 0;
while (true) {
const result = await fetcher(offset, pageSize);
allItems.push(...result.items);
if (allItems.length >= result.pagination.total) break;
offset += pageSize;
}
return allItems;
}
// Usage: Fetch all items from a collection
const allItems = await fetchAllItems((offset, limit) =>
webflow.collections.items.listItems(collectionId, { offset, limit })
.then(res => ({
items: res.items || [],
pagination: res.pagination || { limit, offset, total: 0 },
}))
);
Access rate limit headers using .withRawResponse():
async function getWithRateLimitInfo(siteId: string) {
// .withRawResponse() returns { data, rawResponse }
const response = await webflow.sites.get(siteId)
// @ts-ignore — withRawResponse is available on all SDK methods
;
// For rate limit monitoring, use a wrapper that extracts headers
const rawFetch = await fetch(
`https://api.webflow.com/v2/sites/${siteId}`,
{
headers: {
Authorization: `Bearer ${process.env.WEBFLOW_API_TOKEN}`,
"Content-Type": "application/json",
},
}
);
const rateLimitRemaining = rawFetch.headers.get("X-RateLimit-Remaining");
const rateLimitLimit = rawFetch.headers.get("X-RateLimit-Limit");
const retryAfter = rawFetch.headers.get("Retry-After");
console.log(`Rate limit: ${rateLimitRemaining}/${rateLimitLimit}`);
return rawFetch.json();
}
For apps serving multiple Webflow workspaces:
const clients = new Map<string, WebflowClient>();
export function getClientForTenant(tenantId: string): WebflowClient {
if (!clients.has(tenantId)) {
const token = getTenantToken(tenantId); // From your DB/vault
clients.set(
tenantId,
new WebflowClient({ accessToken: token })
);
}
return clients.get(tenantId)!;
}
// Rotate a tenant's token without downtime
export function rotateTenantToken(tenantId: string, newToken: string): void {
clients.set(
tenantId,
new WebflowClient({ accessToken: newToken })
);
}
The CMS bulk endpoints accept up to 100 items per request:
async function bulkCreateItems(
collectionId: string,
items: Array<{ fieldData: Record<string, any>; isDraft?: boolean }>,
batchSize = 100
): Promise<string[]> {
const createdIds: string[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const result = await webflow.collections.items.createItemsBulk(
collectionId,
{ items: batch }
);
if (result.items) {
createdIds.push(...result.items.map(item => item.id!));
}
console.log(`Created batch ${Math.floor(i / batchSize) + 1}: ${batch.length} items`);
}
return createdIds;
}
import { z } from "zod";
const WebflowSiteSchema = z.object({
id: z.string(),
displayName: z.string(),
shortName: z.string(),
lastPublished: z.string().nullable(),
customDomains: z.array(z.object({
url: z.string(),
})).optional(),
});
const WebflowCollectionItemSchema = z.object({
id: z.string(),
isDraft: z.boolean(),
isArchived: z.boolean(),
createdOn: z.string(),
lastUpdated: z.string(),
fieldData: z.record(z.unknown()),
});
// Validate API responses at runtime
async function getValidatedSite(siteId: string) {
const site = await webflow.sites.get(siteId);
return WebflowSiteSchema.parse(site);
}
| Pattern | Use Case | Benefit |
|---|---|---|
| Safe wrapper | All API calls | Prevents uncaught exceptions |
| Status classification | Error triage | Clear remediation path |
| Pagination helper | Large datasets | No missed items |
| Zod validation | Response integrity | Catches API changes |
| Token rotation | Security compliance | Zero-downtime rotation |
Apply patterns in webflow-core-workflow-a for CMS content management.