Plugins can read and write to the Framer CMS. Allowing you to create anything from a Notion Sync plugin, to a custom database integration, or an exporter.
Check out the video above for an overview on CMS Plugin concepts and a walkthrough of the CMS Starter Plugin. There are two kinds of ways to work with the CMS, Managed Collections and Unmanaged Collections. Unmanaged Collections are Collections in Framer that are primarily created and updated by people, whereas Managed Collections are primarily controlled by Plugins.
If you’re looking to build a Plugin to sync data into Framer, the best way to get started is the CMS Starter Plugin. You can use it via the following commands:
npm create framer-plugin@latest -- --starter cms
yarn create framer-plugin --starter cms
pnpm create framer-plugin --starter cms
npm create framer-plugin@latest -- --starter cms
yarn create framer-plugin --starter cms
pnpm create framer-plugin --starter cms
npm create framer-plugin@latest -- --starter cms
yarn create framer-plugin --starter cms
pnpm create framer-plugin --starter cms
Any kind of Collection can be read from. Collection refers to Unmanaged Collections that are created by Users. Use the collection Mode to access CMS data from your plugin.
See our Export CSV example.
const collection = await framer.getActiveCollection()
const collection = await framer.getActiveCollection()
const collection = await framer.getActiveCollection()
You can get a list of all the available collections.
await framer.getCollections()
await framer.getCollections()
await framer.getCollections()
When you have a reference to a specific collection, you can get its fields or items. In the CMS, fields are the columns used to define data types.
This will return an array of objects.
[
{
id: "BnNuS2i3o",
type: "string",
name: "Title",
},
{
id: "hgNaskh4n",
type: "boolean",
name: "Featured",
},
{
id: "JO9uOAhun",
type: "date",
name: "Published Date",
},
}
[
{
id: "BnNuS2i3o",
type: "string",
name: "Title",
},
{
id: "hgNaskh4n",
type: "boolean",
name: "Featured",
},
{
id: "JO9uOAhun",
type: "date",
name: "Published Date",
},
}
[
{
id: "BnNuS2i3o",
type: "string",
name: "Title",
},
{
id: "hgNaskh4n",
type: "boolean",
name: "Featured",
},
{
id: "JO9uOAhun",
type: "date",
name: "Published Date",
},
}You can also get all CMS items.
This will return all the data in the CMS as an array of objects. Item objects contain id, slug and fieldData. Field data uses the field id as keys in an object.
[
{
id: "XTM8FSHGs",
slug: "post-1",
draft: false,
fieldData: {
BnNuS2i3o: "My First Post",
hgNaskh4n: false,
JO9uOAhun: "Wed, 15 Apr 2024 08:30:27 GMT"
}
},
{
id: "AaN8oZOpy",
slug: "post-2",
draft: true,
fieldData: {
BnNuS2i3o: "My Second Post",
hgNaskh4n: true,
JO9uOAhun: "Wed, 02 Aug 2024 10:13:27 GMT"
}
}
]
[
{
id: "XTM8FSHGs",
slug: "post-1",
draft: false,
fieldData: {
BnNuS2i3o: "My First Post",
hgNaskh4n: false,
JO9uOAhun: "Wed, 15 Apr 2024 08:30:27 GMT"
}
},
{
id: "AaN8oZOpy",
slug: "post-2",
draft: true,
fieldData: {
BnNuS2i3o: "My Second Post",
hgNaskh4n: true,
JO9uOAhun: "Wed, 02 Aug 2024 10:13:27 GMT"
}
}
]
[
{
id: "XTM8FSHGs",
slug: "post-1",
draft: false,
fieldData: {
BnNuS2i3o: "My First Post",
hgNaskh4n: false,
JO9uOAhun: "Wed, 15 Apr 2024 08:30:27 GMT"
}
},
{
id: "AaN8oZOpy",
slug: "post-2",
draft: true,
fieldData: {
BnNuS2i3o: "My Second Post",
hgNaskh4n: true,
JO9uOAhun: "Wed, 02 Aug 2024 10:13:27 GMT"
}
}
]
Supported field types:
boolean — True or false
color — A color of RGBA/HSL/HEX format
number — A number
string — Any string of text
formattedText — HTML Content. H1-H6, P and other standard content elements are supported
image — An instance of an ImageAsset
file — An instance of an FileAsset
link — URL in string format
date — A date in UTC format, or DD-MM-YYYY
enum — Requires enum case options to be defined
collectionReference — A reference to an item in another Collection
multiCollectionReference — Multiple references to items in another Collection
array — An array of fields. Currently only supports a single image field, which will create a Gallery.
There is also an unsupported field type. This is returned when Framer uses a field that the plugin API does not yet support.
Since field values can be many different kinds of type, always check the type before using it.
const titleField = fieldData["XTM8FSHGs"]
if (typeof titleField === "string) {
console.log(titleField.toUpperCase())
}
const assetField = fieldData["AaN8oZOpy"]
if (isImageAsset(assetField) || isFileAsset(assetField) {
console.log(assetField.url)
}
const titleField = fieldData["XTM8FSHGs"]
if (typeof titleField === "string) {
console.log(titleField.toUpperCase())
}
const assetField = fieldData["AaN8oZOpy"]
if (isImageAsset(assetField) || isFileAsset(assetField) {
console.log(assetField.url)
}
const titleField = fieldData["XTM8FSHGs"]
if (typeof titleField === "string) {
console.log(titleField.toUpperCase())
}
const assetField = fieldData["AaN8oZOpy"]
if (isImageAsset(assetField) || isFileAsset(assetField) {
console.log(assetField.url)
}Managed Collections are fully controlled by the plugin. Fields and items can only be added, edited and deleted by the plugin and not the user.
Use a Managed Collection when you want to be able sync data between Framer and a third-party. See our Notion example.
Your Managed Collection plugin will only become available within the CMS when it supports both the configureManagedCollection and syncManagedCollection modes. Make sure to add these modes to the framer.json file. Read more on how to set this up on the configuration page.
The active mode can be checked using framer.mode. When your plugin is launched with a newly created collection, the mode will be configureManagedCollection. In configuration mode the user expects to setup the fields and data source.
After the initial creation step, the user can choose to open the plugin in either sync or configuration mode. For sync mode, the best experience is to not show any UI, a toast will be visible to indicate the activity of the plugin.
if (framer.mode === "syncManagedCollection") {
await importData(collection, rssSourceId)
await framer.closePlugin()
} else if (framer.mode === "configureManagedCollection") {
framer.showUI()
}
if (framer.mode === "syncManagedCollection") {
await importData(collection, rssSourceId)
await framer.closePlugin()
} else if (framer.mode === "configureManagedCollection") {
framer.showUI()
}
if (framer.mode === "syncManagedCollection") {
await importData(collection, rssSourceId)
await framer.closePlugin()
} else if (framer.mode === "configureManagedCollection") {
framer.showUI()
}
When your plugin is launched in either configureManagedCollection or syncManagedCollection mode, it can get the active collection via the framer.getManagedCollection().
const collection = await framer.getManagedCollection()
const collection = await framer.getManagedCollection()
const collection = await framer.getManagedCollection()
Once you have the Collection, there are several methods to perform common actions, such as getting the current fields, or adding items.
interface ManagedCollection {
getItemIds(): Promise<string[]>
setItemOrder(ids: string[]): Promise<void>
getFields(): Promise<CollectionField[]>
setFields(fields: CollectionField[]): Promise<void>
addItems(items: CollectionItem): Promise<void>
removeItems(ids: string[]): Promise<void>
}interface ManagedCollection {
getItemIds(): Promise<string[]>
setItemOrder(ids: string[]): Promise<void>
getFields(): Promise<CollectionField[]>
setFields(fields: CollectionField[]): Promise<void>
addItems(items: CollectionItem): Promise<void>
removeItems(ids: string[]): Promise<void>
}interface ManagedCollection {
getItemIds(): Promise<string[]>
setItemOrder(ids: string[]): Promise<void>
getFields(): Promise<CollectionField[]>
setFields(fields: CollectionField[]): Promise<void>
addItems(items: CollectionItem): Promise<void>
removeItems(ids: string[]): Promise<void>
}Fields are used to defines the type of data that is added to the collection. You can configure up to 30 custom fields.
Each custom field consists of an id, name, and type. For the id it is best to use a unique identifier that stays the same in all future synchronizations. Any change in id can break data assignments on the canvas the user has made.
You can change the type of a field while reusing the existing id, Framer will take make sure that won't result in any errors. The maximum length for an id is 64 characters.
const titleId = "Shd4oMspa"
const descriptionId = "DmV2tOwlJ"
const contentId = "eDniSM8L9"
const avatarId = "Ur2HpfEB1"
const createdAtId = "vUvFzeUxy"
const vehicleId = "ZfN0uPuHc"
const brandId = "f6gl1d2gA"
const driversId = "H46Ahd8Ad"
const galleryId = "J51ah58Aj"
const galleryImageId = "Ur2HpfEB1"
const collection = await framer.getCollection()
await collection.setFields([
{
id: titleId,
name: "Title",
type: "string",
},
{
id: descriptionId,
name: "Description",
type: "string",
},
{
id: contentId,
name: "Content",
type: "string",
},
{
id: avatarId,
name: "Avatar",
type: "image",
},
{
id: createdAtId,
name: "Created At",
type: "date",
},
{
id: vehicleId,
name: "Vehicle",
type: "enum",
cases: [
{ id: "1", name: "Car" },
{ id: "2", name: "Boat" },
{ id: "3", name: "Plane" },
],
},
{
id: brandId,
name: "Brand",
type: "collectionReference",
collectionId: brandsCollectionId,
},
{
id: driversId,
name: "Drivers",
type: "multiCollectionReference",
collectionId: driversCollectionId
},
{
id: galleryId,
name: "Gallery",
type: "array",
fields: [
{
id: galleryImageId,
type: "image",
}
]
},
])
const titleId = "Shd4oMspa"
const descriptionId = "DmV2tOwlJ"
const contentId = "eDniSM8L9"
const avatarId = "Ur2HpfEB1"
const createdAtId = "vUvFzeUxy"
const vehicleId = "ZfN0uPuHc"
const brandId = "f6gl1d2gA"
const driversId = "H46Ahd8Ad"
const galleryId = "J51ah58Aj"
const galleryImageId = "Ur2HpfEB1"
const collection = await framer.getCollection()
await collection.setFields([
{
id: titleId,
name: "Title",
type: "string",
},
{
id: descriptionId,
name: "Description",
type: "string",
},
{
id: contentId,
name: "Content",
type: "string",
},
{
id: avatarId,
name: "Avatar",
type: "image",
},
{
id: createdAtId,
name: "Created At",
type: "date",
},
{
id: vehicleId,
name: "Vehicle",
type: "enum",
cases: [
{ id: "1", name: "Car" },
{ id: "2", name: "Boat" },
{ id: "3", name: "Plane" },
],
},
{
id: brandId,
name: "Brand",
type: "collectionReference",
collectionId: brandsCollectionId,
},
{
id: driversId,
name: "Drivers",
type: "multiCollectionReference",
collectionId: driversCollectionId
},
{
id: galleryId,
name: "Gallery",
type: "array",
fields: [
{
id: galleryImageId,
type: "image",
}
]
},
])
const titleId = "Shd4oMspa"
const descriptionId = "DmV2tOwlJ"
const contentId = "eDniSM8L9"
const avatarId = "Ur2HpfEB1"
const createdAtId = "vUvFzeUxy"
const vehicleId = "ZfN0uPuHc"
const brandId = "f6gl1d2gA"
const driversId = "H46Ahd8Ad"
const galleryId = "J51ah58Aj"
const galleryImageId = "Ur2HpfEB1"
const collection = await framer.getCollection()
await collection.setFields([
{
id: titleId,
name: "Title",
type: "string",
},
{
id: descriptionId,
name: "Description",
type: "string",
},
{
id: contentId,
name: "Content",
type: "string",
},
{
id: avatarId,
name: "Avatar",
type: "image",
},
{
id: createdAtId,
name: "Created At",
type: "date",
},
{
id: vehicleId,
name: "Vehicle",
type: "enum",
cases: [
{ id: "1", name: "Car" },
{ id: "2", name: "Boat" },
{ id: "3", name: "Plane" },
],
},
{
id: brandId,
name: "Brand",
type: "collectionReference",
collectionId: brandsCollectionId,
},
{
id: driversId,
name: "Drivers",
type: "multiCollectionReference",
collectionId: driversCollectionId
},
{
id: galleryId,
name: "Gallery",
type: "array",
fields: [
{
id: galleryImageId,
type: "image",
}
]
},
])By default, managed collection fields set by a plugin won't be editable by users using the CMS. However, there are cases where you might want a few fields edited by the user. To allow this, you can see the userEditable attribute.
In this example the description field can be edited by the user in the UI, the title cannot.
await collection.setFields([
{
id: titleId,
name: "Title",
type: "string",
},
{
id: descriptionId,
name: "Description",
type: "string",
userEditable: true
},
])await collection.setFields([
{
id: titleId,
name: "Title",
type: "string",
},
{
id: descriptionId,
name: "Description",
type: "string",
userEditable: true
},
])await collection.setFields([
{
id: titleId,
name: "Title",
type: "string",
},
{
id: descriptionId,
name: "Description",
type: "string",
userEditable: true
},
])Note that fields marked as userEditable can no longer have their values set by the plugin when using addItems. Trying to edit or add a value for an editable field will be ignored.
Fields of type collectionReference and multiCollectionReference allow you to create fields that point to items in another Collection.
In Plugins, items are referenced by slug—either a single slug for a collectionReference or an array of slugs for a multiCollectionReference.
For Managed Collections, where item IDs are controlled by the plugin, items are referenced by their ID. Note that for managed collections, you can only create references between Collections that are managed by the same Plugin.
Now that your fields are setup you can add items to the collection. Typically this will involve fetching data from an external resource and looping over the items to prepare them for the custom fields that were configured earlier.
Each item has a required id and slug. The data for the custom fields has to be added via fieldData. The id property for each custom field should be used as the key within the fieldData object.
The addItems method can be used for both adding new items as well as updating existing ones.
import { framer, CollectionItem } from 'framer-plugin'
const collection = await framer.getCollection()
const fields = await collection.getFields()
const contentField: FormattedTextField = fields[0]
const blogPosts = await fetchBlogPosts()
const collectionItems: CollectionItem[] = []
for (const post of blogPosts) {
collectionItems.push({
id: post.id,
slug: slugify(post.title),
fieldData: {
[titleId]: { value: post.title },
[contentField.id]: { value: post.content },
}
})
}
await collection.addItems(collectionItems)import { framer, CollectionItem } from 'framer-plugin'
const collection = await framer.getCollection()
const fields = await collection.getFields()
const contentField: FormattedTextField = fields[0]
const blogPosts = await fetchBlogPosts()
const collectionItems: CollectionItem[] = []
for (const post of blogPosts) {
collectionItems.push({
id: post.id,
slug: slugify(post.title),
fieldData: {
[titleId]: { value: post.title },
[contentField.id]: { value: post.content },
}
})
}
await collection.addItems(collectionItems)import { framer, CollectionItem } from 'framer-plugin'
const collection = await framer.getCollection()
const fields = await collection.getFields()
const contentField: FormattedTextField = fields[0]
const blogPosts = await fetchBlogPosts()
const collectionItems: CollectionItem[] = []
for (const post of blogPosts) {
collectionItems.push({
id: post.id,
slug: slugify(post.title),
fieldData: {
[titleId]: { value: post.title },
[contentField.id]: { value: post.content },
}
})
}
await collection.addItems(collectionItems)We have recently added support for our "Gallery" fields. This comes in the form of a new field type of "array", which is due to Galleries being implemented in a way that is future-proof with arbitrary arrays of nested fields. At the moment, fields of type "array" must contain a single image field.
const collection = await framer.getActiveCollection()
const fields = await collection.getFields()
const galleryField = fields.find((field) => field.type === "array")
const galleryImageField = galleryField.fields[0]
await collection.addItems([
{
id: post.id,
slug: slugify(post.title),
fieldData: {
[galleryField.id]: {
type: "array",
value: post.images.map(image => ({
fieldData: {
[galleryImageField.id]: {
type: "image",
value: image.url,
},
},
})),
},
},
},
])
const collection = await framer.getActiveCollection()
const fields = await collection.getFields()
const galleryField = fields.find((field) => field.type === "array")
const galleryImageField = galleryField.fields[0]
await collection.addItems([
{
id: post.id,
slug: slugify(post.title),
fieldData: {
[galleryField.id]: {
type: "array",
value: post.images.map(image => ({
fieldData: {
[galleryImageField.id]: {
type: "image",
value: image.url,
},
},
})),
},
},
},
])
const collection = await framer.getActiveCollection()
const fields = await collection.getFields()
const galleryField = fields.find((field) => field.type === "array")
const galleryImageField = galleryField.fields[0]
await collection.addItems([
{
id: post.id,
slug: slugify(post.title),
fieldData: {
[galleryField.id]: {
type: "array",
value: post.images.map(image => ({
fieldData: {
[galleryImageField.id]: {
type: "image",
value: image.url,
},
},
})),
},
},
},
])
Sometimes you want to remove data from the Managed Collection. For example if certain data is no longer present in the external data source.
const itemsIds = await collection.getItemIds()
const unseenItemIds = new Set(itemsIds)
const blogPosts = await fetchBlogPosts()
for (const post of blogPosts) {
unseenItemIds.delete(post.id)
}
const itemsToRemove = Array.from(unseenItemIds)
await collection.removeItems(itemsToRemove)const itemsIds = await collection.getItemIds()
const unseenItemIds = new Set(itemsIds)
const blogPosts = await fetchBlogPosts()
for (const post of blogPosts) {
unseenItemIds.delete(post.id)
}
const itemsToRemove = Array.from(unseenItemIds)
await collection.removeItems(itemsToRemove)const itemsIds = await collection.getItemIds()
const unseenItemIds = new Set(itemsIds)
const blogPosts = await fetchBlogPosts()
for (const post of blogPosts) {
unseenItemIds.delete(post.id)
}
const itemsToRemove = Array.from(unseenItemIds)
await collection.removeItems(itemsToRemove)Similar to local storage, you can store custom data on the Managed Collection. By storing the last synchronization date, you might be able for skip some of the expensive synchronization work. But you can also store other data like the specific notion database the collection is connected to. See also the guide for Plugin Data.
const lastSyncedAtStorageKey = "lastSynchronizedAt"
const currentDate = new Date().toISOString()
await collection.setPluginData(lastSyncedAtStorageKey, currentDate)
const lastSynchronized = await collection.getPluginData(lastSyncedAtStorageKey)
if (lastSynchronized && post.lastUpdatedAt <= lastSynchronized) {
continue
}const lastSyncedAtStorageKey = "lastSynchronizedAt"
const currentDate = new Date().toISOString()
await collection.setPluginData(lastSyncedAtStorageKey, currentDate)
const lastSynchronized = await collection.getPluginData(lastSyncedAtStorageKey)
if (lastSynchronized && post.lastUpdatedAt <= lastSynchronized) {
continue
}const lastSyncedAtStorageKey = "lastSynchronizedAt"
const currentDate = new Date().toISOString()
await collection.setPluginData(lastSyncedAtStorageKey, currentDate)
const lastSynchronized = await collection.getPluginData(lastSyncedAtStorageKey)
if (lastSynchronized && post.lastUpdatedAt <= lastSynchronized) {
continue
}