Problem
Since #46 (Open Graph metadata) shipped, poll links unfurl with the correct title + description but no image — og:image was removed because the only available asset (/og-image.png) is a 512x512 square that rendered as a giant awkward card on Telegram/Slack/Discord.
A generic wide (1200x628) logo/brand image would be a small improvement, but the real win is per-poll images that show the scheduling context at a glance.
Proposed
Render a dedicated image per poll, served at something like `GET /p/:did/:rkey/og.png` and referenced from the injected `og:image` tag in the poll route's HTML.
Content ideas (open to iteration):
- Poll title in large type
- Small heatmap strip showing response density across the poll's dates
- Date range + timezone
- Open/Scheduled badge (with the scheduled time when applicable)
- Avails wordmark in a corner
Implementation notes
- Rendering stack: `@vercel/og` (Satori + resvg) is the common serverless choice — JSX-to-PNG, runs without a headless browser. Node `canvas` works but needs native deps that might not be clean on Nixpacks. Evaluate both.
- Caching: generate once, cache the PNG keyed on `(did, rkey, record CID)`. Serve stale while revalidating on poll edits / status changes.
- Response heatmap: the poll record doesn't include responses — either fetch them at image-render time (adds PDS round-trip) or pre-compute a small stat struct.
- Fallback: if image rendering fails for any reason, the poll route should still serve the HTML without og:image — never break the text preview.
- Railway constraints: generation must stay well under the platform's request timeout; cache hit must be very fast (it's on the crawler hot path).
Not this issue
- Static 1200x628 brand image — could be a quick predecessor PR (just design the asset + update template), filed separately if wanted.
- Per-response avatars / Bluesky handles — out of scope, privacy considerations.
Follows up #46.
Problem
Since #46 (Open Graph metadata) shipped, poll links unfurl with the correct title + description but no image —
og:imagewas removed because the only available asset (/og-image.png) is a 512x512 square that rendered as a giant awkward card on Telegram/Slack/Discord.A generic wide (1200x628) logo/brand image would be a small improvement, but the real win is per-poll images that show the scheduling context at a glance.
Proposed
Render a dedicated image per poll, served at something like `GET /p/:did/:rkey/og.png` and referenced from the injected `og:image` tag in the poll route's HTML.
Content ideas (open to iteration):
Implementation notes
Not this issue
Follows up #46.