{
  "id": "generate-og-images-at-build-time",
  "type": "prompts",
  "category": "prompts",
  "locale": "en",
  "url": "/prompts/generate-og-images-at-build-time",
  "title": "Prompt to Generate OG Images at Build Time",
  "description": "AI agent prompt to generate per-page Open Graph images at build time in Astro or Next.js using Satori and sharp, with no external service.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Astro",
    "Next.js",
    "TypeScript"
  ],
  "tags": [
    "seo",
    "astro",
    "nextjs",
    "typescript"
  ],
  "difficulty": "medium",
  "updated": "2026-06-08",
  "markdown": "Give this prompt to your agent to generate a unique OG image PNG for every page at\nbuild time using Satori and sharp — avoiding the latency and cost of runtime OG\nimage services like Vercel OG.\n\n## Main Prompt\n\n```txt title=\"Main Prompt\"\nYou are working in an existing Astro 5 static site with TypeScript.\n\nTask: generate a static Open Graph PNG image for every blog post at build time.\n\nRequirements:\n- Install `satori` and `sharp`.\n- Create `src/lib/generate-og.ts` that exports `generateOgImage({ title, description }: OgData): Promise<Buffer>`.\n  - Use Satori to render a JSX template (800 x 420 px) with: site name top-left, post title\n    (large, bold, white), description (smaller, gray), a solid dark background (#0f172a).\n  - Convert the Satori SVG output to PNG using `sharp(Buffer.from(svg)).png().toBuffer()`.\n  - Use only system-safe fonts or load `src/fonts/Inter-Bold.ttf` (create a note to add the file).\n- Create `src/pages/og/[slug].png.ts` as an Astro endpoint:\n  - `getStaticPaths`: call `getCollection('blog')` and return a path per post.\n  - `GET`: call `generateOgImage` with the post's title and description, set\n    `Content-Type: image/png`, return the buffer.\n- In the blog post layout, set `<meta property=\"og:image\" content={ogImageUrl} />` where\n  `ogImageUrl` is constructed as `/og/${post.slug}.png`.\n- Do NOT use `@vercel/og`, `next/og`, or any cloud image generation service.\n- Do NOT use Canvas or puppeteer.\n\nStop and list all planned file changes before writing any code.\n```\n\n## Implementation Notes\n\n- Satori accepts JSX-like objects (React element syntax), but it does NOT use the React runtime.\n  Pass the element as a plain object tree using `satori`'s first argument directly.\n- Font loading: Satori requires at least one font. Use `fs.readFileSync` to load the `.ttf` file\n  at build time — this is fine in an Astro static build.\n- The generated PNGs will be in the `dist/og/` folder after `astro build`. Check their size; at\n  800x420 they should be under 100 KB each if using a flat color background.\n- For Next.js instead of Astro: use a Route Handler at `app/og/[slug]/route.ts` and call\n  `NextResponse` with the PNG buffer.\n\n## Expected File Changes\n\n```txt\nsrc/lib/generate-og.ts              (new)\nsrc/pages/og/[slug].png.ts          (new — Astro endpoint)\nsrc/layouts/BlogPost.astro          (edited — add og:image meta)\nsrc/fonts/Inter-Bold.ttf            (add manually — not auto-generated)\npackage.json                        (edited — add satori, sharp)\n```\n\n## Acceptance Criteria\n\n- `astro build` generates a `.png` file under `dist/og/` for each blog post.\n- Each PNG is 800 x 420 pixels.\n- The blog post layout includes `og:image` pointing to the correct `/og/<slug>.png` path.\n- Opening the OG image URL in a browser shows a styled card with the post title.\n\n## Test Commands\n\n```bash\nbun add satori sharp\nbun run build\nls dist/og/\nfile dist/og/my-first-post.png   # should output \"PNG image data, 800 x 420\"\ncurl -I http://localhost:4321/og/my-first-post.png | grep content-type\n```\n\n## Common AI Mistakes\n\n- Importing React and trying to render with `ReactDOMServer` — Satori does not use React's runtime.\n- Forgetting to load a font, which causes Satori to throw `\"No font found for the first character\"`.\n- Setting `og:image` to a relative path like `./og/slug.png` — it must be an absolute URL in production.\n- Using `canvas` package which requires native bindings incompatible with many CI environments.\n\n## Fix Prompt\n\n```txt title=\"Fix Prompt\"\nSatori throws a font error or the PNG is blank. Fix in order:\n1. Add font loading: `const fontData = fs.readFileSync('src/fonts/Inter-Bold.ttf'); fonts: [{ name: 'Inter', data: fontData, weight: 700 }]` in the satori call.\n2. Make sure the JSX-like element passed to satori is a plain object, not a JSX expression — call `satori(element, options)` directly.\n3. For the og:image meta tag, use an absolute URL: `const base = import.meta.env.SITE; ogImageUrl = new URL(\\`/og/\\${slug}.png\\`, base).toString();`\nShow only the corrected diff.\n```"
}