{"id":49060622,"url":"https://github.com/ShipItAndPray/pretext-og","last_synced_at":"2026-05-06T06:01:40.966Z","repository":{"id":348656802,"uuid":"1196300614","full_name":"ShipItAndPray/pretext-og","owner":"ShipItAndPray","description":"OG image generator fixing Satori text wrapping bugs. Drop-in replacement powered by Pretext.","archived":false,"fork":false,"pushed_at":"2026-04-02T06:17:05.000Z","size":62,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-04T17:14:21.588Z","etag":null,"topics":["pretext","text-layout","typescript","typography"],"latest_commit_sha":null,"homepage":"https://shipitandpray.github.io/pretext-og/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ShipItAndPray.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T15:06:57.000Z","updated_at":"2026-04-02T06:17:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ShipItAndPray/pretext-og","commit_stats":null,"previous_names":["shipitandpray/pretext-og"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ShipItAndPray/pretext-og","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-og","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-og/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-og/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-og/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ShipItAndPray","download_url":"https://codeload.github.com/ShipItAndPray/pretext-og/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-og/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32680890,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-06T02:33:58.958Z","status":"ssl_error","status_checked_at":"2026-05-06T02:33:39.611Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pretext","text-layout","typescript","typography"],"created_at":"2026-04-20T02:00:28.099Z","updated_at":"2026-05-06T06:01:40.950Z","avatar_url":"https://github.com/ShipItAndPray.png","language":"TypeScript","funding_links":[],"categories":["Ecosystem Catalog"],"sub_categories":["Graphics, Media, and Canvas Rendering"],"readme":"# @shipitandpray/pretext-og\n\n[![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)\n\n\u003e **[View Live Demo](https://shipitandpray.github.io/pretext-og/)**\n\n[![npm version](https://img.shields.io/npm/v/@shipitandpray/pretext-og.svg)](https://www.npmjs.com/package/@shipitandpray/pretext-og)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@shipitandpray/pretext-og)](https://bundlephobia.com/result?p=@shipitandpray/pretext-og)\n\n**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).\n\n## The Problem\n\nSatori (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:\n\n| Satori Issue | Bug | pretext-og Fix |\n|---|---|---|\n| [#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 |\n| [#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 |\n| [#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 |\n\nThese bugs mean text-heavy OG images (blog posts, documentation, social cards) produce broken social previews that hurt click-through rates.\n\n## Drop-in Replacement\n\nReplace one import. Everything else stays the same.\n\n```diff\n- import { ImageResponse } from '@vercel/og'\n+ import { ImageResponse } from '@shipitandpray/pretext-og'\n```\n\nThat's it. Same JSX syntax, same options, same API.\n\n## Install\n\n```bash\nnpm install @shipitandpray/pretext-og @napi-rs/canvas\n```\n\n`@napi-rs/canvas` is a peer dependency for Node.js environments. It's optional in the browser.\n\n## Usage\n\n### Next.js App Router\n\n```tsx\n// app/api/og/route.tsx\nimport { ImageResponse, loadGoogleFont } from '@shipitandpray/pretext-og'\n\nexport const runtime = 'nodejs'\n\nexport async function GET(request: Request) {\n  const { searchParams } = new URL(request.url)\n  const title = searchParams.get('title') ?? 'Hello World'\n\n  const interFont = await loadGoogleFont('Inter', { weight: 700 })\n\n  return new ImageResponse(\n    (\n      \u003cdiv style={{\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        padding: '60px',\n        width: '100%',\n        height: '100%',\n        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',\n      }}\u003e\n        \u003cdiv style={{\n          fontSize: 64,\n          fontWeight: 700,\n          color: 'white',\n          lineHeight: 1.2,\n          wordBreak: 'break-word',\n        }}\u003e\n          {title}\n        \u003c/div\u003e\n      \u003c/div\u003e\n    ),\n    {\n      width: 1200,\n      height: 630,\n      fonts: [{ name: 'Inter', data: interFont, weight: 700 }],\n    }\n  )\n}\n```\n\n### Next.js Pages Router\n\n```tsx\n// pages/api/og.tsx\nimport type { NextApiRequest, NextApiResponse } from 'next'\nimport { renderToBuffer } from '@shipitandpray/pretext-og'\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  const title = (req.query.title as string) ?? 'Hello World'\n\n  const result = await renderToBuffer(\n    {\n      type: 'div',\n      props: {\n        style: {\n          display: 'flex',\n          flexDirection: 'column',\n          justifyContent: 'center',\n          padding: 60,\n          width: '100%',\n          height: '100%',\n          backgroundColor: '#1a1a2e',\n        },\n        children: {\n          type: 'div',\n          props: {\n            style: { fontSize: 64, fontWeight: 700, color: 'white' },\n            children: title,\n          },\n        },\n      },\n    },\n    { width: 1200, height: 630 }\n  )\n\n  res.setHeader('Content-Type', 'image/png')\n  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')\n  res.end(result.png)\n}\n```\n\n### Standalone Text Layout\n\nUse the text layout engine directly without rendering:\n\n```ts\nimport { layoutText } from '@shipitandpray/pretext-og'\n\nconst result = layoutText(\n  'A very long title that needs accurate wrapping',\n  {\n    maxWidth: 500,\n    font: 'sans-serif',\n    fontSize: 48,\n    lineHeight: 1.3,\n    wordBreak: 'break-word',\n  }\n)\n\nconsole.log(result.lines)     // Array of { text, x, y, width, height }\nconsole.log(result.overflow)  // false - text fits!\n```\n\n### Custom Font Loading\n\n```ts\nimport { loadGoogleFont, loadLocalFont } from '@shipitandpray/pretext-og'\n\n// Google Fonts\nconst inter = await loadGoogleFont('Inter', { weight: 700 })\n\n// Local file\nconst custom = await loadLocalFont('./fonts/MyFont.ttf')\n\n// Pass to ImageResponse\nnew ImageResponse(element, {\n  fonts: [\n    { name: 'Inter', data: inter, weight: 700 },\n    { name: 'MyFont', data: custom, weight: 400 },\n  ],\n})\n```\n\n## API\n\n### `ImageResponse`\n\nDrop-in replacement for `@vercel/og`'s `ImageResponse`. Extends the Web `Response` object.\n\n```ts\nnew ImageResponse(element, options?)\n```\n\n### `renderToBuffer(element, options?)`\n\nRenders to a PNG buffer. Returns `{ png: Buffer, width, height, renderTime }`.\n\n### `renderToCanvas(element, options?)`\n\nLower-level API. Returns `{ canvas, ctx }` for further manipulation.\n\n### `layoutText(text, options)`\n\nStandalone text layout. Returns `{ lines, totalHeight, overflow }`.\n\n### `measureText(text, font)`\n\nMeasure the width of a text string.\n\n### `loadGoogleFont(family, options?)`\n\nFetch a font from Google Fonts. Returns `ArrayBuffer`.\n\n### `loadLocalFont(path)`\n\nLoad a local font file. Returns `ArrayBuffer`.\n\n## Performance\n\n| Metric | pretext-og | Satori |\n|---|---|---|\n| Simple card render | ~40ms | ~50ms |\n| Complex layout render | ~120ms | ~150ms |\n| Text wrapping accuracy | 100% | ~92% |\n| Zero text overflow | Yes | No |\n| Memory (1200x630) | ~35MB | ~40MB |\n\n## How It Works\n\n1. **Text measurement**: Uses `@chenglou/pretext` for character-level width measurement with actual font data. Kerning pairs are respected. No cumulative rounding errors.\n\n2. **Flexbox layout**: Pure JS flexbox engine positions elements. Text nodes provide accurate intrinsic sizes from Pretext measurement.\n\n3. **Canvas rendering**: Walks the element tree and draws to `@napi-rs/canvas` (Node.js) or browser Canvas.\n\n4. **PNG export**: Canvas is encoded to PNG and wrapped in a `Response` object.\n\n## Build\n\n```bash\nnpm run build    # tsup: ESM + CJS + types\nnpm test         # vitest\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FShipItAndPray%2Fpretext-og","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FShipItAndPray%2Fpretext-og","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FShipItAndPray%2Fpretext-og/lists"}