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:
Any kind of Collection can be read from. Collection refers to Unmanaged Collections that are created by Users. Use the collectionMode to access CMS data from your plugin.
// Get the current collection the user is viewing in the CMS.constcollection = awaitframer.getActiveCollection()
// Get the current collection the user is viewing in the CMS.constcollection = awaitframer.getActiveCollection()
// Get the current collection the user is viewing in the CMS.constcollection = awaitframer.getActiveCollection()
You can get a list of all the available collections.
awaitframer.getCollections()
awaitframer.getCollections()
awaitframer.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.
awaitframer.getFields()
awaitframer.getFields()
awaitframer.getFields()
This will return an array of objects.
// An example snippet from `getFields`.[// ...{id:"BnNuS2i3o",type:"string",name:"Title",},{id:"hgNaskh4n",type:"boolean",name:"Featured",},{id:"JO9uOAhun",type:"date",name:"Published Date",},//...}
// An example snippet from `getFields`.[// ...{id:"BnNuS2i3o",type:"string",name:"Title",},{id:"hgNaskh4n",type:"boolean",name:"Featured",},{id:"JO9uOAhun",type:"date",name:"Published Date",},//...}
// An example snippet from `getFields`.[// ...{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.
awaitframer.getItems()
awaitframer.getItems()
awaitframer.getItems()
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.
// An example snippet from `getItems`.[//...{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"}}//...]
// An example snippet from `getItems`.[//...{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"}}//...]
// An example snippet from `getItems`.[//...{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.
// Standard JavaScript type checks can be used for built-in types.consttitleField = fieldData["XTM8FSHGs"]if(typeoftitleField === "string) {
console.log(titleField.toUpperCase())}// Use helpers provided by framer-plugin to check for images and files.constassetField = fieldData["AaN8oZOpy"]if(isImageAsset(assetField) || isFileAsset(assetField){console.log(assetField.url)}
// Standard JavaScript type checks can be used for built-in types.consttitleField = fieldData["XTM8FSHGs"]if(typeoftitleField === "string) {
console.log(titleField.toUpperCase())}// Use helpers provided by framer-plugin to check for images and files.constassetField = fieldData["AaN8oZOpy"]if(isImageAsset(assetField) || isFileAsset(assetField){console.log(assetField.url)}
// Standard JavaScript type checks can be used for built-in types.consttitleField = fieldData["XTM8FSHGs"]if(typeoftitleField === "string) {
console.log(titleField.toUpperCase())}// Use helpers provided by framer-plugin to check for images and files.constassetField = fieldData["AaN8oZOpy"]if(isImageAsset(assetField) || isFileAsset(assetField){console.log(assetField.url)}
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.
When your plugin is launched in either configureManagedCollection or syncManagedCollection mode, it can get the active collection via the framer.getManagedCollection().
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.
// By using ids, fields can be renamed without breaking// existing data assignments on the canvas.consttitleId = "Shd4oMspa"constdescriptionId = "DmV2tOwlJ"constcontentId = "eDniSM8L9"constavatarId = "Ur2HpfEB1"constcreatedAtId = "vUvFzeUxy"constvehicleId = "ZfN0uPuHc"constbrandId = "f6gl1d2gA"constdriversId = "H46Ahd8Ad"constgalleryId = "J51ah58Aj"constgalleryImageId = "Ur2HpfEB1"constcollection = awaitframer.getCollection()// Here we include all of the different field types you can use.awaitcollection.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},// Beta{id:galleryId,name:"Gallery",type:"array",fields:[{id:galleryImageId,type:"image",}]},])
// By using ids, fields can be renamed without breaking// existing data assignments on the canvas.consttitleId = "Shd4oMspa"constdescriptionId = "DmV2tOwlJ"constcontentId = "eDniSM8L9"constavatarId = "Ur2HpfEB1"constcreatedAtId = "vUvFzeUxy"constvehicleId = "ZfN0uPuHc"constbrandId = "f6gl1d2gA"constdriversId = "H46Ahd8Ad"constgalleryId = "J51ah58Aj"constgalleryImageId = "Ur2HpfEB1"constcollection = awaitframer.getCollection()// Here we include all of the different field types you can use.awaitcollection.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},// Beta{id:galleryId,name:"Gallery",type:"array",fields:[{id:galleryImageId,type:"image",}]},])
// By using ids, fields can be renamed without breaking// existing data assignments on the canvas.consttitleId = "Shd4oMspa"constdescriptionId = "DmV2tOwlJ"constcontentId = "eDniSM8L9"constavatarId = "Ur2HpfEB1"constcreatedAtId = "vUvFzeUxy"constvehicleId = "ZfN0uPuHc"constbrandId = "f6gl1d2gA"constdriversId = "H46Ahd8Ad"constgalleryId = "J51ah58Aj"constgalleryImageId = "Ur2HpfEB1"constcollection = awaitframer.getCollection()// Here we include all of the different field types you can use.awaitcollection.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},// Beta{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.
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'constcollection = awaitframer.getCollection()constfields = awaitcollection.getFields()constcontentField: FormattedTextField = fields[0]constblogPosts = awaitfetchBlogPosts()constcollectionItems: CollectionItem[] = []for(constpostofblogPosts){collectionItems.push({id:post.id,slug:slugify(post.title),fieldData:{[titleId]:{value:post.title},// The key here must match the "id" value of the field// set in "collection.setFields". Because this field was// configured as "formattedText" it supports (basic) HTML.[contentField.id]:{value:post.content},}})}awaitcollection.addItems(collectionItems)
import{framer,CollectionItem}from'framer-plugin'constcollection = awaitframer.getCollection()constfields = awaitcollection.getFields()constcontentField: FormattedTextField = fields[0]constblogPosts = awaitfetchBlogPosts()constcollectionItems: CollectionItem[] = []for(constpostofblogPosts){collectionItems.push({id:post.id,slug:slugify(post.title),fieldData:{[titleId]:{value:post.title},// The key here must match the "id" value of the field// set in "collection.setFields". Because this field was// configured as "formattedText" it supports (basic) HTML.[contentField.id]:{value:post.content},}})}awaitcollection.addItems(collectionItems)
import{framer,CollectionItem}from'framer-plugin'constcollection = awaitframer.getCollection()constfields = awaitcollection.getFields()constcontentField: FormattedTextField = fields[0]constblogPosts = awaitfetchBlogPosts()constcollectionItems: CollectionItem[] = []for(constpostofblogPosts){collectionItems.push({id:post.id,slug:slugify(post.title),fieldData:{[titleId]:{value:post.title},// The key here must match the "id" value of the field// set in "collection.setFields". Because this field was// configured as "formattedText" it supports (basic) HTML.[contentField.id]:{value:post.content},}})}awaitcollection.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.
Sometimes you want to remove data from the Managed Collection. For example if certain data is no longer present in the external data source.
constitemsIds = awaitcollection.getItemIds()constunseenItemIds = newSet(itemsIds)constblogPosts = awaitfetchBlogPosts()for(constpostofblogPosts){// Mark the item as seen.unseenItemIds.delete(post.id)}// Remove all items that were not seen.constitemsToRemove = Array.from(unseenItemIds)awaitcollection.removeItems(itemsToRemove)
constitemsIds = awaitcollection.getItemIds()constunseenItemIds = newSet(itemsIds)constblogPosts = awaitfetchBlogPosts()for(constpostofblogPosts){// Mark the item as seen.unseenItemIds.delete(post.id)}// Remove all items that were not seen.constitemsToRemove = Array.from(unseenItemIds)awaitcollection.removeItems(itemsToRemove)
constitemsIds = awaitcollection.getItemIds()constunseenItemIds = newSet(itemsIds)constblogPosts = awaitfetchBlogPosts()for(constpostofblogPosts){// Mark the item as seen.unseenItemIds.delete(post.id)}// Remove all items that were not seen.constitemsToRemove = Array.from(unseenItemIds)awaitcollection.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.
constlastSyncedAtStorageKey = "lastSynchronizedAt"// Store a timestamp after a successful synchronizion.constcurrentDate = newDate().toISOString()awaitcollection.setPluginData(lastSyncedAtStorageKey,currentDate)// Get the last synchronization date when you start synchronizing.constlastSynchronized = awaitcollection.getPluginData(lastSyncedAtStorageKey)// Skip items that were last updated before the previous synchronization.if(lastSynchronized && post.lastUpdatedAt <= lastSynchronized){continue}
constlastSyncedAtStorageKey = "lastSynchronizedAt"// Store a timestamp after a successful synchronizion.constcurrentDate = newDate().toISOString()awaitcollection.setPluginData(lastSyncedAtStorageKey,currentDate)// Get the last synchronization date when you start synchronizing.constlastSynchronized = awaitcollection.getPluginData(lastSyncedAtStorageKey)// Skip items that were last updated before the previous synchronization.if(lastSynchronized && post.lastUpdatedAt <= lastSynchronized){continue}
constlastSyncedAtStorageKey = "lastSynchronizedAt"// Store a timestamp after a successful synchronizion.constcurrentDate = newDate().toISOString()awaitcollection.setPluginData(lastSyncedAtStorageKey,currentDate)// Get the last synchronization date when you start synchronizing.constlastSynchronized = awaitcollection.getPluginData(lastSyncedAtStorageKey)// Skip items that were last updated before the previous synchronization.if(lastSynchronized && post.lastUpdatedAt <= lastSynchronized){continue}