PinataCloud/pinataframes

How to generate images for frame with satori?

Closed this issue · 2 comments

Hi! I'm trying to build a simple frame which will show some info for the user on one of the frames

In my api/frame/route.ts I have this:

import { fdk } from "@/app/page"
import { NextRequest, NextResponse } from "next/server"
import { getConnectedAddressForUser } from "@/utils/fc"

async function getResponse(req: NextRequest): Promise<NextResponse> {
    const searchParams = req.nextUrl.searchParams
    const id: any = searchParams.get("id")
    const idAsNumber = parseInt(id)

    /* ----------------------------- GETTING ADDRESS ---------------------------- */
    const body = await req.json()
    const fid = body.untrustedData.fid
    const address = await getConnectedAddressForUser(fid)

    const nextId = idAsNumber + 1

    if (idAsNumber === 1) {
        try {
            const frameMetadata = await fdk.getFrameMetadata({
                post_url: `${process.env.BASE_URL}/api/frame?id=${nextId}`,
                buttons: [{ label: "Show my role", action: "post" }],
                aspect_ratio: "1:1",
                image: { url: `${process.env.BASE_URL}/loading.png`, ipfs: false }
            })
            return new NextResponse(frameMetadata)
        } catch (error) {
            console.log(error)
            return NextResponse.json({ error: error })
        }
    } else if (idAsNumber === 2) {
        try {
            const frameMetadata = await fdk.getFrameMetadata({
                post_url: `${process.env.BASE_URL}/api/frame?id=${nextId}`,
                buttons: [
                    { label: "Mint Role", action: "post", target: `${process.env.BASE_URL}/api/frame?id=${4}` },
                    { label: "Why?", action: "post", target: `${process.env.BASE_URL}/api/frame?id=${3}` }
                ],
                aspect_ratio: "1:1",
                image: { url: `${process.env.BASE_URL}/role.png`, ipfs: false }
            })
            return new NextResponse(frameMetadata)
        } catch (error) {
            console.log(error)
            return NextResponse.json({ error: error })
        }
    } else if (idAsNumber === 3) {
        try {
            const frameMetadata = await fdk.getFrameMetadata({
                post_url: `${process.env.BASE_URL}/api/frame?id=${nextId}`,
                buttons: [{ label: "Mint Role", action: "post" }],
                aspect_ratio: "1:1",
                image: { url: `${process.env.BASE_URL}/api/image` }
            })
            return new NextResponse(frameMetadata)
        } catch (error) {
            console.log(error)
            return NextResponse.json({ error: error })
        }
    } else {
        try {
            const frameMetadata = await fdk.getFrameMetadata({
                post_url: `${process.env.BASE_URL}/api/end`,
                buttons: [
                    { label: "Mint NFT", action: "post" },
                    { label: "Button 2", action: "post_redirect" },
                    { label: "Button 3", action: "post_redirect" },
                    { label: "Button 4", action: "post_redirect" }
                ],
                aspect_ratio: "1:1",
                image: { url: `${process.env.BASE_URL}/mint.png`, ipfs: false }
            })
            return new NextResponse(frameMetadata)
        } catch (error) {
            console.log(error)
            return NextResponse.json({ error: error })
        }
    }
}

export async function POST(req: NextRequest): Promise<Response> {
    return getResponse(req)
}

export const dynamic = "force-dynamic"

In the third steps I'm referring to image: { url: `${process.env.BASE_URL}/api/image` } for my image which is api/image.tsx:

import type { NextApiRequest, NextApiResponse } from "next"
import sharp from "sharp"
import { Data } from "@/app/types"
import satori from "satori"
import { join } from "path"
import * as fs from "fs"

