Framer CMS Integration

Framer CMS Integration

Framer CMS Integration

Connect your Framer CMS to Wrodium using a custom plugin for AI-powered content optimization.

Created date:

Dec 5, 2025

Updated date:

Dec 11, 2025

How Framer CMS Works

Unlike WordPress, Webflow, or Ghost, Framer CMS doesn't expose a public HTTP REST API. Instead, Framer content is managed through the Framer Plugin API, which runs inside the Framer editor.

This means Wrodium cannot directly pull or push content to Framer CMS. Instead, you'll install a Framer plugin that bridges Wrodium and your Framer CMS.

Architecture

Setup Steps

Step 1: Install the Wrodium Framer Plugin

  1. Open your Framer project

  2. Go to Plugins in the left sidebar

  3. Search for "Wrodium" or import from URL

  4. Click Install

Note: If the plugin isn't published yet, see "Building the Plugin" below.

Step 2: Configure the Plugin

  1. Open the Wrodium plugin in Framer

  2. Enter your Wrodium Brand URL (e.g., yourbrand.wrodium.com)

  3. Enter your Wrodium API Key (from Settings → API)

  4. Select the CMS Collection to connect

  5. Click Save Configuration

Step 3: Sync Content

With the plugin configured:

  1. Click Pull from CMS to load articles into Wrodium

  2. Articles appear in your Wrodium Maintenance tab

  3. Optimize content using Wrodium's AI tools

  4. Click Push to CMS in the plugin to sync optimized content back

Building the Wrodium Framer Plugin

If you need to build or customize the plugin:

Prerequisites

  • Node.js 18+

  • Framer Plugin CLI

Create the Plugin

npm create framer-plugin@latest -- --starter cms
cd

Plugin Code

Create src/index.tsx:

// src/index.tsx
import { framer, CollectionItem } from "framer-plugin";

// Field IDs for your CMS collection
const TITLE_FIELD_ID = "titleField";
const CONTENT_FIELD_ID = "contentField";
const EXCERPT_FIELD_ID = "excerptField";
const PUBLISHED_AT_FIELD_ID = "publishedAtField";

/**
 * Configure fields for a Managed Collection.
 */
async function configureManagedCollection() {
  const collection = await framer.getManagedCollection();

  await collection.setFields([
    {
      id: TITLE_FIELD_ID,
      name: "Title",
      type: "string",
    },
    {
      id: CONTENT_FIELD_ID,
      name: "Content",
      type: "formattedText",
    },
    {
      id: EXCERPT_FIELD_ID,
      name: "Excerpt",
      type: "string",
      userEditable: true,
    },
    {
      id: PUBLISHED_AT_FIELD_ID,
      name: "Published At",
      type: "date",
    },
  ]);
}

/**
 * Sync optimized content from Wrodium into Framer CMS.
 */
