An open API service indexing awesome lists of open source software.

https://github.com/ShipItAndPray/pretext-og

OG image generator fixing Satori text wrapping bugs. Drop-in replacement powered by Pretext.
https://github.com/ShipItAndPray/pretext-og

pretext text-layout typescript typography

Last synced: about 1 month ago
JSON representation

OG image generator fixing Satori text wrapping bugs. Drop-in replacement powered by Pretext.

Awesome Lists containing this project

README

          

# @shipitandpray/pretext-og

[![Live Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://shipitandpray.github.io/pretext-og/) [![GitHub](https://img.shields.io/github/stars/ShipItAndPray/pretext-og?style=social)](https://github.com/ShipItAndPray/pretext-og)

> **[View Live Demo](https://shipitandpray.github.io/pretext-og/)**

[![npm version](https://img.shields.io/npm/v/@shipitandpray/pretext-og.svg)](https://www.npmjs.com/package/@shipitandpray/pretext-og)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@shipitandpray/pretext-og)](https://bundlephobia.com/result?p=@shipitandpray/pretext-og)

**OG image generator that fixes Satori's text wrapping bugs.** Drop-in replacement for `@vercel/og` with pixel-perfect text layout powered by [Pretext](https://github.com/chenglou/pretext).

## The Problem

Satori (by Vercel) is the de facto standard for generating OG images in Next.js via `@vercel/og`. But it has persistent text wrapping bugs that have remained open for years:

| Satori Issue | Bug | pretext-og Fix |
|---|---|---|
| [#484](https://github.com/vercel/satori/issues/484) | Text overflows container boundaries with certain font/size combinations due to cumulative rounding errors | Pretext measures exact character widths including kerning pairs |
| [#393](https://github.com/vercel/satori/issues/393) | Long words without spaces (URLs, hashes) cause horizontal overflow | `overflow-wrap: break-word` correctly breaks mid-word when a single word exceeds container width |
| [#532](https://github.com/vercel/satori/issues/532) | Line height calculations are incorrect for multi-line text, producing overlapping lines | Line height computed from font metrics (ascent + descent + lineGap), not approximated |

These bugs mean text-heavy OG images (blog posts, documentation, social cards) produce broken social previews that hurt click-through rates.

## Drop-in Replacement

Replace one import. Everything else stays the same.

```diff
- import { ImageResponse } from '@vercel/og'
+ import { ImageResponse } from '@shipitandpray/pretext-og'
```

That's it. Same JSX syntax, same options, same API.

## Install

```bash
npm install @shipitandpray/pretext-og @napi-rs/canvas
```

`@napi-rs/canvas` is a peer dependency for Node.js environments. It's optional in the browser.

## Usage

### Next.js App Router

```tsx
// app/api/og/route.tsx
import { ImageResponse, loadGoogleFont } from '@shipitandpray/pretext-og'

export const runtime = 'nodejs'

export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') ?? 'Hello World'

const interFont = await loadGoogleFont('Inter', { weight: 700 })

return new ImageResponse(
(



{title}


),
{
width: 1200,
height: 630,
fonts: [{ name: 'Inter', data: interFont, weight: 700 }],
}
)
}
```

### Next.js Pages Router

```tsx
// pages/api/og.tsx
import type { NextApiRequest, NextApiResponse } from 'next'
import { renderToBuffer } from '@shipitandpray/pretext-og'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const title = (req.query.title as string) ?? 'Hello World'

const result = await renderToBuffer(
{
type: 'div',
props: {
style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 60,
width: '100%',
height: '100%',
backgroundColor: '#1a1a2e',
},
children: {
type: 'div',
props: {
style: { fontSize: 64, fontWeight: 700, color: 'white' },
children: title,
},
},
},
},
{ width: 1200, height: 630 }
)

res.setHeader('Content-Type', 'image/png')
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
res.end(result.png)
}
```

### Standalone Text Layout

Use the text layout engine directly without rendering:

```ts
import { layoutText } from '@shipitandpray/pretext-og'

const result = layoutText(
'A very long title that needs accurate wrapping',
{
maxWidth: 500,
font: 'sans-serif',
fontSize: 48,
lineHeight: 1.3,
wordBreak: 'break-word',
}
)

console.log(result.lines) // Array of { text, x, y, width, height }
console.log(result.overflow) // false - text fits!
```

### Custom Font Loading

```ts
import { loadGoogleFont, loadLocalFont } from '@shipitandpray/pretext-og'

// Google Fonts
const inter = await loadGoogleFont('Inter', { weight: 700 })

// Local file
const custom = await loadLocalFont('./fonts/MyFont.ttf')

// Pass to ImageResponse
new ImageResponse(element, {
fonts: [
{ name: 'Inter', data: inter, weight: 700 },
{ name: 'MyFont', data: custom, weight: 400 },
],
})
```

## API

### `ImageResponse`

Drop-in replacement for `@vercel/og`'s `ImageResponse`. Extends the Web `Response` object.

```ts
new ImageResponse(element, options?)
```

### `renderToBuffer(element, options?)`

Renders to a PNG buffer. Returns `{ png: Buffer, width, height, renderTime }`.

### `renderToCanvas(element, options?)`

Lower-level API. Returns `{ canvas, ctx }` for further manipulation.

### `layoutText(text, options)`

Standalone text layout. Returns `{ lines, totalHeight, overflow }`.

### `measureText(text, font)`

Measure the width of a text string.

### `loadGoogleFont(family, options?)`

Fetch a font from Google Fonts. Returns `ArrayBuffer`.

### `loadLocalFont(path)`

Load a local font file. Returns `ArrayBuffer`.

## Performance

| Metric | pretext-og | Satori |
|---|---|---|
| Simple card render | ~40ms | ~50ms |
| Complex layout render | ~120ms | ~150ms |
| Text wrapping accuracy | 100% | ~92% |
| Zero text overflow | Yes | No |
| Memory (1200x630) | ~35MB | ~40MB |

## How It Works

1. **Text measurement**: Uses `@chenglou/pretext` for character-level width measurement with actual font data. Kerning pairs are respected. No cumulative rounding errors.

2. **Flexbox layout**: Pure JS flexbox engine positions elements. Text nodes provide accurate intrinsic sizes from Pretext measurement.

3. **Canvas rendering**: Walks the element tree and draws to `@napi-rs/canvas` (Node.js) or browser Canvas.

4. **PNG export**: Canvas is encoded to PNG and wrapped in a `Response` object.

## Build

```bash
npm run build # tsup: ESM + CJS + types
npm test # vitest
```

## License

MIT