Open Graph Image Generator
The OG image generator (scripts/generate_og_images.ts) creates 1200×630 social-media preview cards for every rendered HTML page in dist/. Each card is first composed as SVG — title, route path, logo, optional code panel, optional background image — then rasterized to PNG using the globally installed vips binary. The resulting <meta> tags are injected directly into each page's <head>.
bun og:images
This runs as part of bun ssg and can be re-run standalone after config changes without rebuilding the entire site.
Prerequisites
The generator uses vips (libvips command-line) to rasterize SVG to PNG. It is fetched by the installer and lives at the project root as vips (or vips.exe on Windows). No npm package dependency is required — the SVG composition is pure string templating and the rasterization is a single vips copy subprocess.
CLI Options
bun scripts/generate_og_images.ts [options]
| Option | Default | Description |
|---|---|---|
--dist <dir> | ./dist | Rendered site directory |
--site-url <url> | $SITE_URL (required) | Public origin for absolute image URLs |
--help | - | Print usage and exit |
If --site-url is not provided and SITE_URL is not in the environment, the script exits with an error — the <meta property="og:image"> tags need an absolute URL.
Output
For each rendered page in dist/, the generator:
- Writes a PNG to
dist/images/og/{route}.png(e.g.dist/images/og/docs/install.png). - Injects
<meta>tags into the page's<head>, wrapped in<!-- reeweb:og:start -->/<!-- reeweb:og:end -->markers so subsequent runs can update tags without disturbing other<head>content.
dist/
├── images/
│ └── og/
│ ├── index.png ← homepage card
│ ├── docs/
│ │ └── install.png ← /docs/install card
│ └── blog/
│ └── first-post.png ← /blog/first-post card
Injected Meta Tags
<!-- reeweb:og:start -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Installation" />
<meta property="og:url" content="https://example.com/docs/install/" />
<meta property="og:image" content="https://example.com/images/og/docs/install.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<!-- reeweb:og:end -->
The markers enable idempotent re-runs — the generator strips the previous block before inserting the new one.
Card Design
Each card is a 1200×630 SVG with these elements:
| Element | Source | Notes |
|---|---|---|
| Background | config/og_images colour | Solid background_color fill |
| Background image | First <img src="/images/..."> | Desaturated, overlaid at low opacity. Only when present on the page. |
| Logo | config/og_images.logo_path | Tinted with logo_color, also rendered large and faded behind text |
| Label | config/og_images.label | Small uppercase text above the title |
| Page title | <title> tag | Auto-wrapped across up to 4 lines, 28–34 characters per line |
| Route path | Page URL | Rendered as subtle grey text at the bottom |
| Code panel (opt) | First <pre> code block | Dark panel with route comment and code snippet, 3 coloured dots |
Background Image
When a page contains an <img> pointing to /images/..., the first such image is base64-encoded, desaturated, and composited behind the text at reduced opacity (62%). This gives documentation pages with screenshots and blog posts with hero images a distinctive social card without manual per-page configuration.
The background image is skipped when the page has no images under /images/.
Code Panel
When show_code is enabled in config and no background image is present, a dark code panel appears on the right side of the card. It extracts the first <pre> block from the page HTML (up to 90 characters) and renders it as a code snippet with three coloured macOS-style dots. If no <pre> block is found, it falls back to "reeweb" as the snippet text.
The code panel is automatically suppressed when a background image is already present, keeping the card clean.
Title Wrapping
The title is read from each page's <title> tag and automatically word-wrapped across up to 4 lines. The character limit per line adapts to the card layout:
| Layout | Characters per line |
|---|---|
| Background image present | 28 |
| Code panel visible | 19 |
| Plain (no image, no code) | 34 |
Lines that overflow are truncated with ....
Configuration
Card styling is configured in config/og_images.ts:
export const og_images = {
logo_path: "images/logo-reepolee-text.svg",
logo_color: "#ffffff",
brand_color: "#b40000",
background_color: "#0f172a",
label: "Ree Web",
show_code: true,
} as const;
| Field | Type | Description |
|---|---|---|
logo_path | string | Path to the logo SVG, relative to dist/. Must be an SVG file. |
logo_color | string | Hex colour for the logo (replaces currentColor in the SVG). |
brand_color | string | Hex colour for the label text, code-panel dots, and gradient overlay. |
background_color | string | Hex colour for the card background fill. |
label | string | Short uppercase label above the title (e.g. project name or tagline). |
show_code | boolean | When true, renders the code panel on pages without a background image. |
This file is project-owned — copy and adjust it in another ReeWeb project to use its own identity without changing the generator itself.
How It Works
- Collects rendered pages — walks
dist/withBun.Glob("**/index.html"), skipping anything underog/to avoid re-processing generated images. - Reads the logo — loads the SVG from
dist/<logo_path>, replacescurrentColorwithlogo_color, and base64-encodes it as a data URL. - For each page:
- Reads the
<title>and first<pre>block from the rendered HTML. - Resolves the route path from the HTML file's location.
- Scans for a background image under
/images/.
- Reads the
- Composes the SVG — builds the full 1200×630 SVG string with all configured elements.
- Rasterizes with vips — writes the SVG to a temp file, runs
vips copy <svg> <png>, then removes the temp SVG. - Injects OG tags — strips any existing
<!-- reeweb:og:start -->...<!-- reeweb:og:end -->block and inserts the new<meta>tags before</head>.
The rasterization uses vips copy (a single subprocess call per page) rather than a Node/Bun-native SVG renderer, keeping the dependency surface small.
Markers and Idempotency
The <!-- reeweb:og:start --> / <!-- reeweb:og:end --> comment markers ensure the generator can be re-run safely. On each run, the previous block is stripped before a new one is inserted. This means editing config/og_images.ts and running bun og:images again updates every page's tags without leaving stale duplicates.