Skip to content

feat: OG image generation — workers-og + blog template (POC)#681

Merged
codercatdev merged 4 commits intodevfrom
feat/og-images
Mar 17, 2026
Merged

feat: OG image generation — workers-og + blog template (POC)#681
codercatdev merged 4 commits intodevfrom
feat/og-images

Conversation

@codercatdev
Copy link
Contributor

@codercatdev codercatdev commented Mar 16, 2026

OG Image Generation — Track D

Dynamic OG image generation on Cloudflare Workers using workers-og (Satori + resvg-wasm).

Architecture

Why workers-og?

Purpose-built for CF Workers — handles WASM bundling that breaks with @vercel/og. HTML input via HTMLRewriter (no JSX needed). Wraps Satori + resvg-wasm internally.

Infrastructure (astro.config.mjs)

  • rawFonts Vite plugin: Reads .ttf/.otf at build time → inlines as Uint8Array (CF Workers have no filesystem)
  • assetsInclude: ["**/*.wasm"]: WASM files treated as assets
  • assetsExclude: ["**/*.ttf", "**/*.otf"]: Fonts handled by rawFonts plugin, not Vite
  • ssr.external: Node.js builtins externalized for SSR

Shared utilities (src/lib/og-utils.ts)

  • Brand tokens matching design system (dark gradient, violet primary, content type colors)
  • loadFonts() — centralized Inter Bold/Regular font loading
  • titleFontSize() — adaptive sizing (56px/48px/42px based on length)
  • generateOgHtml() — full-featured template (logo, type badge, title, author avatar)
  • generateDefaultOgHtml() — simpler template (branding + title + subtitle)
  • OG_CACHE_HEADER — 1 day browser, 7 day edge

Endpoints

Endpoint Params Default Type
/api/og/blog.png title, author, type Blog (blue)
/api/og/podcast.png title, author, type, episodeNumber Podcast (amber)
/api/og/course.png title, author, type Course (emerald)
/api/og/default.png title, subtitle Generic (no badge)

SocialMeta.astro Component

Generates all social sharing meta tags:

  • og:title, og:description, og:image, og:type, og:url
  • twitter:card (summary_large_image), twitter:title, twitter:description, twitter:image
  • article:published_time, article:author (when type=article)
  • <link rel="canonical">

Usage:

<SocialMeta
  title="Building OG Images with Astro"
  description="Learn how to generate dynamic social sharing images"
  ogImage="https://codingcat.dev/api/og/blog.png?title=Building+OG+Images"
  type="article"
  publishedAt="2026-03-16T00:00:00Z"
  author="Alex Patterson"
/>

Files Changed (8 files, +457/-142)

File Change
astro.config.mjs Added rawFonts plugin, WASM/font asset config, SSR externals
src/assets/fonts/Inter-Bold.ttf Inter Bold font (304KB)
src/assets/fonts/Inter-Regular.ttf Inter Regular font (304KB)
src/lib/og-utils.ts Shared OG utilities (293 lines)
src/pages/api/og/blog.png.ts Blog endpoint (refactored: 175→33 lines)
src/pages/api/og/podcast.png.ts Podcast endpoint with episode number
src/pages/api/og/course.png.ts Course endpoint
src/pages/api/og/default.png.ts Generic endpoint
src/components/SocialMeta.astro Social meta tag component

Build Status

✅ Build passes at 18.60s — all 4 endpoints compile cleanly.

⚠️ Runtime validation pending — WASM execution on CF Workers needs deployment to verify. Build compilation ≠ runtime compatibility.

Wiring (Track C)

SocialMeta component will be integrated into page templates during Track C (page redesigns). Each page will construct the appropriate OG image URL and pass it to SocialMeta.

Miriad and others added 2 commits March 16, 2026 22:11
Proof of concept for dynamic OG image generation on CF Workers:

Infrastructure:
- workers-og package (wraps Satori + resvg-wasm for CF Workers)
- rawFonts Vite plugin: inlines .ttf/.otf as Uint8Array at build time
  (CF Workers have no filesystem access)
- astro.config.mjs: added rawFonts plugin, assetsInclude for .wasm,
  assetsExclude for .ttf/.otf, ssr.external for node builtins
- Inter Bold + Regular font files in src/assets/fonts/

Blog template (/api/og/blog.png):
- 1200x630 PNG with dark gradient background
- Brand tokens: violet primary, content type color coding
- Dynamic title sizing (56px/48px/42px based on length)
- Author initials avatar with gradient
- Content type badge (Blog/Podcast/Course/Video)
- Cache headers: 1 day browser, 7 day edge

Usage: /api/og/blog.png?title=My+Post&author=Alex+Patterson&type=Blog

Build verified: 19.94s ✅

Co-authored-by: seniordeveloper <seniordeveloper@miriad.systems>
…SocialMeta component

