Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
EXAMPLE_PROJECT_URL=https://framer.com/projects/Sites--aabbccddeeff
FRAMER_API_KEY=12345678-1234-1234-1234-123456789
12 changes: 12 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@ jobs:
- run: npm ci
- run: npm run check

typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

- run: npm ci
- run: npm run typecheck
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Framer Server API Examples

This repository contains examples for the Framer Server API. Each example is a standalone project that can be run independently.

## How to run examples

You need to obtain a Framer project URL and API key. You can get the API key from the Framer project settings and find the project URL in the browser URL bar.

Then, you need to set the `EXAMPLE_PROJECT_URL` and `FRAMER_API_KEY` environment variables.

## How to connect and get a framer client

```ts
const projectUrl = "https://framer.com/projects/Sites--aabbccddeeff";

const framer = await connect(projectUrl, apiKey);
// ... your code here ...
await framer.disconnect();
```

Starting with Node.js v24, you can use the `using` keyword to ensure that the Framer client is closed after the block is executed.

```ts
using framer = await connect(projectUrl, apiKey);

// ... your code here ...
// The disconnect is automatically called when the block is exited.
```

You can also use the environment variable `FRAMER_API_KEY` to set the API key and omit the API key parameter.

```ts
using framer = await connect(projectUrl);
```
77 changes: 40 additions & 37 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"indentStyle": "space",
"indentWidth": 4,
"lineWidth": 120,
"lineEnding": "lf",
"attributePosition": "auto",
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"indentStyle": "space",
"indentWidth": 4,
"lineWidth": 120,
"lineEnding": "lf",
"attributePosition": "auto",
"bracketSpacing": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"useLiteralKeys": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
13 changes: 13 additions & 0 deletions examples/csv-importer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# CSV Importer

This example shows how to import a CSV file into a Framer collection.

How to use:

```bash
node --env-file=../../.env src/csv-to-collection.ts

bun run src/csv-to-collection.ts

deno run src/csv-to-collection.ts
```
9 changes: 9 additions & 0 deletions examples/csv-importer/data/sample-products.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
slug,title,description,price,inStock,category
wireless-mouse,Wireless Mouse,Ergonomic wireless mouse with precision tracking,29.99,true,Electronics
mechanical-keyboard,Mechanical Keyboard,RGB mechanical keyboard with cherry switches,89.99,true,Electronics
usb-c-cable,USB-C Cable,Fast charging USB-C cable 6ft length,12.99,false,Accessories
monitor-stand,Monitor Stand,Adjustable aluminum monitor stand,49.99,true,Furniture
desk-lamp,LED Desk Lamp,Dimmable LED lamp with USB charging port,34.99,true,Lighting
webcam-hd,HD Webcam,1080p webcam with built-in microphone,59.99,true,Electronics
mouse-pad,Large Mouse Pad,Extended gaming mouse pad with stitched edges,19.99,true,Accessories
headphone-stand,Headphone Stand,Wooden headphone stand with cable holder,24.99,false,Furniture
18 changes: 18 additions & 0 deletions examples/csv-importer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "csv-importer",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"framer-api": "^0.0.1-alpha.6",
"papaparse": "^5.5.3",
"typescript": "^5.9.3"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/papaparse": "^5.3.15"
}
}
92 changes: 92 additions & 0 deletions examples/csv-importer/src/csv-to-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import assert from "node:assert";
import path from "node:path";
import { type CreateField, connect, type FieldDataEntryInput, type FieldDataInput } from "framer-api";
import { type FieldType, loadCsv } from "./load-csv.ts";

// Configuration

const projectUrl = process.env["EXAMPLE_PROJECT_URL"];
assert(projectUrl, "EXAMPLE_PROJECT_URL environment variable is required");

const csvPath = process.env["CSV_PATH"] ?? path.join(import.meta.dirname, "../data/sample-products.csv");
const collectionName = process.env["COLLECTION_NAME"] ?? "Products";

const { columns, rows, fieldTypes } = loadCsv(csvPath);

