# Publish Webflow

> Use when the user wants to publish a blog post draft to Webflow CMS via the Webflow API v2. Reads a markdown draft, maps frontmatter to CMS fields, creates or updates the CMS item, and optionally publishes it live.

- Skill: `busyeugene/publish-webflow` (Agent Skill)
- Install (CLI): `npx skillmds@latest add busyeugene/publish-webflow`
- Raw SKILL.md: https://api.skillmd.com/api/skills/busyeugene/publish-webflow/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: MIT
- Author: busyeugene (https://skillmd.com/u/busyeugene)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/busyeugene/publish-webflow

---


# Publish → Webflow

Publish a local markdown draft to Webflow CMS using the [Webflow Data API v2](https://developers.webflow.com/data/reference/rest-introduction).

## Setup

Required:
- `WEBFLOW_API_TOKEN` — a Data API token (Site Settings → Apps & Integrations → API Access). Scopes needed: `cms:read`, `cms:write`, `sites:read`, and (if publishing live) `sites:write`.
- `WEBFLOW_SITE_ID` — the site ID. Get it via `GET https://api.webflow.com/v2/sites`.
- `WEBFLOW_COLLECTION_ID` — the collection ID for your blog. Get it via `GET /v2/sites/{site_id}/collections`.

Optional:
- `SLACK_WEBHOOK_URL` — Slack notification when a post goes live.

All requests use:
```
Authorization: Bearer ${WEBFLOW_API_TOKEN}
accept-version: 2.0.0
Content-Type: application/json
```

## Inputs

1. **Draft path** — markdown file under `posts/`.
2. **Mode** — `draft` (create as unpublished) or `live` (create and publish). Default `draft`.
3. **Update existing?** — if an item with the same slug exists, `skip`, `update`, or `create-new`. Default `update`.
4. **Field mapping override** — optional: path to a JSON file mapping markdown frontmatter keys to Webflow field slugs. Default mapping is documented below.

Use `AskUserQuestion` for mode and existing-item handling.

## Process

### 1. Validate collection schema

Call `GET /v2/collections/{WEBFLOW_COLLECTION_ID}` and cache the field list. For each required frontmatter field, confirm there's a matching Webflow field. If a field is missing, fail fast with a clear error naming which Webflow field to add.

**Default frontmatter → Webflow field mapping:**

| Frontmatter key | Webflow field slug | Type |
|---|---|---|
| `title` | `name` | PlainText |
| `slug` | `slug` | PlainText |
| `meta_title` | `meta-title` | PlainText |
| `meta_description` | `meta-description` | PlainText |
| `author` | `author` | Reference (fallback PlainText) |
| `date` | `published-on` | Date |
| `hero_image` | `hero-image` | Image |
| `hero_alt` | `hero-alt` | PlainText |
| `keyword` | `primary-keyword` | PlainText |
| `status` | — | (ignored; controlled by publish mode) |
| _body_ | `post-body` | RichText |

If the user provides a custom mapping file, it overrides the defaults.

### 2. Load and transform the draft

- Read the draft file.
- Parse frontmatter.
- Convert the markdown body to Webflow-compatible HTML (rich text field accepts HTML). Preserve: headings, paragraphs, lists, code blocks, blockquotes, images, internal and external links. Strip any HTML comments.
- For the hero image:
  - If `hero_image` is a local path, upload via the Webflow 2-step asset flow:
    1. `POST /v2/sites/{site_id}/assets` with `{ "fileName": "...", "fileHash": "<md5>" }`. Response contains `uploadUrl` and `uploadDetails` (S3 presigned fields).
    2. POST the file as multipart/form-data to `uploadUrl`, including every field from `uploadDetails` before the `file` field. Use the returned asset `hostedUrl` in `fieldData`.
  - If it's already a URL, use it directly.
- For author as a reference field: resolve the name to an author collection item ID via `GET /v2/collections/{authors_collection}/items?name={author}`. If not found, create the author item (or fall back to PlainText if the field allows).

### 3. Check for existing item

`GET /v2/collections/{WEBFLOW_COLLECTION_ID}/items?slug={slug}`

- If found and mode is `skip` → stop and report.
- If found and mode is `update` → `PATCH /v2/collections/{collection_id}/items/{item_id}` with the new field values.
- If found and mode is `create-new` → append a timestamp to the slug, then create.
- If not found → create.

### 4. Create or update

**Create (staged / draft):**
```
POST /v2/collections/{WEBFLOW_COLLECTION_ID}/items
{
  "isArchived": false,
  "isDraft": <true if mode=draft else false>,
  "fieldData": { ...mapped fields }
}
```

For bulk creates (multiple posts at once), use `POST /v2/collections/{collection_id}/items/bulk` with `{ "items": [...] }`. As of Dec 2024 newly-created items default to `isDraft: true` unless the live endpoint is used.

**Create live (mode = live, single call):** use `POST /v2/collections/{collection_id}/items/live` with the same body and `isDraft: false`. This creates and publishes in one shot.

**Update:**
```
PATCH /v2/collections/{WEBFLOW_COLLECTION_ID}/items/{item_id}
{
  "fieldData": { ...mapped fields }
}
```

Handle the response: capture `id` (item ID), `lastUpdated`, and the `fieldData.slug`.

### 5. Publish live (if mode = live)

There is **no per-item "promote to live" endpoint** in the Data API v2. Options:

- **New item**: use the `POST /v2/collections/{collection_id}/items/live` create-live endpoint above in step 4 instead of the staged create.
- **Existing staged item**: PATCH the item with `isDraft: false` via `PATCH /v2/collections/{collection_id}/items/{item_id}/live` (the `/live` bulk-publish path accepts an item list) — or publish the entire site, which flushes all staged items.

Then publish the site so any staged changes are visible:
```
POST /v2/sites/{WEBFLOW_SITE_ID}/publish
{ "publishToWebflowSubdomain": false, "customDomains": [...] }
```

Or if the user only wants the staging subdomain:
```
{ "publishToWebflowSubdomain": true }
```

Ask via `AskUserQuestion` the first time a publish runs in a session.

### 6. Optional Slack notification

If `SLACK_WEBHOOK_URL` is set and publish was successful:

```
POST ${SLACK_WEBHOOK_URL}
Content-Type: application/json

{ "text": "📤 New post published: <{live URL}|{title}>" }
```

### 7. Write a publish log

Append a line to `publish-log.md` at the project root:

```
| {YYYY-MM-DD HH:mm} | webflow | {mode} | {title} | {item_id} | {live URL or "—"} | {status} |
```

### 8. Print summary

One block: mode, item ID, live URL (if live), Slack notified (Y/N), publish-log updated.

## Fallbacks

- **Missing required env var:** fail fast with the exact variable name.
- **Collection schema mismatch:** print the missing field names and the expected types; do not guess.
- **Image upload fails:** ask the user to upload manually via Webflow and retry; don't create the item without the hero if the field is required.
- **Site publish rate limited:** create/update the item anyway, skip the site publish, and note it in the log so the user can publish manually from the Webflow dashboard.

## Safety

- Default mode is `draft`. This skill never publishes live without explicit user confirmation in the current session.
- Never delete existing items. If a collision happens in `skip` mode, stop and report.

## Verification

1. Item exists in the target collection (`GET /items/{id}` returns 200).
2. `fieldData` contains all mapped fields with expected values.
3. If mode = live, the public URL returns 200.
4. `publish-log.md` has a new row.
5. Slack notification delivered (if configured).

