Skills framer-cms
install
source · Clone the upstream repo
git clone https://github.com/openclaw/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/berthelol/framer-crm-api" ~/.claude/skills/clawdbot-skills-framer-cms && rm -rf "$T"
manifest:
skills/berthelol/framer-crm-api/SKILL.mdsource content
Framer CMS — Server API Skill
Manage Framer CMS content programmatically via the
framer-api npm package. Push articles, upload images, create collections, and publish/deploy — all from the terminal, no Framer app needed.
First-time setup (onboarding)
If this is the first time the user uses this skill in a project, run the onboarding flow described in
references/onboarding.md.
Quick check: Look for
FRAMER_PROJECT_URL and FRAMER_API_KEY in the user's .env file or environment. If missing, onboard.
How it works
This skill uses the Framer Server API (
framer-api npm package) which connects to Framer projects via WebSocket using an API key. It provides full CMS CRUD, image uploads, publishing, and deployment.
Important: The
framer-api package must be installed in the project. If not present, run:
npm i framer-api
All operations use ES module scripts (
.mjs files) with this connection pattern:
import { connect } from "framer-api" // IMPORTANT: API key is passed as a plain string (2nd argument), NOT as {apiKey: "..."} const framer = await connect(process.env.FRAMER_PROJECT_URL, process.env.FRAMER_API_KEY) try { // ... operations ... } finally { await framer.disconnect() }
Available operations
CMS Collections
| Operation | Method | Notes |
|---|---|---|
| List collections | | Returns all CMS collections |
| Get one collection | | By collection ID |
| Create collection | | Creates empty collection |
| Get fields | | Field definitions (name, type, id) |
| Add fields | | Add new fields to collection |
| Remove fields | | Delete fields by ID |
| Reorder fields | | Set field display order |
CMS Items (articles, entries)
| Operation | Method | Notes |
|---|---|---|
| List items | | All items with field data |
| Create items | | Create new items. Returns — re-fetch with to get IDs |
| Update item fields | | MUST wrap in — without it, values are silently ignored |
| Update item slug/draft | | Slug and draft are set directly (NOT inside fieldData) |
| Delete item | | Single item |
| Bulk delete | | Multiple items |
| Reorder items | | Set display order |
⚠️ Critical: How to update CMS item fields
The
setAttributes method has a non-obvious API design — field values MUST be wrapped in a fieldData key:
// ✅ CORRECT — fields wrapped in fieldData await item.setAttributes({ fieldData: { [titleFieldId]: { type: "string", value: "New Title" } } }) // ❌ WRONG — silently ignored, no error thrown await item.setAttributes({ [titleFieldId]: { type: "string", value: "New Title" } }) // ❌ WRONG — also silently ignored await item.setAttributes({ [titleFieldId]: "New Title" })
Partial updates work: Only specified fields are changed. Other fields are preserved.
Non-field attributes (slug, draft) go directly on the object, NOT inside fieldData:
await item.setAttributes({ slug: "new-slug", draft: false })
Field data format
When creating/updating items, field data is keyed by field ID (not name):
const fields = await collection.getFields() const titleField = fields.find(f => f.name === "Title") await collection.addItems([{ slug: "my-article", fieldData: { [titleField.id]: { type: "string", value: "My Article Title" }, } }])
Supported field types and their value format:
| Type | Value format | Example |
|---|---|---|
| | |
| | |
| | |
| (UTC ISO) | |
| (HTML) | |
| (URL) | |
| object | See image upload section |
| (case name) | |
| (hex/rgba) | |
| object | Similar to image |
| (item ID) | |
| | |
Images
Upload images from public URLs, then use the returned asset in CMS items:
const asset = await framer.uploadImage("https://example.com/photo.jpg") // asset = { id, url, thumbnailUrl } await item.setAttributes({ fieldData: { [thumbnailField.id]: { type: "image", value: asset.url } } })
Publishing & deployment
// Create a preview deployment const result = await framer.publish() // result = { deployment: { id }, hostnames: [...] } // Promote preview to production await framer.deploy(result.deployment.id)
Always ask the user before deploying to production. Publishing a preview is safe; deploying is live.
Project info & changes
await framer.getProjectInfo() // { id, name, apiVersion1Id } await framer.getCurrentUser() // { id, name, avatar } await framer.getPublishInfo() // Current deployment status await framer.getChangedPaths() // { added, removed, modified } await framer.getChangeContributors() // Contributor UUIDs await framer.getDeployments() // All deployment history
Other operations
| Operation | Method | Notes |
|---|---|---|
| Color styles | , | Design tokens |
| Text styles | , | Typography tokens |
| Code files | , | Custom code overrides |
| Custom code | | Head/body code injection |
| Fonts | | Project fonts |
| Locales | , | i18n |
| Pages | , | Page management |
| Screenshots | | PNG buffer of any node |
| Redirects | | Requires paid plan |
| Node tree | , , | DOM traversal |
Common workflows
Push a new article to CMS
See
references/cms-operations.md for the full pattern including field resolution, image upload, and error handling.
Bulk update articles
const items = await collection.getItems() for (const item of items) { await item.setAttributes({ fieldData: { [metaField.id]: { type: "string", value: generateMeta(item) } } }) }
Publish after CMS changes
const changes = await framer.getChangedPaths() if (changes.added.length || changes.modified.length || changes.removed.length) { const result = await framer.publish() console.log("Preview:", result.hostnames) // Ask user before: await framer.deploy(result.deployment.id) }
Important notes
- API key scope: Each key is bound to one project. For multiple Framer sites, store multiple keys.
- WebSocket connection: The
call opens a persistent WebSocket. Always callconnect()
when done, or usedisconnect()
for auto-cleanup.using framer = await connect(...) - Field IDs, not names: CMS operations use field IDs. Always call
first and resolve names to IDs.getFields() - Image fields: Pass the full
URL fromframerusercontent.com
, not the asset ID.uploadImage() - Proxy methods: Most methods (getCollections, publish, etc.) are proxied — they don't appear in
but work correctly.Object.keys(framer) - Rate limits: No documented rate limits, but avoid hammering. Add small delays for bulk operations (100+ items).
fields: Accept standard HTML (h1-h6, p, ul, ol, li, a, strong, em, img, blockquote, pre, code, table, etc.).formattedText- Draft items: Items can have
— drafts are excluded from publishing.draft: true - Blog Posts collection: Collections managed by
are read-only via the API. Only"thisPlugin"
managed collections can be modified."user"