- Extract brand tokens, TYPE_COLORS, font loading, and HTML generation into src/lib/og-utils.ts
- Refactor blog.png.ts to use shared utilities (DRY)
- Add podcast.png.ts endpoint (amber #f59e0b, optional episodeNumber)
- Add course.png.ts endpoint (emerald #10b981)
- Add default.png.ts endpoint (simpler layout, no author/badge)
- Add SocialMeta.astro component for og:* and twitter:* meta tags
- All endpoints: SSR (prerender=false), workers-og ImageResponse, cache headers
- Adaptive title font size: >60 chars=42px, >40 chars=48px, else 56px

Co-authored-by: seniordeveloper <seniordeveloper@miriad.systems>
# Conflicts:
#	apps/web/src/components/SocialMeta.astro
@codercatdev codercatdev marked this pull request as ready for review March 17, 2026 00:23
Copy link
Contributor Author

@codercatdev codercatdev left a comment

Choose a reason for hiding this comment

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

PR #681 Review — OG Image Generation (Track D)

🔴 BLOCKER: Font Files Are Broken (GitHub 404 HTML Pages)

Both Inter-Bold.ttf and Inter-Regular.ttf are NOT actual font files. They contain GitHub 404 HTML pages (<!DOCTYPE html> with "Page not found · GitHub · GitHub" in the title). The diff shows 1,468 lines of HTML for each "font" file.

The download URL used was likely https://github.com/rsms/inter/raw/master/fonts/desktop/Inter-Bold.otf — which returns a 404 page because the Inter font repo has restructured. The canonical URL is now the Inter releases page.

Impact: The rawFonts Vite plugin will read these HTML files and inline them as Uint8Array. Satori will then fail at runtime when trying to parse HTML as a TrueType font, causing every OG endpoint to crash with a 500 error.

Fix: Download actual Inter font files from the official Inter releases or from Google Fonts. Use the .ttf variants from the variable font package.


🟡 Security: No Input Sanitization on Query Parameters

Query parameters (title, author, type, episodeNumber, subtitle) are interpolated directly into HTML template strings via generateOgHtml() and generateDefaultOgHtml(). While Satori renders to SVG (not a browser DOM), and workers-og uses HTMLRewriter, there's still a defense-in-depth concern:

  • A malicious title like <img src=x onerror=alert(1)> gets embedded directly into the HTML string
  • Satori should ignore script-like elements, but relying on the renderer for sanitization is fragile
  • The type parameter controls CSS color values via TYPE_COLORS[type] — an unknown type falls back to BRAND.typeBlog, which is fine, but the raw type string is also rendered in the badge HTML

Recommendation: Add a simple HTML entity escape utility for all user-provided strings before interpolation. Satori's escape-html dependency is already in the tree — consider using it.


🟢 Architecture & Code Quality — Well Done

  1. DRY design: Shared og-utils.ts with centralized brand tokens, font loading, and HTML generation is clean
  2. Vite plugin: The rawFonts plugin approach is correct for CF Workers (no filesystem). The resolveId/load hooks are properly implemented
  3. SSR externals: node:buffer, node:path, node:fs correctly externalized — these are only used at build time by the Vite plugin, not at runtime
  4. Cache headers: public, max-age=86400, s-maxage=604800 (1 day browser, 7 day edge) is appropriate for OG images
  5. prerender = false: Correctly set on all endpoints for SSR
  6. TypeScript: Good use of as const for brand tokens, proper interface for OgHtmlOptions
  7. Adaptive font sizing: titleFontSize() with 3 breakpoints (42/48/56px) is a nice touch

🟡 Minor Issues

  1. loadFonts() called on every request: Font data is already inlined as module-level constants, but loadFonts() creates new array/objects each call. Consider memoizing or making it a module-level constant.

  2. (InterBoldData as any).buffer || InterBoldData: The .buffer fallback suggests uncertainty about the data format. With the rawFonts plugin producing Uint8Array, the .buffer property would give the underlying ArrayBuffer. This works but the as any cast hides potential type issues.

  3. authorInitials() edge case: author.split(" ").map((n) => n[0]) will throw if author is an empty string (since n[0] on an empty string is undefined). The default value of "CodingCat.dev" prevents this in practice, but it's worth noting.

  4. workers-og version ^0.0.27: This is a pre-1.0 package with a caret range. Minor version bumps could introduce breaking changes. Consider pinning to exact version.

Summary

Category Status
Font files 🔴 BLOCKER — HTML 404 pages, not fonts
Input sanitization 🟡 Should add HTML escaping
Vite plugin 🟢 Correct
CF Workers compat 🟢 Good
Code quality 🟢 Clean, DRY
Caching 🟢 Appropriate
TypeScript 🟢 Good

Verdict: CHANGES NEEDED — The font files must be replaced with actual Inter .ttf files before this can be merged. Everything else is solid work.

Previous font files were GitHub 404 HTML pages (curl silently
downloaded HTML instead of font data from rsms/inter raw URLs).

Downloaded real Inter v4.1 TTF files from official release:
- Inter-Bold.ttf (420KB, TrueType v1.0)
- Inter-Regular.ttf (411KB, TrueType v1.0)

Source: https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip

Build: 23.05s ✅

Co-authored-by: seniordeveloper <seniordeveloper@miriad.systems>
@codercatdev codercatdev merged commit d827b3f into dev Mar 17, 2026
@codercatdev codercatdev deleted the feat/og-images branch March 17, 2026 00:29
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.

1 participant