Framework guide

Dynamic Open Graph images for Astro

Bake per-entry OGKit URLs in Astro layouts or content collections at build time (private OGKIT_KEY, never PUBLIC_). After title changes: rebuild, deploy, rescrape.

Every framework only needs a stable, absolute HTTPS og:image. OGKit gives you one URL, production templates, and query parameters for titles, images, logos, authors, products, events, jobs, and code snippets.

Server-rendered metadata is the SEO boundary

Open Graph images only help distribution when the final HTML response already contains the metadata. Most scrapers do not wait for client-side JavaScript, so React hydration, client routers, and analytics callbacks are too late. Build the OGKit URL in the server route, loader, layout, view helper, or static generation step that owns the document head.

Use one deterministic image URL per canonical page

The strongest pattern is one stable 1200x630 image URL for each canonical URL. Put the same image in Open Graph and Twitter/X metadata, keep the title aligned with the visible H1, and include page-specific context such as author, product name, release version, or docs section. That gives Slack, Discord, LinkedIn, iMessage, and browser-assisted LLM crawlers the same topic signal as the page body.

When to choose hosted templates over custom renderers

Custom Satori, Puppeteer, or screenshot routes make sense when you need arbitrary layout control. Hosted templates make more sense when the business need is repeatable: blog cards, launch pages, changelogs, docs pages, product pages, and comparison pages that should look consistent without a renderer living in every codebase.

Implementation pattern

Generate the OGKit URL before the crawler sees the HTML. That can happen during static generation, in a server route, or in the framework metadata layer. Start with the API reference, deep guides, test the URL in the Playground, then validate the deployed page with the preview debugging tools.

---
// src/layouts/BlogPost.astro — static-friendly: URL baked at build
const { title, description } = Astro.props;
const image = new URL("https://www.webmorp.art/api/og/article");
image.searchParams.set("key", import.meta.env.OGKIT_KEY);
image.searchParams.set("title", title);
if (description) image.searchParams.set("subtitle", description);
const og = image.toString();
---
<meta property="og:image" content={og} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={og} />

Deeper Astro notes

Content collections (unique per slug)

Map each collection entry to an OGKit URL from entry.data.title. On static output, every article ships a unique absolute card URL with zero runtime cost — scrapers never need your Astro server.

---
import { getCollection } from "astro:content";
// pages/blog/[...slug].astro
const { entry } = Astro.props;
const og = new URL("https://www.webmorp.art/api/og/article");
og.searchParams.set("key", import.meta.env.OGKIT_KEY);
og.searchParams.set("title", entry.data.title);
og.searchParams.set("subtitle", entry.data.description ?? "Blog");
---
<meta property="og:image" content={og.toString()} />
<meta name="twitter:image" content={og.toString()} />

astro.config env (private only)

Put the key in the server/build environment. Do not prefix with PUBLIC_. In CI, inject OGKIT_KEY as a secret before npm run build so static HTML contains a real key query param.

# .env (local) / GitHub Actions secret (CI)
OGKIT_KEY=ogk_live_...
# Never: PUBLIC_OGKIT_KEY=...

Static site rescrape

Astro SSG freezes og:image into HTML at build. Changing a title requires a rebuild and deploy, then a platform rescrape. Prefer updating the OGKit title/subtitle query (new URL) over hoping Slack drops the old PNG. See Troubleshooting and Caching & rescrape.

MDX / docs layouts

Put meta tags in the layout that wraps MDX so docs and blog share one pattern. Pass frontmatter into the props used for the query string — do not set a homepage card inside a nested docs layout.

Worked steps

  1. Add OGKIT_KEY to Astro env (non-PUBLIC) and CI secrets.
  2. Build the URL in the blog/docs layout from frontmatter.
  3. Emit og:image + twitter:image (+ width/height).
  4. Inspect dist/**/*.html for absolute https://…/api/og/… URLs.
  5. Deploy, then Facebook Debugger → Scrape Again on 1–2 posts.

Checklist

  • Build URLs in layouts or content collections at build time (SSG).
  • Store OGKIT_KEY as a private env (never PUBLIC_).
  • Set og:image width/height (1200×630).
  • Per-entry titles from collection frontmatter — not one site logo.
  • After rebuilds that change titles, rescrape Facebook/LinkedIn.

Common pitfalls

  • Hardcoding one preview for all markdown pages
  • Using import.meta.env.PUBLIC_OGKIT_KEY (leaks to the browser)
  • Relative /og.png paths in production HTML
  • Building locally without OGKIT_KEY → empty key= in committed HTML
  • Expecting crawlers to see client-only meta from React islands

When a hosted OG API makes sense

A hosted Open Graph image API is useful when you need consistent cards across many pages but do not want to maintain a custom renderer in every app. It is especially useful for docs, changelogs, launch pages, public customer pages, and content collections where the title and summary change often. For HMAC signing and rescrape workflows, see signed URLs and caching & rescrape.

Frequently asked questions

How do I add dynamic Open Graph images to Astro?

Build an absolute OGKit image URL on the server or during static generation, then place it in og:image and twitter:image metadata for each important page.

Can I use OGKit with Astro without a custom image route?

Yes. OGKit returns a normal 1200x630 PNG URL from template and query parameters, so you do not need to maintain a Satori, Puppeteer, or screenshot pipeline.

Where should I keep the OGKit API key?

Keep the API key in server-side environment variables, framework runtime config, or build-time secrets. Do not bundle it into client-side JavaScript.

I changed the title but Slack still shows the old card — what now?

Rebuild/redeploy so HTML has the new OGKit query, confirm with curl, then rescrape Facebook/LinkedIn. Slack often needs a new image URL (updated fields or v=). See /guides/troubleshooting.

Related reading

OGKit turns one HTTPS URL into a 1200×630 Open Graph image. Read the API reference, deep guides, the Open Graph SEO guide, try the Playground, or sign in to create API keys.