async function syncManagedCollection() {
  const collection = await framer.getManagedCollection();

  // Get brand URL from plugin settings
  const brandUrl = await collection.getPluginData("brandUrl");
  const apiKey = await collection.getPluginData("apiKey");

  if (!brandUrl || !apiKey) {
    console.error("Missing Wrodium configuration");
    return;
  }

  // Fetch optimized articles from Wrodium
  const response = await fetch(
    `https://api.wrodium.com/framer/sync`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${apiKey}`,
      },
      body: JSON.stringify({ brandUrl }),
    }
  );

  if (!response.ok) {
    console.error("Wrodium sync failed", await response.text());
    return;
  }

  type WrodiumArticle = {
    id: string;
    slug: string;
    title: string;
    content_html: string;
    excerpt: string;
    published_at?: string;
  };

  const articles: WrodiumArticle[] = await response.json();

  // Transform to Framer CollectionItems
  const items: CollectionItem[] = articles.map((article) => ({
    id: article.id,
    slug: article.slug,
    fieldData: {
      [TITLE_FIELD_ID]: { value: article.title },
      [CONTENT_FIELD_ID]: { value: article.content_html },
      [EXCERPT_FIELD_ID]: { value: article.excerpt },
      [PUBLISHED_AT_FIELD_ID]: { 
        value: article.published_at ?? new Date().toISOString() 
      },
    },
  }));

  // Add/update items in the collection
  await collection.addItems(items);
}

/**
 * Plugin entry point
 */
export default async function run() {
  const mode = framer.mode;

  if (mode === "configureManagedCollection") {
    await configureManagedCollection();
    framer.showUI(); // Show config UI
  } else if (mode === "syncManagedCollection") {
    await syncManagedCollection();
    await framer.closePlugin();
  } else {
    await framer.closePlugin();
  }
}

Plugin UI

Create src/App.tsx for the configuration interface:

// src/App.tsx
import { useState, useEffect } from "react";
import { framer } from "framer-plugin";

export function App() {
  const [brandUrl, setBrandUrl] = useState("");
  const [apiKey, setApiKey] = useState("");
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    // Load existing configuration
    async function loadConfig() {
      const collection = await framer.getManagedCollection();
      const savedBrandUrl = await collection.getPluginData("brandUrl");
      const savedApiKey = await collection.getPluginData("apiKey");
      if (savedBrandUrl) setBrandUrl(savedBrandUrl);
      if (savedApiKey) setApiKey(savedApiKey);
    }
    loadConfig();
  }, []);

  async function saveConfig() {
    setSaving(true);
    try {
      const collection = await framer.getManagedCollection();
      await collection.setPluginData("brandUrl", brandUrl);
      await collection.setPluginData("apiKey", apiKey);
      alert("Configuration saved!");
    } catch (err) {
      alert("Failed to save: " + err);
    }
    setSaving(false);
  }

  return (
    <div style={{ padding: 16 }}>
      <h2>Wrodium Configuration</h2>
      
      <label>
        Brand URL
        <input
          type="text"
          value={brandUrl}
          onChange={(e) => setBrandUrl(e.target.value)}
          placeholder="yourbrand"
        />
      </label>

      <label>
        API Key
        <input
          type="password"
          value={apiKey}
          onChange={(e) => setApiKey(e.target.value)}
          placeholder="wrod_..."
        />
      </label>

      <button onClick={saveConfig} disabled={saving}>
        {saving ? "Saving..." : "Save Configuration"}
      </button>
    </div>
  );
}

Build and Deploy

# Build the plugin
npm run build

# Publish to Framer
npm

Wrodium Backend Endpoint

Wrodium provides a /framer/sync endpoint for plugin communication:

# backend/routers/framer.py
from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel
from typing import List

router = APIRouter(prefix="/framer", tags=["framer"])

class FramerSyncRequest(BaseModel):
    brandUrl: str

class FramerArticle(BaseModel):
    id: str
    slug: str
    title: str
    content_html: str
    excerpt: str
    published_at: str | None = None

@router.post("/sync", response_model=List[FramerArticle])
async def framer_sync(
    request: FramerSyncRequest,
    authorization: str = Header(...),
):
    """
    Called by the Framer plugin to fetch optimized articles.
    """
    # Validate API key
    if not authorization.startswith("Bearer "):
        raise HTTPException(401, "Invalid authorization header")
    
    api_key = authorization.replace("Bearer ", "")
    # Verify api_key belongs to brand_url...
    
    # Fetch optimized articles for this brand
    # Return articles ready for Framer CMS
    articles = await get_optimized_articles_for_brand(request.brandUrl)
    
    return [
        FramerArticle(
            id=a.id,
            slug=a.slug,
            title=a.title,
            content_html=a.optimized_content,
            excerpt=a.excerpt,
            published_at=a.published_at.isoformat() if a.published_at else None,
        )
        for a in articles
    ]

Workflow

Pull Content to Wrodium

  1. In Framer, open the Wrodium plugin

  2. Click Push to Wrodium

  3. Plugin sends current CMS items to Wrodium

  4. View items in Wrodium's Maintenance tab

Push Optimized Content to Framer

  1. In Wrodium, optimize your articles

  2. In Framer, click Sync on the CMS collection

  3. Plugin pulls optimized content from Wrodium

  4. Framer CMS updates with new content

Limitations

  • No real-time sync: Requires manual trigger in Framer

  • Plugin must be open: Sync only works when Framer editor is active

  • Field mapping: Field IDs must match between plugin and CMS

Alternative: Export/Import

If the plugin approach doesn't fit your workflow:

  1. Export content from Framer CMS as JSON

  2. Upload to Wrodium for optimization

  3. Download optimized JSON from Wrodium

  4. Import back into Framer CMS

Troubleshooting

Plugin Not Loading

  • Ensure plugin is installed in your Framer project

  • Check browser console for JavaScript errors

Sync Fails

  • Verify API key is correct

  • Check network connectivity to Wrodium API

  • Ensure brand URL matches your Wrodium account

Content Not Updating

  • Framer may cache CMS content

  • Republish your Framer site after sync

  • Check that field IDs match in plugin configuration

Next Steps

Found this article insightful? Spread the words on…

Found this article insightful?
Spread the words on…

X.com

Share on X

X.com

Share on X

X.com

Share on X

X.com

Share on LinkedIn

X.com

Share on LinkedIn

X.com

Share on LinkedIn

Found this documentation insightful? Share it on…

X.com

LinkedIn

Contents

Checkout other documentations

Checkout other documentations

Checkout other documentations

Let us help you win on

ChatGPT

Let us help you win on

ChatGPT

Let us help you win on

ChatGPT