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.
- Host: GitHub
- URL: https://github.com/ShipItAndPray/pretext-og
- Owner: ShipItAndPray
- Created: 2026-03-30T15:06:57.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-02T06:17:05.000Z (3 months ago)
- Last Synced: 2026-04-04T17:14:21.588Z (2 months ago)
- Topics: pretext, text-layout, typescript, typography
- Language: TypeScript
- Homepage: https://shipitandpray.github.io/pretext-og/
- Size: 60.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-pretext - `pretext-og` - style flows. | [live](https://shipitandpray.github.io/pretext-og/) | (Ecosystem Catalog / Graphics, Media, and Canvas Rendering)
README
# @shipitandpray/pretext-og
[](https://shipitandpray.github.io/pretext-og/) [](https://github.com/ShipItAndPray/pretext-og)
> **[View Live Demo](https://shipitandpray.github.io/pretext-og/)**
[](https://www.npmjs.com/package/@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