# Open Graph images

Takumi renders typed React templates to PNG on Node and workerd.

## Request to image [#request-to-image]

The OG registry pairs each template with its input schema and render function. The route validates query parameters, selects a supported locale and renders the configured image dimensions. Home, default, post and profile templates cover public pages. The `locale` parameter comes from the page's language, which on a public page is its path (`/de/privacy` → `…/api/og/default?locale=de&…`), so each language's URL shares with its own card and the edge cache keys them apart.

Node development uses the native Takumi renderer. Workerd uses the WASM renderer and bundled font data. The existing renderer adapter contains historical runtime-specific imports; documentation adds no new dynamic imports.

On workerd the route checks Cloudflare's edge cache (`caches.default`) before rendering and stores the PNG afterwards, keyed by the validated parameters, the locale and `OG_VERSION` (`platform/og/og-cache.ts`): a crawler storm on one share image costs one render per colo. Node has no edge cache and renders every time; a `*.workers.dev` origin treats the Cache API as a no-op. Bumping `OG_VERSION` changes the URL in every page head and the cache key with it. `/robots.txt` allows `/api/og/` explicitly, because `Disallow: /api/` would otherwise keep social crawlers from fetching the image.

The brand name on the frame comes from the site identity (`platform/seo/site-identity.ts`, [SEO and discovery](/docs/features/seo)); the colors in `platform/og/config.ts` mirror the UI tokens and are edited by hand when rebranding.

## React Compiler boundary [#react-compiler-boundary]

OG components are called by the image renderer outside React's normal dispatcher. They use the `"use no memo"` directive so React Compiler does not insert hook-dependent memoization.

```tsx
function ImageHeading({ title }: { title: string }) {
  "use no memo";
  return <div style={{ fontSize: 64 }}>{title}</div>;
}
```

Use renderer-supported layout and inline styles. Do not assume browser CSS, client hooks or arbitrary DOM APIs work inside an OG template.

## Add a template [#add-a-template]

Define a bounded Zod parameter schema, add a registry entry and render with the shared frame. Connect it through the SEO helper so canonical image URLs use the intended origin. Pages without a dedicated template, including every docs page, use the `default` template with the page title and the description as subtitle. Test every locale with native rendering and verify the actual built Worker path; a Node PNG is not proof that the WASM path works.

## Key files [#key-files]

- [`apps/web/src/platform/og/registry.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/og/registry.ts)
- [`apps/web/src/platform/og/render.server.tsx`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/og/render.server.tsx)
- [`apps/web/src/platform/og/render-wasm.server.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/og/render-wasm.server.ts)
- [`apps/web/src/platform/og/frame.tsx`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/og/frame.tsx)
- [`apps/web/src/platform/seo/og-url.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/seo/og-url.ts)
- [`apps/web/src/platform/og/og-cache.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/og/og-cache.ts)

