Skip to content

✨ Add dynamic OG image generation for blog posts#1148

Merged
cowboyd merged 1 commit intov4from
blog-og-images
Apr 7, 2026
Merged

✨ Add dynamic OG image generation for blog posts#1148
cowboyd merged 1 commit intov4from
blog-og-images

Conversation

@cowboyd
Copy link
Copy Markdown
Member

@cowboyd cowboyd commented Apr 7, 2026

Motivation

Blog posts have custom 1200×630 SVG header images designed for social sharing, but every page uses the same static meta-effection.png for og:image and twitter:image meta tags. Since most social platforms don't render SVG, blog posts have no visual identity when shared on Twitter, Slack, Discord, etc.

Approach

SVG-to-PNG conversion route

A new route at /blog/:id/:name.png converts blog post SVG header images to PNG on demand using resvg-wasm. When a request includes w and h query parameters, the route reads the corresponding .svg file, renders it at the requested dimensions, and returns a PNG with long-lived cache headers. CSS animations in the SVGs (which use opacity: 0 as their initial state) are neutralized before rendering so that all elements appear at their final, visible state.

Font loading

resvg-wasm renders SVGs in an isolated environment with no access to system fonts. The blog SVGs reference ui-sans-serif and ui-monospace font stacks that fall back through system fonts no browser would be missing — but resvg has none of them. To produce accurate renders, we fetch Proxima Nova (the site's primary typeface, served via Typekit) and JetBrains Mono (for monospace labels) at boot and register them with resvg as defaultFontFamily and monospaceFamily respectively. This causes resvg to use them as fallbacks when it can't resolve the named fonts in the CSS stack.

2x rendering for social previews

The og:image URL requests a 2400×1260 PNG — twice the standard 1200×630 OG dimensions. Social platforms downscale to fit, which produces noticeably sharper text than rendering at 1x. Since resvg uses grayscale antialiasing without subpixel hinting, the extra resolution compensates for what would otherwise be visibly soft text in the preview cards.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 7, 2026

Open in StackBlitz

npm i https://pkg.pr.new/effection@1148

commit: b3b4e8f

Convert blog post SVG header images to PNG on the fly for social
preview cards. Requests to /blog/:id/:name.png with w and h query
params render the SVG at the specified dimensions using resvg-wasm.
Static PNGs are served directly if they exist.

- Add fonts resource to fetch Proxima Nova and JetBrains Mono at boot
- Add image-store resource to manage resvg wasm and render cache
- Add blog image route for on-demand SVG-to-PNG conversion
- Fix useAbsoluteUrl to preserve query strings
- Make blog post image frontmatter required with zod validation
- Set OG image to 2400x1260 PNG for retina-quality social previews
@taras taras self-requested a review April 7, 2026 17:05
@cowboyd cowboyd merged commit 25c1020 into v4 Apr 7, 2026
17 checks passed
@cowboyd cowboyd deleted the blog-og-images branch April 7, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants