Quickstart Guide: Display Frames
This guide shows you how to add frames rendering to your next.js + tailwind app using frames.js.
Steps
Create a new repo
Create a new Next.js app
npx create-next-app@latest my-project --ts --eslint --tailwind --app
cd my-project
Add @frames.js/render
to your project
npm
npm install @frames.js/render
Add proxies for routing frame requests via your backend for privacy + preventing CORS issues
./app/frames/route.tsx
export { GET, POST } from "@frames.js/render/next";
Set up the useFrame()
hook
See the useFrame()
hook documentation for more information.
"use client";
import {
type FarcasterSigner,
signFrameAction,
} from "@frames.js/render/farcaster";
import { useFrame } from "@frames.js/render/use-frame";
import { fallbackFrameContext } from "@frames.js/render";
export default function App() {
// @TODO: replace with your farcaster signer
const farcasterSigner: FarcasterSigner = {
fid: 1,
status: "approved",
publicKey:
"0x00000000000000000000000000000000000000000000000000000000000000000",
privateKey:
"0x00000000000000000000000000000000000000000000000000000000000000000",
};
const frameState = useFrame({
// replace with frame URL
homeframeUrl: "https://framesjs.org",
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/frames",
frameGetProxy: "/frames",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log(
"A frame button was pressed without a signer. Perhaps you want to prompt a login"
);
},
signFrameAction,
async logout() {
// here you can add your logout logic
console.log("logout");
},
},
});
// @TODO here will go the renderer
return null;
}
Add the renderer to your page
"use client";
import {
type FarcasterSigner,
signFrameAction,
} from "@frames.js/render/farcaster";
import { useFrame } from "@frames.js/render/use-frame";
import { fallbackFrameContext } from "@frames.js/render";
import {
FrameUI,
type FrameUIComponents,
type FrameUITheme,
} from "@frames.js/render/ui";
/**
* StylingProps is a type that defines the props that can be passed to the components to style them.
*/
type StylingProps = {
className?: string;
style?: React.CSSProperties;
};
/**
* You can override components to change their internal logic or structure if you want.
* By default it is not necessary to do that since the default structure is already there
* so you can just pass an empty object and use theme to style the components.
*
* You can also style components here and completely ignore theme if you wish.
*/
const components: FrameUIComponents<StylingProps> = {};
/**
* By default there are no styles so it is up to you to style the components as you wish.
*/
const theme: FrameUITheme<StylingProps> = {
ButtonsContainer: {
className: "flex gap-[8px] px-2 pb-2 bg-white",
},
Button: {
className:
"border text-sm text-gray-700 rounded flex-1 bg-white border-gray-300 p-2",
},
Root: {
className:
"flex flex-col w-full gap-2 border rounded-lg overflow-hidden bg-white relative",
},
Error: {
className:
"flex text-red-500 text-sm p-2 bg-white border border-red-500 rounded-md shadow-md aspect-square justify-center items-center",
},
LoadingScreen: {
className: "absolute top-0 left-0 right-0 bottom-0 bg-gray-300 z-10",
},
Image: {
className: "w-full object-cover max-h-full",
},
ImageContainer: {
className:
"relative w-full h-full border-b border-gray-300 overflow-hidden",
style: {
aspectRatio: "var(--frame-image-aspect-ratio)", // fixed loading skeleton size
},
},
TextInput: {
className: "p-[6px] border rounded border-gray-300 box-border w-full",
},
TextInputContainer: {
className: "w-full px-2",
},
};
export default function App() {
// @TODO: replace with your farcaster signer
const farcasterSigner: FarcasterSigner = {
fid: 1,
status: "approved",
publicKey:
"0x00000000000000000000000000000000000000000000000000000000000000000",
privateKey:
"0x00000000000000000000000000000000000000000000000000000000000000000",
};
const frameState = useFrame({
// replace with frame URL
homeframeUrl: "https://framesjs.org",
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/frames",
frameGetProxy: "/frames",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log(
"A frame button was pressed without a signer. Perhaps you want to prompt a login"
);
},
signFrameAction,
async logout() {
// here you can add your logout logic
console.log("logout");
},
},
});
return (
<FrameUI frameState={frameState} components={components} theme={theme} />
);
}
Allow images from any domain
next.config.js
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
},
};
Run
npm
npm run dev
Done! 🎉
Using Next.js Image Optimization for image proxying
You can use next/image
to proxy images via your backend to preserve user privacy and prevent CORS issues.
import { type FrameUIComponents } from "@frames.js/render/ui";
import Image from "next/image";
/**
* StylingProps is a type that defines the props that can be passed to the components to style them.
*/
type StylingProps = {
className?: string;
style?: React.CSSProperties;
};
/**
* You can override components to change their internal logic or structure if you want.
* By default it is not necessary to do that since the default structure is already there
* so you can just pass an empty object and use theme to style the components.
*
* You can also style components here and completely ignore theme if you wish.
*/
const components: FrameUIComponents<StylingProps> = {
Image: (props, stylingProps) => {
if (props.status === "frame-loading") {
return <></>;
}
// Here you can add your own logic to sanitize and validate the image URL
let sanitizedSrc = props.src;
// Don't allow data URLs that are not images -- we don't want to allow arbitrary data to be loaded
if (props.src.startsWith("data:") && !props.src.startsWith("data:image")) {
sanitizedSrc = "";
}
// Don't allow SVG data URLs -- could contain malicious code
if (props.src.startsWith("data:image/svg")) {
sanitizedSrc = "";
}
// Can set the dimensions based on the aspect ratio of the image
// const aspectRatio = props.aspectRatio; // "1:1" or "1.91:1"
return (
<Image
{...stylingProps}
src={sanitizedSrc}
onLoad={props.onImageLoadEnd}
onError={props.onImageLoadEnd}
alt="Frame image"
sizes="100vw"
height={0}
width={0}
/>
);
},
};