const fontPath = join(process.cwd(), "Roboto-Regular.ttf")
let fontData = fs.readFileSync(fontPath)

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    try {
        let data = { address: "user address" }

        const svg = await satori(
            <div
                style={{
                    justifyContent: "flex-start",
                    alignItems: "center",
                    display: "flex",
                    width: "100%",
                    height: "100%",
                    backgroundColor: "f4f4f4",
                    padding: 50,
                    lineHeight: 1.2,
                    fontSize: 24
                }}
            >
                <div style={{ display: "flex", flexDirection: "column", padding: 20 }}>
                    <h2 style={{ textAlign: "center", color: "lightgray" }}>Your Data</h2>
                    <div
                        style={{
                            backgroundColor: "#007bff",
                            color: "#fff",
                            padding: 10,
                            marginBottom: 10,
                            borderRadius: 4,
                            width: `100%`,
                            whiteSpace: "nowrap",
                            overflow: "visible"
                        }}
                    >
                        {data.address}
                    </div>
                </div>
            </div>,
            {
                width: 400,
                height: 400,
                fonts: [
                    {
                        data: fontData,
                        name: "Roboto",
                        style: "normal",
                        weight: 400
                    }
                ]
            }
        )

        // Convert SVG to PNG using Sharp
        const pngBuffer = await sharp(Buffer.from(svg)).toFormat("png").toBuffer()

        // Set the content type to PNG and send the response
        res.setHeader("Content-Type", "image/png")
        res.setHeader("Cache-Control", "max-age=10")
        res.send(pngBuffer)
    } catch (error) {
        console.error(error)
        res.status(500).send("Error generating image")
    }
}

However it gives 500 error on this frame in the test environment and I have no idea why.... Can somebody please advise me on how to fix this? I've seen analytics-frame project in this repo and satori.ts file but I would like to keep the images locally so I don't really know how to adjust it for my needs....

Assuming the output isn’t larger than 256 bytes, you can convert the png buffer to a dataURI and return that. For the open graph images to work, then need to be URIs but dataURIs work.

Fixed it with this:


export const uploadToIpfs = async (image: Buffer): Promise<string> => {
    try {
        const tempPath = path.join("/tmp", "image.png")
        fs.writeFileSync(tempPath, image)

        const file = fs.readFileSync(tempPath)
        const formData = new FormData()
        formData.append("file", new Blob([file]), "image.png")

        const metadata = JSON.stringify({ name: `image.png` })
        formData.append("pinataMetadata", metadata)
        const imageUpload = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
            method: "POST",
            headers: {
                Authorization: `Bearer ${process.env.PINATA_JWT}`
            },
            body: formData
        })
        const { IpfsHash } = await imageUpload.json()

        const url = `${process.env.GATEWAY_URL}/ipfs/${IpfsHash}?filename=image.png`
        console.log({ url })
        return url
    } catch (error) {
        console.log(error)
        throw error
    }
}


export const generateImage = async (stat: any): Promise<Buffer> => {
    const monoFontReg = await fetch(
        "https://api.fontsource.org/v1/fonts/inter/latin-400-normal.ttf",
    );

    const monoFontBold = await fetch(
        "https://api.fontsource.org/v1/fonts/inter/latin-700-normal.ttf",
    );

    const template: any = html(`<div>Demo html</div>`);

    // convert html to svg
    const svg = await satori(template, {
        width: 1200,
        height: 630,
        fonts: [
            {
                name: "Roboto Mono",
                data: await monoFontReg.arrayBuffer(),
                weight: 400,
                style: "normal",
            },
            {
                name: "Roboto Mono",
                data: await monoFontBold.arrayBuffer(),
                weight: 700,
                style: "normal",
            },
        ]
    });

    const pngBuffer = await sharp(Buffer.from(svg)).toFormat("png").toBuffer()

    return pngBuffer
}

export const getAnalyticsImageUrl = async (stat: wrappedStatType) => {
    try {
        const image = await generateImage(stat);
        const url = await uploadToIpfs(image);
        return url;
    } catch (error) {
        console.log(error);
        throw error;
    }
}