Skip to main content
Diagram showing the flow from mini app URL to metadata reading to image generation and final embed rendering in the Base app

How metadata transforms into embeds

When users share your mini app URL, the Base app requests your page, reads the fc:miniapp metadata, and fetches the imageUrl. You can serve either a static file (same image for everyone) or a dynamic endpoint that generates unique images on-demand based on URL parameters.
Embeds are the first thing users see when they encounter your mini app in their feed. Each share can display unique, contextual content tailored to drive engagement.
This guide uses Minikit but the principles apply to any framework with server-side rendering.

Implementation

This guide shows how to create shareable links with dynamic embed images. Users click a share button, which opens a compose window with their personalized link. When shared, the embed displays a unique image with their username.
1

Install the required package

Install @vercel/og by running the following command inside your project directory. This isn’t required for Next.js App Router projects, as the package is already included:
npm install @vercel/og
2

Create the image generation API endpoint

Build an API route that generates images based on the username parameter.
app/api/og/[username]/route.tsx
import { ImageResponse } from "next/og";

export const dynamic = "force-dynamic";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ username: string }> }
) {
  const { username } = await params;

  return new ImageResponse(
    (
      <div
          style={{
            backgroundColor: 'black',
            backgroundSize: '150px 150px',
            height: '100%',
            width: '100%',
            display: 'flex',
            color: 'white',
            textAlign: 'center',
            alignItems: 'center',
            justifyContent: 'center',
            flexDirection: 'column',
            flexWrap: 'nowrap',
          }}
        >
            Hello {username}        
        </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  );
}
This endpoint generates a unique image for each username: /api/og/alice, /api/og/bob, etc.
<div> elements must have display: "flex" or display: "none". If you see a 500 error when accessing /share/[username], check your ImageResponse JSX structure.
3

Create shareable page with dynamic metadata

Build a page route that uses the username to generate fc:miniapp metadata pointing to your image endpoint.
app/share/[username]/page.tsx
import { minikitConfig } from "../../../minikit.config";
import { Metadata } from "next";

export async function generateMetadata(
  { params }: { params: Promise<{ username: string }> }
): Promise<Metadata> {
  try {
    const { username } = await params;
    
    return {
      title: minikitConfig.miniapp.name,
      description: minikitConfig.miniapp.description,
      other: {
        "fc:miniapp": JSON.stringify({
          version: minikitConfig.miniapp.version,
          imageUrl: `${minikitConfig.miniapp.homeUrl}/api/og/${username}`,
          button: {
            title: `Join the ${minikitConfig.miniapp.name} Waitlist`,
            action: {
              name: `Launch ${minikitConfig.miniapp.name}`,
              type: "launch_frame",
              url: `${minikitConfig.miniapp.homeUrl}`,
            },
          },
        }),
      },
    };
  } catch (e) {
    const errorMessage = e instanceof Error ? e.message : 'Unknown error';
    console.log(JSON.stringify({ 
      timestamp: new Date().toISOString(), 
      level: 'error', 
      message: 'Failed to generate metadata', 
      error: errorMessage 
    }));
    
    return {
      title: minikitConfig.miniapp.name,
      description: minikitConfig.miniapp.description,
    };
  }
}

export default async function SharePage(
  { params }: { params: Promise<{ username: string }> }
) {
  const { username } = await params;

  return (
    <div>
      <h1>Share Page - {username}</h1>
    </div>
  );
}
When someone visits /share/alice, the metadata points to /api/og/alice for the embed image.
4

Add share button with composeCast

Create a button that opens Farcaster’s compose window with the user’s personalized share link.
app/page.tsx
import { useMiniKit, useComposeCast } from "@coinbase/onchainkit/minikit";
import { minikitConfig } from "./minikit.config";

export default function HomePage() {
  const { context } = useMiniKit();
  const { composeCast } = useComposeCast();


  const handleShareApp = () => {
    const userName = context?.user?.displayName || 'anonymous';
    composeCast({
      text: `Check out ${minikitConfig.miniapp.name}!`,
      embeds: [`${window.location.origin}/share/${userName}`]
    });
  };

  return (
    <div>
      <button onClick={handleShareApp}>
        Share Mini App
      </button>
    </div>
  );
}
When you click the button, it opens the compose window with /share/alice as the embed. The embed displays the dynamic image from /api/og/alice.
5

Test the flow

Verify the complete sharing flow works.
# Start your app
npm run dev

# Test the image endpoint directly
curl http://localhost:3000/api/og/testuser > test.png
open test.png

# Visit the share page to verify metadata
curl http://localhost:3000/share/testuser | grep "fc:miniapp"
Click the share button in your app to test the full experience. You should see the compose window open with your personalized share link, and the embed should display your custom generated image.