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.
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.
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()
}
| Operation | Method | Notes |
|---|---|---|
| ----------- | -------- | ------- |
| List collections | framer.getCollections() | Returns all CMS collections |
| Get one collection | framer.getCollection(id) | By collection ID |
| Create collection | framer.createCollection(name) | Creates empty collection |
| Get fields | collection.getFields() | Field definitions (name, type, id) |
| Add fields | collection.addFields([{type, name}]) | Add new fields to collection |
| Remove fields | collection.removeFields([fieldId]) | Delete fields by ID |
| Reorder fields | collection.setFieldOrder([fieldIds]) | Set field display order |
| Operation | Method | Notes |
|---|---|---|
| ----------- | -------- | ------- |
| List items | collection.getItems() | All items with field data |
| Create items | collection.addItems([{slug, fieldData}]) | Create new items. Returns undefined — re-fetch with getItems() to get IDs |
| Update item fields | item.setAttributes({ fieldData: { [fieldId]: {type, value} } }) | MUST wrap in fieldData: — without it, values are silently ignored |
| Update item slug/draft | item.setAttributes({ slug: "new", draft: false }) | Slug and draft are set directly (NOT inside fieldData) |
| Delete item | item.remove() | Single item |
| Bulk delete | collection.removeItems([itemIds]) | Multiple items |
| Reorder items | collection.setItemOrder([itemIds]) | Set display order |
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 })
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 |
|---|---|---|
| ------ | ------------- | --------- |
string | string | { type: "string", value: "Hello" } |
number | number | { type: "number", value: 42 } |
boolean | boolean | { type: "boolean", value: true } |
date | string (UTC ISO) | { type: "date", value: "2026-04-06T00:00:00Z" } |
formattedText | string (HTML) | { type: "formattedText", value: " |
link | string (URL) | { type: "link", value: "https://example.com" } |
image | ImageAsset object | See image upload section |
enum | string (case name) | { type: "enum", value: "Published" } |
color | string (hex/rgba) | { type: "color", value: "#FF0000" } |
file | FileAsset object | Similar to image |
collectionReference | string (item ID) | { type: "collectionReference", value: "itemId123" } |
multiCollectionReference | string[] | { type: "multiCollectionReference", value: ["id1","id2"] } |
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 }
}
})
// 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.
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
| Operation | Method | Notes |
|---|---|---|
| ----------- | -------- | ------- |
| Color styles | getColorStyles(), createColorStyle() | Design tokens |
| Text styles | getTextStyles(), createTextStyle() | Typography tokens |
| Code files | getCodeFiles(), createCodeFile(name, code) | Custom code overrides |
| Custom code | getCustomCode() | Head/body code injection |
| Fonts | getFonts() | Project fonts |
| Locales | getLocales(), getDefaultLocale() | i18n |
| Pages | createWebPage(path), removeNode(id) | Page management |
| Screenshots | screenshot(nodeId, options) | PNG buffer of any node |
| Redirects | addRedirects([{from, to}]) | Requires paid plan |
| Node tree | getNode(id), getChildren(id), getParent(id) | DOM traversal |
See references/cms-operations.md for the full pattern including field resolution, image upload, and error handling.
const items = await collection.getItems()
for (const item of items) {
await item.setAttributes({
fieldData: {
[metaField.id]: { type: "string", value: generateMeta(item) }
}
})
}
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)
}
connect() call opens a persistent WebSocket. Always call disconnect() when done, or use using framer = await connect(...) for auto-cleanup.getFields() first and resolve names to IDs.framerusercontent.com URL from uploadImage(), not the asset ID.Object.keys(framer) but work correctly.formattedText fields: Accept standard HTML (h1-h6, p, ul, ol, li, a, strong, em, img, blockquote, pre, code, table, etc.).draft: true — drafts are excluded from publishing."thisPlugin" are read-only via the API. Only "user" managed collections can be modified.共 1 个版本