Skip to content

Comments

feat(www): add OG image generation for blog posts#1120

Open
taras wants to merge 6 commits intov4from
blog-og-thumbnails
Open

feat(www): add OG image generation for blog posts#1120
taras wants to merge 6 commits intov4from
blog-og-thumbnails

Conversation

@taras
Copy link
Member

@taras taras commented Feb 19, 2026

Motivation

Social media platforms (Twitter, LinkedIn, Facebook, etc.) don't support SVG images in Open Graph og:image meta tags. Our blog posts use SVG featured images that render beautifully on the blog but appear broken or missing when shared on social media.

This PR adds build-time PNG generation from SVG featured images so social previews work correctly.

Approach

  • PNG generation script: Uses Resvg WASM to render SVGs to 1200×630 PNGs (standard OG dimensions)
  • Effection structured concurrency: Uses bounded parallelism (4 concurrent) for efficient batch processing
  • Animation stripping: Removes CSS animations from animated SVGs to show "final state" where all content is visible
  • Light mode forcing: Uses cascade override injection (!important rules at end of SVG) to ensure consistent light mode colors regardless of system theme
  • TTF fonts committed: Inter and JetBrains Mono (OFL licensed) since Resvg WASM doesn't support WOFF2

Build & Deploy Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                            GitHub Actions CI                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. Generate OG Images                                                      │
│     ┌──────────────────┐                                                    │
│     │ deno task        │     ┌─────────┐      ┌─────────┐                   │
│     │ generate-og-     │────▶│  SVG    │─────▶│  PNG    │                   │
│     │ images           │     │ (blog)  │      │ (1200×  │                   │
│     └──────────────────┘     └─────────┘      │  630)   │                   │
│            │                                   └─────────┘                   │
│            │ Resvg WASM                             │                        │
│            │ • Strip animations                     │                        │
│            │ • Force light mode                     │                        │
│            │ • Embed TTF fonts                      │                        │
│            ▼                                        ▼                        │
│  2. Start Dev Server                                                        │
│     ┌──────────────────┐                                                    │
│     │ deno task dev    │  serves both SVG and PNG via assetsRoute("blog")   │
│     │ localhost:8000   │                                                    │
│     └──────────────────┘                                                    │
│            │                                                                │
│            ▼                                                                │
│  3. Staticalize                                                             │
│     ┌──────────────────┐     ┌─────────────────────────────────────────┐    │
│     │ staticalize      │────▶│ Crawls sitemap.xml                      │    │
│     │ --site localhost │     │ For each page:                          │    │
│     │ --output built/  │     │   • Downloads HTML                      │    │
│     │ --base effection │     │   • Parses [content] attributes         │    │
│     │   .deno.dev      │     │   • Finds og:image meta tag URL         │    │
│     └──────────────────┘     │   • Downloads referenced PNG ◀────────┐ │    │
│            │                 │   • Rewrites URL to production base   │ │    │
│            │                 └───────────────────────────────────────┼─┘    │
│            │                                                         │      │
│            │                 <meta property="og:image"               │      │
│            │                   content="http://localhost:8000/       │      │
│            │                     blog/.../image.png">────────────────┘      │
│            ▼                                                                │
│  4. Deploy to Deno Deploy                                                   │
│     ┌──────────────────┐                                                    │
│     │ built/           │  Contains HTML + PNG files                         │
│     │ ├── blog/        │  og:image URLs rewritten to                        │
│     │ │   └── post/    │  https://effection.deno.dev/blog/.../image.png     │
│     │ │       └── .png │                                                    │
│     │ └── ...          │                                                    │
│     └──────────────────┘                                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Result

When a blog post is shared on social media, the platform fetches the og:image URL and displays the PNG thumbnail instead of showing a broken image or generic fallback.

Generate PNG thumbnails from SVG featured images for Open Graph meta tags.
Social media platforms don't support SVG for og:image, so we render PNGs
at build time using Resvg WASM.

Key features:
- Strip CSS animations to show final visible state
- Force light mode for consistent social previews
- Cascade override injection for reliable text colors
- Bounded concurrency (4 parallel) using Effection
- TTF fonts committed (Inter, JetBrains Mono) for Resvg compatibility

The PNGs are generated during CI before staticalize runs, and staticalize
automatically discovers them via og:image meta tag content URLs.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 19, 2026

Open in StackBlitz

npm i https://pkg.pr.new/thefrontside/effection@1120

commit: e0bfd7a

The code is self-documenting; inline comments were redundant.
Reduces file from ~230 lines to ~170 lines.
Copy link
Member

@cowboyd cowboyd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd try lazy generation first. It will be a less invasive change, and it means the website will work the same in local context as everywhere (no need to have a separate task at build time)

It also means that if generation isn't working, it will be caught during dev time before CI even runs

@cowboyd
Copy link
Member

cowboyd commented Feb 19, 2026

If there is principle here to extract, it is that what makes staticalize work is that the there is no build, only capture. The server is the build.

Replace build-time PNG generation with on-demand rendering via
Revolution plugin. PNGs are now generated when requested and cached
using the Web Cache API.

Changes:
- Add www/plugins/og-image.ts - intercepts /blog/**/*.png requests
- Use @effectionx/fs for Effection-native file operations
- Remove www/scripts/generate-og-images.ts build script
- Remove generate-og-images task from deno.json
- Remove CI step for pre-generating images

Benefits:
- Server IS the build (cowboyd's principle)
- Same behavior locally and in production
- Issues caught during dev before CI runs
- No separate build step needed
The PNG is generated on-demand by the og-image plugin, so we check
if the source SVG exists instead of the output PNG.
- Use @effectionx/fetch instead of native fetch for WASM download
- Make route detection generic (check if SVG exists instead of /blog/ prefix)
- Make transformSvg stateless using reduce pipeline
- Add documentation comment explaining transformSvg purpose
- Make stripAnimations use local variable instead of reassigning parameter
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