Cover photo

How to Make a Frame on Farcaster using IPFS

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: https://mktg.mypinata.cloud.

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 includesTypescript, 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 NEXT_PUBLIC_BASE_URL will just be set to our localhost until we upload to Vercel, at which point we’ll use whatever domain it assigns us. The NEXT_PUBLIC_GATEWAY_URL is the Dedicated Gateway we grabbed from our account earlier, and it should be in the format of https://mktg.mypinata.cloud.

Now, let’s go into the app/page.tsx, 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>
    </>
  );
}

In this file, we’re setting up a few things. To start, we’re importing some dependencies from onchainkit and next to allow us to export some metadata tags. In the frameMetadata, 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 post_url which will be an API endpoint taking us to the next page of our story book. We also have regular metadata, including an openGraph 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 layout.tsx 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>
  );
}

Now, we need to build the api endpoint that we’re making a post request to. Create a folder called api inside the app directory, and then another folder called frame with a route.ts 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 route.ts, 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';

In this endpoint file, we’re going to start by getting the query parameter id 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 POST http://localhost:3000/api/frame?id=1 and see if the metadata tags are returning correctly. You’ll notice that the endpoint for frame 7 is api/end, and we have a special property called fc:frame:button:action with the value of post_redirect. This allows these buttons to be used a redirects to different URLs.

Let’s build that now, adding a directory end inside api and making a route.ts file:

.
├── app
│  ├── api
│  │  ├── end
│  │  │  └── route.ts
│  │  └── frame
│  │     └── route.ts

Inside of the end 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';

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 untrustedData, we can get the buttonIndex 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 cosmiccowboys and pinatacloud as folders inside app, each with a page.tsx.

.
├── 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 page.tsx files, we can just add the following code:

export default function Page() {
  return <h1>Redirecting...</h1>
}

The real magic will be in our next.config.mjs 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 /pinatacloud or /cosmiccowboys 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 NEXT_PUBLIC_BASE_URL 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!

Pinata logo
Subscribe to Pinata and never miss a post.
  • Loading comments...