# Dynamic OG Images \[Generate social preview images from page metadata]

## Overview

Open Graph images power link previews in Slack, Discord, X, and other clients. Vocs generates them per page in three steps:

1. It reads each page's `title` and `description` from frontmatter.
2. It resolves [`ogImageUrl`](/reference/site-config#ogimageurl) for the current page.
3. It substitutes `%title`, `%description`, and `%logo` into that URL, then writes the result to both `og:image` and `twitter:image`.

In Waku-powered Vocs sites, `/api/og` is available by default. If you want a different design, set up your own endpoint as below.

## Setup

Set up a dynamic OG image endpoint in four steps.

::::steps

### Create the API route

Add a file at `src/pages/_api/api/og.tsx`. Vocs strips the `_api` prefix, so this becomes `/api/og`.

:::file-tree

* +src
  * +pages
    * +\_api
      * +api
        * **og.tsx**
          :::

### Implement the handler

Use [`Handler.og`](/reference/components) from `vocs/server` to render JSX as a PNG. It reads `title` and `description` from the query string, falls back to your site config, and passes a resolved `logo` based on `logoUrl`.

```tsx [src/pages/_api/api/og.tsx]
import { Handler } from 'vocs/server'

export default function handler(request: Request) {
  return Handler.og(({ title, description, logo }) => ( // [!code focus:31]
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        padding: 80,
        backgroundColor: '#161616',
        color: '#ffffff',
      }}
    >
      {logo && (
        <img alt="" src={logo} style={{ height: 48, position: 'absolute', right: 40, bottom: 40 }} />
      )}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div style={{ fontSize: title.length < 15 ? 80 : 64, fontWeight: 700, lineHeight: 1.1 }}>
          {title}
        </div>
        {description && (
          <div style={{ fontSize: 28, color: 'rgba(255, 255, 255, 0.6)', maxWidth: 800 }}>
            {description}
          </div>
        )}
      </div>
    </div>
  )).fetch(request)
}
```

### Point `ogImageUrl` at it

Configure [`ogImageUrl`](/reference/site-config#ogimageurl) so Vocs emits absolute URLs to your new endpoint. Setting `baseUrl` is required for social crawlers.

```ts [vocs.config.ts]
export default defineConfig({
  baseUrl: 'https://docs.example.com', // [!code focus]
  ogImageUrl: (_path, { baseUrl }) => // [!code focus:3]
    `${baseUrl ?? ''}/api/og?title=%title&description=%description`,
})
```

### Verify

Run `pnpm dev`, open any page, and visit the URL printed in the page source under `<meta property="og:image">`. You should see your rendered PNG. For production, paste the deployed URL into [opengraph.xyz](https://www.opengraph.xyz/) to preview how it looks in each client.

::::

## Recipes

### Vary Images By Path

`ogImageUrl` receives the current pathname, so you can send different sections of your docs to different templates or static assets.

```ts [vocs.config.ts]
export default defineConfig({
  baseUrl: 'https://docs.example.com',
  ogImageUrl: (path, { baseUrl }) => { // [!code focus:11]
    const origin = baseUrl ?? ''

    if (path.startsWith('/blog/'))
      return `${origin}/api/og/blog?title=%title&description=%description`

    if (path === '/introduction/getting-started')
      return `${origin}/images/getting-started-og.png`

    return `${origin}/api/og?title=%title&description=%description`
  },
})
```

### Use Per-Page Metadata

Vocs feeds normal page frontmatter into `%title` and `%description` substitutions — no separate `ogImage` field needed.

```mdx
---
title: Deploying with Waku
description: Set up a production-ready Vocs site with Waku and Vite.
---
```

If one page needs a completely different image, return a different URL for that page from `ogImageUrl`.

### Tips

* Set `baseUrl` so generated OG image URLs are absolute — social crawlers reject relative URLs.
* Render at `1200x630` (the default for `Handler.og`) for the broadest sharing compatibility.
* Keep titles short and use high-contrast colors so previews stay readable at thumbnail size.
* If you use `renderStrategy: 'full-static'`, configure `ogImageUrl` explicitly — the default dynamic `/api/og` fallback is not used.

## See More

<Cards>
  <Card title="ogImageUrl" description="Site config option that controls which image URL Vocs emits per page." icon="image" to="/reference/site-config#ogimageurl" />

  <Card title="Handler.og" description="Server helper for rendering JSX to a PNG response." icon="server" to="/reference/components" />

  <Card title="API Routes" description="How _api file-system routes work and where to put handlers." icon="plug" to="/features/api-routes" />

  <Card title="Frontmatter" description="title, description, and other per-page metadata fields." icon="file-text" to="/writing/frontmatter" />
</Cards>
