✨ Add dynamic OG image generation for blog posts#1148
Merged
Conversation
commit: |
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
approved these changes
Apr 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Blog posts have custom 1200×630 SVG header images designed for social sharing, but every page uses the same static
meta-effection.pngforog:imageandtwitter:imagemeta 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.pngconverts blog post SVG header images to PNG on demand using resvg-wasm. When a request includeswandhquery parameters, the route reads the corresponding.svgfile, renders it at the requested dimensions, and returns a PNG with long-lived cache headers. CSS animations in the SVGs (which useopacity: 0as 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-serifandui-monospacefont 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 asdefaultFontFamilyandmonospaceFamilyrespectively. 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:imageURL 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.