assert(columns.includes("slug"), "CSV must contain a 'slug' column");

// The `using` keyword is used to ensure that the Framer client is closed after the block is executed.
// If you don't use the `using` keyword, you need to manually close the client using `await framer.disconnect()`.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using
using framer = await connect(projectUrl);

// Find or Create Collection

const existingCollections = await framer.getCollections();
let collection = existingCollections.find((c) => c.name === collectionName);

if (!collection) {
collection = await framer.createCollection(collectionName);
}

// Add Missing Fields

const existingFields = await collection.getFields();
const existingFieldNames = new Set(existingFields.map((f) => f.name.toLowerCase()));

const fieldsToCreate = columns
.filter((column) => column !== "slug" && !existingFieldNames.has(column.toLowerCase()))
.map(
(column): CreateField => ({
type: fieldTypes.get(column) ?? "string",
name: column,
}),
);

if (fieldsToCreate.length > 0) {
await collection.addFields(fieldsToCreate);
}

// Build Items & Import

const fields = await collection.getFields();
const fieldNameToId = new Map(fields.map((f) => [f.name.toLowerCase(), f.id]));

const existingItems = await collection.getItems();
const slugToExistingId = new Map(existingItems.map((item) => [item.slug, item.id]));

const items = rows.map((row) => {
const fieldData: FieldDataInput = {};

for (const column of columns) {
if (column === "slug") continue;

const fieldId = fieldNameToId.get(column.toLowerCase());
if (!fieldId) continue;

const value = row[column] ?? "";
const fieldType = fieldTypes.get(column) ?? "string";
fieldData[fieldId] = toFieldData(value, fieldType);
}

const slug = row["slug"];
assert(slug && slug.length > 0, "slug is required and must be non-empty");
const existingId = slugToExistingId.get(slug);

return { id: existingId, slug, fieldData };
});

await collection.addItems(items);

console.log(`Imported ${items.length} items`);

function toFieldData(value: string, type: FieldType): FieldDataEntryInput {
switch (type) {
case "boolean":
return { type: "boolean" as const, value: value.toLowerCase() === "true" };
case "number":
return { type: "number" as const, value: parseFloat(value) || 0 };
case "string":
return { type: "string" as const, value };
}
}
52 changes: 52 additions & 0 deletions examples/csv-importer/src/load-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { readFileSync } from "node:fs";
import Papa from "papaparse";

export type FieldType = "string" | "number" | "boolean";

export interface CsvData {
columns: string[];
rows: Record<string, string>[];
fieldTypes: Map<string, FieldType>;
}

export function loadCsv(path: string): CsvData {
const csvContent = readFileSync(path, "utf-8");
const { data: rows, meta } = Papa.parse<Record<string, string>>(csvContent, {
header: true,
skipEmptyLines: true,
transformHeader: (header: string) => header.trim(),
transform: (value: string) => value.trim(),
});

if (!meta.fields) {
throw new Error("CSV file has no header row");
}

const fieldTypes = new Map(inferFieldTypes(rows, meta.fields));

return { columns: meta.fields, rows, fieldTypes };
}

function inferFieldType(values: string[]): FieldType {
const nonEmptyValues = values.filter((v) => v !== "");
if (nonEmptyValues.length === 0) return "string";

const allBooleans = nonEmptyValues.every((v) => v === "true" || v === "false");
if (allBooleans) return "boolean";

const allNumbers = nonEmptyValues.every((v) => !Number.isNaN(parseFloat(v)) && Number.isFinite(Number(v)));
if (allNumbers) return "number";

return "string";
}

/**
* Infer the field types from the data in the CSV file.
* Returns the column name and the inferred field type.
*/
function inferFieldTypes(rows: Record<string, string>[], columns: string[]): [string, FieldType][] {
return columns.map((column) => {
const values = rows.map((row) => row[column] ?? "");
return [column, inferFieldType(values)];
});
}
4 changes: 4 additions & 0 deletions examples/csv-importer/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["**/*.ts"]
}
Loading