feat: OG image generation — workers-og + blog template (POC)#681
feat: OG image generation — workers-og + blog template (POC)#681codercatdev merged 4 commits intodevfrom
Conversation
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
left a comment
There was a problem hiding this comment.
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
titlelike<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
typeparameter controls CSS color values viaTYPE_COLORS[type]— an unknown type falls back toBRAND.typeBlog, which is fine, but the rawtypestring 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
- DRY design: Shared
og-utils.tswith centralized brand tokens, font loading, and HTML generation is clean - Vite plugin: The
rawFontsplugin approach is correct for CF Workers (no filesystem). TheresolveId/loadhooks are properly implemented - SSR externals:
node:buffer,node:path,node:fscorrectly externalized — these are only used at build time by the Vite plugin, not at runtime - Cache headers:
public, max-age=86400, s-maxage=604800(1 day browser, 7 day edge) is appropriate for OG images prerender = false: Correctly set on all endpoints for SSR- TypeScript: Good use of
as constfor brand tokens, proper interface forOgHtmlOptions - Adaptive font sizing:
titleFontSize()with 3 breakpoints (42/48/56px) is a nice touch
🟡 Minor Issues
-
loadFonts()called on every request: Font data is already inlined as module-level constants, butloadFonts()creates new array/objects each call. Consider memoizing or making it a module-level constant. -
(InterBoldData as any).buffer || InterBoldData: The.bufferfallback suggests uncertainty about the data format. With the rawFonts plugin producingUint8Array, the.bufferproperty would give the underlyingArrayBuffer. This works but theas anycast hides potential type issues. -
authorInitials()edge case:author.split(" ").map((n) => n[0])will throw ifauthoris an empty string (sincen[0]on an empty string isundefined). The default value of "CodingCat.dev" prevents this in practice, but it's worth noting. -
workers-ogversion^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>
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 viaHTMLRewriter(no JSX needed). Wraps Satori + resvg-wasm internally.Infrastructure (
astro.config.mjs)rawFontsVite plugin: Reads.ttf/.otfat build time → inlines asUint8Array(CF Workers have no filesystem)assetsInclude: ["**/*.wasm"]: WASM files treated as assetsassetsExclude: ["**/*.ttf", "**/*.otf"]: Fonts handled by rawFonts plugin, not Vitessr.external: Node.js builtins externalized for SSRShared utilities (
src/lib/og-utils.ts)loadFonts()— centralized Inter Bold/Regular font loadingtitleFontSize()— 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 edgeEndpoints
/api/og/blog.pngtitle,author,type/api/og/podcast.pngtitle,author,type,episodeNumber/api/og/course.pngtitle,author,type/api/og/default.pngtitle,subtitleSocialMeta.astroComponentGenerates all social sharing meta tags:
og:title,og:description,og:image,og:type,og:urltwitter:card(summary_large_image),twitter:title,twitter:description,twitter:imagearticle:published_time,article:author(when type=article)<link rel="canonical">Usage:
Files Changed (8 files, +457/-142)
astro.config.mjssrc/assets/fonts/Inter-Bold.ttfsrc/assets/fonts/Inter-Regular.ttfsrc/lib/og-utils.tssrc/pages/api/og/blog.png.tssrc/pages/api/og/podcast.png.tssrc/pages/api/og/course.png.tssrc/pages/api/og/default.png.tssrc/components/SocialMeta.astroBuild Status
✅ Build passes at 18.60s — all 4 endpoints compile cleanly.
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.