When it comes to the implementation of open web standards, there are few (if any) that cause a rush of inspiration and has developers flocking to their computers. Of course, the exception would be Frames by Farcaster. In case you missed it, Frames are small windows that look like social media posts in your feed, but in reality are fully functional apps. Within just a few days of their release by the Farcaster team, there was a huge increase in sign-ups for the platform and dozens of mini hackathons with over $30,000 up for grabs. In truth, we are only seeing the beginning of what is possible and how these small little frames could change the way we interact with our social media platforms. In this post, we’ll show you the basics and how to build one yourself!
What are Frames?
Before we dig into the technical side of Frames, it might be helpful to give some context as to what they are and how they work. Frames are supported by the Open Graph standard; if that doesn’t sound familiar thats ok, you actually see and use them all the time. When you share a link on a social media platform, you’re going to get a fun preview that looks something like this:
These are also known as “rich previews” and can help seal the deal for a click when sharing links. They’re built using HTML meta tags, which have information about the website link, such as the URL, title, description, and a preview image. Frames use this same standard, but add in extra meta tags for buttons and an endpoint URL that a POST request would be sent to. While using Farcaster, these POST requests have information about the user and signature data for verifying their identity. The possibilities with this are endless, from ordering cookies, chatting with emojis, even playing DOOM.
Building a Frame
Enough talk, let’s build one! To do this, you will need some general programming experience and the following tools:
The frame we’re going to build is really simple. Kind of like a comic / story book / ad for Cosmic Cowboys:
Before you jump into the code, you’ll need some content to go in the the frame. There’s lots of ways to go about this - including things like Satori - and one of them is to upload the content to IPFS. Luckily, Pinata makes that easy! Just sign up for a free account and upload your files. I structured my content in a folder, and then uploaded the folder so that I can iterate through the links, like so:
https://mktg.mypinata.cloud/ipfs/Qme4FXhoxHHfyzTfRxSpASbMF8kajLEPkRQWhwWu9pkUjm/1.png
After you upload your files, you will want to save your Dedicated Gateway domain too, as we’ll use it in an environment variable later. It should look something like this: <span class="code-inline">https://mtkg.mypinata.cloud</span>
There are lots of ways you could build a frame, as it just needs to return HTML meta tags, but for this tutorial we’ll use one of the most popular frameworks - Next.js. To start, open up your terminal and run the following:
npx create-next-app@latest frame-tutorial
It will ask some questions based on your preferences, so if you’re familiar with using Next.js, you’ll probably know what to do. For this tutorial, we’ll use all the defaults, which includes Typescript, Tailwindcss, and the App router. After it’s done creating the repo, we will want to change directories into it and install the dependencies.
cd frame-tutorial && npm install && npm install @coinbase/onchainkit
After that is done, we can go ahead and open the project in our code editor. Let’s take a look at the default template structure:
.
├── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── README.md
├── tailwind.config.ts
└── tsconfig.json
Next, you’ll want to add an .env.local to the root of your folder, and add the following variables:
NEXT_PUBLIC_GATEWAY_URL=YOUR_DEDICATED_GATEWAY
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Our <span class="code-inline">NEXT_PUBLIC_BASE_URL</span> will just be set to our localhost until we upload to Vercel, at which point we’ll use whatever domain it assigns us. The <span class="code-inline">NEXT_PUBLIC_GATEWAY_URL</span> the Dedicated Gateway we grabbed from our account earlier, and it should be in the format of <span class="code-inline">https://mktg.mypinata.cloud</span>
Now, let’s go into the <span class="code-inline">app/page.tsx</span>, clear out all the boiler plate, and replace it with the following.
import { getFrameMetadata } from '@coinbase/onchainkit';
import type { Metadata } from 'next';
const frameMetadata = getFrameMetadata({
buttons: [
{
label: "Begin"
}
],
image: `${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/Qme4FXhoxHHfyzTfRxSpASbMF8kajLEPkRQWhwWu9pkUjm/0.png`,
post_url: `${process.env.NEXT_PUBLIC_BASE_URL}/api/frame?id=1`,
});
export const metadata: Metadata = {
title: 'Cosmic Cowboys',
description: 'A frame telling the story of Cosmic Cowboys',
openGraph: {
title: 'Cosmic Cowboys',
description: 'A frame telling the story of Cosmic Cowboys',
images: [`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/Qme4FXhoxHHfyzTfRxSpASbMF8kajLEPkRQWhwWu9pkUjm/0.png`],
},
other: {
...frameMetadata,
},
};
export default function Page() {
return (
<>
<h1>Cosmic Cowboys</h1>
</>
);
}
app/page.tsx
In this file, we’re setting up a few things. To start, we’re importing some dependencies from <span class="code-inline">onchainkit</span> to allow us to export some metadata tags. In the <span class="code-inline">frameMetadata</span> we provide the info that Farcaster will be looking for to be able tell if it’s a frame. It takes in our buttons with labels, image, and a <span class="code-inline">post_url</span> which will be an API endpoint taking us to the next page of our story book. We also have regular metadata, including an <span class="code-inline">openGraph</span> property, which will give us a nice rich preview if we share the link outside of Farcaster. Finally, we have a simple render of the home page with just a title. No one will really see this, but we could give some instructions like “see this on Farcaster,” just in case someone uses it wrong.
We can also replace the <span class="code-inline">layout.tsx</span> boilerplate with some simple info:
export const viewport = {
width: 'device-width',
initialScale: 1.0,
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
app/layout.tsx
Now, we need to build the <span class="code-inline">api</span> endpoint that we’re making a post request to. Create a folder called <span class="code-inline">api</span> inside the <span class="code-inline">app</span> directory, and then another folder called frame with a <span class="code-inline">route.ts</span> file inside the API directory. Our project structure should now look like this:
.
├── app
│ ├── api
│ │ └── frame
│ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── README.md
├── tailwind.config.ts
└── tsconfig.json
Inside that <span class="code-inline">route.ts</span>, we’ll put in the following code for our API endpoint:
import { NextRequest, NextResponse } from 'next/server';
async function getResponse(req: NextRequest): Promise<NextResponse> {
const searchParams = req.nextUrl.searchParams
const id:any = searchParams.get("id")
const idAsNumber = parseInt(id)
const nextId = idAsNumber + 1
if(idAsNumber === 7){
return new NextResponse(`<!DOCTYPE html><html><head>
<title>This is frame 7</title>
<meta property="fc:frame" content="vNext" />
<meta property="fc:frame:image" content="${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/Qme4FXhoxHHfyzTfRxSpASbMF8kajLEPkRQWhwWu9pkUjm/7.png" />
<meta property="fc:frame:button:1" content="Visit CosmicCowboys.cloud" />
<meta property="fc:frame:button:1:action" content="post_redirect" />
<meta property="fc:frame:button:2" content="Learn How this was made" />
<meta property="fc:frame:button:2:action" content="post_redirect" />
<meta property="fc:frame:post_url" content="${process.env.NEXT_PUBLIC_BASE_URL}/api/end" />
</head></html>`);
} else {
return new NextResponse(`<!DOCTYPE html><html><head>
<title>This is frame ${id}</title>
<meta property="fc:frame" content="vNext" />
<meta property="fc:frame:image" content="${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/Qme4FXhoxHHfyzTfRxSpASbMF8kajLEPkRQWhwWu9pkUjm/${id}.png" />
<meta property="fc:frame:button:1" content="Next Page" />
<meta property="fc:frame:post_url" content="${process.env.NEXT_PUBLIC_BASE_URL}/api/frame?id=${nextId}" />
</head></html>`);
}
}
export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}
export const dynamic = 'force-dynamic';
app/api/frame/route.ts
In this endpoint file, we’re going to start by getting the query parameter <span class="code-inline">id</span> which will kind of work like a page number. With this, we can turn it into a number and add to it each time we hit the endpoint. Then, we'll add a conditional statement to keep sending the incremented frame until it reaches 7, at which point we'll send back a new template with some CTA buttons.
At this point, you can test this endpoint with an API client - e.g. Postman or HTTPie - by running a call like <span class="code-inline">POST http://localhost:3000/api/frame?id=1</span> to see if the metadata tags are returning correctly. You’ll notice that the endpoint for frame 7 is <span class="code-inline">api/end</span>, and we have a special property called <span class="code-inline">fc:frame:button:action</span> with the value of <span class="code-inline">post_redirect</span>. This allows these buttons to be used a redirects to different URLs.
Let’s build that now, adding a directory <span class="code-inline">end</span> inside <span class="code-inline">api</span> and making a <span class="code-inline">route.ts</span> file:
.
├── app
│ ├── api
│ │ ├── end
│ │ │ └── route.ts
│ │ └── frame
│ │ └── route.ts
Inside of the <span class="code-inline">end</span> route we’ll just put in a simple redirect function.
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest): Promise<Response> {
const data = await req.json();
const buttonId = data.untrustedData.buttonIndex;
let path: string;
if (buttonId === 1) {
path = 'cosmiccowboys';
} else if (buttonId === 2) {
path = 'pinatacloud';
} else {
path = '';
}
const headers = new Headers();
headers.set('Location', `${process.env.NEXT_PUBLIC_BASE_URL}/`);
const response = NextResponse.redirect(`${process.env.NEXT_PUBLIC_BASE_URL}/${path}`, {
headers: headers,
status: 302,
});
return response;
}
export const dynamic = 'force-dynamic';
app/api/end/route.ts
Here is a good example of how you can use button information in other API calls. If you remember from earlier, when interacting with a Frame inside Farcaster, a JSON body is sent with the button click that contains the following information:
{
untrustedData: {
fid: 6023,
url: 'https://frame-tutorial.vercel.app/',
messageHash: '0x2c2b1100d2b21d2838d9deaaec1b04e2843fa659',
timestamp: 1706748930000,
network: 1,
buttonIndex: 1,
castId: { fid: 6023, hash: '0x0000000000000000000000000000000000000001' }
},
trustedData: {
messageBytes: '0a50080d10872f18828cb22e20018201410a2268747470733a2f2f6672616d652d7475746f7269616c2e76657263656c2e6170702f10011a1908872f1214000000000000000000000000000000000000000112142c2b1100d2b21d2838d9deaaec1b04e2843fa659180122408dee5b99694764b5c6dfb0c42853a83cbd5c2532c842c01808e0de0fd41c4a39ed65a1ce3fb61ec75788d399accb47cc332d972d6087bb8855ae3b6c41c1a60d28013220b6ba9a2754c6634975ebbb4fb45532a3851316b618ca0a477303b256eb176aae'
}
}
Inside the <span class="code-inline">untrustedData</span>, we can get the <span class="code-inline">buttonIndex</span> to see what the user pressed! Based on this information, we add a conditional statement as to where the redirect goes. With the current implementation of Frames, the redirect has to be the same host url as our app, so we can’t do an external url just yet. To do that, we’ll need two more paths for each of our recently declared routes by adding <span class="code-inline">cosmiccowboys</span> and <span class="code-inline">pinatacloud</span> as folders inside app, each with a <span class="code-inline">page.tsx</span>.
.
├── app
│ ├── api
│ │ ├── end
│ │ │ └── route.ts
│ │ └── frame
│ │ └── route.ts
│ ├── cosmiccowboys
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ └── pinatacloud
│ └── page.tsx
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── README.md
├── tailwind.config.ts
└── tsconfig.json
In each of those <span class="code-inline">page.tsx</span> files, we can just add the following code:
export default function Page() {
return <h1>Redirecting...</h1>
}
app/cosmiccowboys/page.tsx
The real magic will be in our <span class="code-inline">next.config.mjs</span> file.
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
async redirects(){
return [
{
source: '/cosmiccowboys',
destination: 'https://cosmiccowboys.cloud',
permanent: false
},
{
source: '/pinatacloud',
destination: 'https://pinata.cloud/blog',
permanent: false
}
]
}
};
export default nextConfig;
It’s really simple! If the <span class="code-inline">/pinatacloud</span> or <span class="code-inline">/cosmiccowboys</span> routes are visited, it will redirect the user to the external urls.
When you host this repo on Vercel, don’t forget to add in your environment variables and to replace the <span class="code-inline"> NEXT_PUBLIC_BASE_URL</span> with the domain Vercel gives it. In the end, we get something like this:
Wrapping Up
While this is a simple example, hopefully it gives you some idea on what is possible with Frames. Some of the real magic is behind the Farcaster open data platform, as you saw in the JSON payload, where developers can utilize FIDs (Farcaster IDs) to access information, such as wallet addresses. They can also take advantage of the fact that Frame clicks work like Farcaster messages, which use a “Ed25519 account key (aka signer) that belongs to the user. “
More and more Frames are popping up with onchain utility and real world value, and we can’t wait to see what you build! Be sure to check out the Frame docs, as well as this awesome guide, to other Frame tools. You can also check out the repo for this project here.
Happy Pinning!