|
| 1 | +--- |
| 2 | +title: Generate Dynamic Embed Images |
| 3 | +description: Create viral loops by turning every user interaction into dynamic, shareable content directly in the feed. |
| 4 | +--- |
| 5 | + |
| 6 | +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. |
| 7 | + |
| 8 | +<Panel> |
| 9 | +<Frame caption="How metadata transforms into embeds"> |
| 10 | + <img src="/images/minikit/Diagram.png" alt="Diagram showing the flow from mini app URL to metadata reading to image generation and final embed rendering in the Base app" /> |
| 11 | +</Frame> |
| 12 | +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. |
| 13 | +</Panel> |
| 14 | + |
| 15 | +<Note> |
| 16 | +This guide uses Minikit but the principles apply to any framework with server-side rendering. |
| 17 | +</Note> |
| 18 | + |
| 19 | +## Implementation |
| 20 | + |
| 21 | +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. |
| 22 | + |
| 23 | +<Steps> |
| 24 | +<Step title="Install the required package"> |
| 25 | +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: |
| 26 | + |
| 27 | +```bash |
| 28 | +npm install @vercel/og |
| 29 | +``` |
| 30 | +</Step> |
| 31 | +<Step title="Create the image generation API endpoint"> |
| 32 | +Build an API route that generates images based on the username parameter. |
| 33 | + |
| 34 | +```tsx lines expandable wrap app/api/og/[username]/route.tsx |
| 35 | +import { ImageResponse } from "next/og"; |
| 36 | + |
| 37 | +export const dynamic = "force-dynamic"; |
| 38 | + |
| 39 | +export async function GET( |
| 40 | + request: Request, |
| 41 | + { params }: { params: Promise<{ username: string }> } |
| 42 | +) { |
| 43 | + const { username } = await params; |
| 44 | + |
| 45 | + return new ImageResponse( |
| 46 | + ( |
| 47 | + <div |
| 48 | + style={{ |
| 49 | + backgroundColor: 'black', |
| 50 | + backgroundSize: '150px 150px', |
| 51 | + height: '100%', |
| 52 | + width: '100%', |
| 53 | + display: 'flex', |
| 54 | + color: 'white', |
| 55 | + textAlign: 'center', |
| 56 | + alignItems: 'center', |
| 57 | + justifyContent: 'center', |
| 58 | + flexDirection: 'column', |
| 59 | + flexWrap: 'nowrap', |
| 60 | + }} |
| 61 | + > |
| 62 | + Hello {username} |
| 63 | + </div> |
| 64 | + ), |
| 65 | + { |
| 66 | + width: 1200, |
| 67 | + height: 630, |
| 68 | + } |
| 69 | + ); |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +This endpoint generates a unique image for each username: `/api/og/alice`, `/api/og/bob`, etc. |
| 74 | + |
| 75 | +<Warning> |
| 76 | + `<div>` elements must have `display: "flex"` or `display: "none"`. If you see a 500 error when accessing `/share/[username]`, check your ImageResponse JSX structure. |
| 77 | +</Warning> |
| 78 | +</Step> |
| 79 | + |
| 80 | +<Step title="Create shareable page with dynamic metadata"> |
| 81 | +Build a page route that uses the username to generate `fc:miniapp` metadata pointing to your image endpoint. |
| 82 | + |
| 83 | +```tsx lines expandable wrap app/share/[username]/page.tsx |
| 84 | +import { minikitConfig } from "../../../minikit.config"; |
| 85 | +import { Metadata } from "next"; |
| 86 | + |
| 87 | +export async function generateMetadata( |
| 88 | + { params }: { params: Promise<{ username: string }> } |
| 89 | +): Promise<Metadata> { |
| 90 | + try { |
| 91 | + const { username } = await params; |
| 92 | + |
| 93 | + return { |
| 94 | + title: minikitConfig.miniapp.name, |
| 95 | + description: minikitConfig.miniapp.description, |
| 96 | + other: { |
| 97 | + "fc:miniapp": JSON.stringify({ |
| 98 | + version: minikitConfig.miniapp.version, |
| 99 | + imageUrl: `${minikitConfig.miniapp.homeUrl}/api/og/${username}`, |
| 100 | + button: { |
| 101 | + title: `Join the ${minikitConfig.miniapp.name} Waitlist`, |
| 102 | + action: { |
| 103 | + name: `Launch ${minikitConfig.miniapp.name}`, |
| 104 | + type: "launch_frame", |
| 105 | + url: `${minikitConfig.miniapp.homeUrl}`, |
| 106 | + }, |
| 107 | + }, |
| 108 | + }), |
| 109 | + }, |
| 110 | + }; |
| 111 | + } catch (e) { |
| 112 | + const errorMessage = e instanceof Error ? e.message : 'Unknown error'; |
| 113 | + console.log(JSON.stringify({ |
| 114 | + timestamp: new Date().toISOString(), |
| 115 | + level: 'error', |
| 116 | + message: 'Failed to generate metadata', |
| 117 | + error: errorMessage |
| 118 | + })); |
| 119 | + |
| 120 | + return { |
| 121 | + title: minikitConfig.miniapp.name, |
| 122 | + description: minikitConfig.miniapp.description, |
| 123 | + }; |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +export default async function SharePage( |
| 128 | + { params }: { params: Promise<{ username: string }> } |
| 129 | +) { |
| 130 | + const { username } = await params; |
| 131 | + |
| 132 | + return ( |
| 133 | + <div> |
| 134 | + <h1>Share Page - {username}</h1> |
| 135 | + </div> |
| 136 | + ); |
| 137 | +} |
| 138 | +``` |
| 139 | +When someone visits `/share/alice`, the metadata points to `/api/og/alice` for the embed image. |
| 140 | +</Step> |
| 141 | + |
| 142 | +<Step title="Add share button with composeCast"> |
| 143 | +Create a button that opens Farcaster's compose window with the user's personalized share link. |
| 144 | + |
| 145 | +```tsx lines expandable wrap app/page.tsx highlight={6, 9-15} |
| 146 | +import { useMiniKit, useComposeCast } from "@coinbase/onchainkit/minikit"; |
| 147 | +import { minikitConfig } from "./minikit.config"; |
| 148 | + |
| 149 | +export default function HomePage() { |
| 150 | + const { context } = useMiniKit(); |
| 151 | + const { composeCast } = useComposeCast(); |
| 152 | + |
| 153 | + |
| 154 | + const handleShareApp = () => { |
| 155 | + const userName = context?.user?.displayName || 'anonymous'; |
| 156 | + composeCast({ |
| 157 | + text: `Check out ${minikitConfig.miniapp.name}!`, |
| 158 | + embeds: [`${window.location.origin}/share/${userName}`] |
| 159 | + }); |
| 160 | + }; |
| 161 | + |
| 162 | + return ( |
| 163 | + <div> |
| 164 | + <button onClick={handleShareApp}> |
| 165 | + Share Mini App |
| 166 | + </button> |
| 167 | + </div> |
| 168 | + ); |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +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`. |
| 173 | +</Step> |
| 174 | + |
| 175 | +<Step title="Test the flow"> |
| 176 | +Verify the complete sharing flow works. |
| 177 | + |
| 178 | +```bash lines wrap |
| 179 | +# Start your app |
| 180 | +npm run dev |
| 181 | + |
| 182 | +# Test the image endpoint directly |
| 183 | +curl http://localhost:3000/api/og/testuser > test.png |
| 184 | +open test.png |
| 185 | + |
| 186 | +# Visit the share page to verify metadata |
| 187 | +curl http://localhost:3000/share/testuser | grep "fc:miniapp" |
| 188 | +``` |
| 189 | + |
| 190 | +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. |
| 191 | +</Step> |
| 192 | +</Steps> |
| 193 | + |
| 194 | +## Related Concepts |
| 195 | + |
| 196 | +<CardGroup cols={1}> |
| 197 | +<Card title="Troubleshooting" href="/mini-apps/troubleshooting/how-search-works"> |
| 198 | + Troubleshooting tips for embeds not displaying. |
| 199 | +</Card> |
| 200 | +</CardGroup> |
0 commit comments