Skip to content

feat: Social media preview with dynamic OG images#34

Merged
adewale merged 12 commits intomainfrom
claude/social-media-preview-tDulx
Jan 7, 2026
Merged

feat: Social media preview with dynamic OG images#34
adewale merged 12 commits intomainfrom
claude/social-media-preview-tDulx

Conversation

@adewale
Copy link
Owner

@adewale adewale commented Jan 6, 2026

Summary

Implements dynamic social media previews for session URLs, so when users share Keyboardia sessions on social platforms, they see rich previews with session-specific titles, descriptions, and images.

Based on spec: specs/SOCIAL-MEDIA-PREVIEW.md

Core Features

  • Crawler Detection: Detects Facebook, Twitter, LinkedIn, Discord, Slack, WhatsApp, Telegram bots
  • Dynamic Meta Tags: Session-specific og:* and twitter:* tags via HTMLRewriter
  • JSON-LD: Schema.org MusicRecording structured data
  • Dynamic OG Images: 600×315 PNG generated with Satori showing:
    • Step grid visualization (4 tracks × 16 steps)
    • Session name
    • Track count and tempo
    • Keyboardia branding

Files Added

File Description
social-preview.ts Crawler detection + HTMLRewriter
og-image.tsx Satori image generation
public/fonts/inter-regular.woff Bundled Inter font
social-preview.test.ts 22 unit tests
og-image.test.tsx 14 unit tests
test/integration/social-preview.test.ts Integration tests

Security

  • XSS prevention via escapeHtml() for user-provided session names
  • Rate limiting on /og/* route (same limits as session creation)
  • Session ID format validation before processing

Audit Findings Addressed

Finding Status
HIGH: Missing font bundle ✅ Fixed - bundled inter-regular.woff
HIGH: Missing loadFont ✅ Fixed - implemented with caching
MEDIUM: Rate limiting on /og/* ✅ Fixed
MEDIUM: OG image unit tests ✅ Fixed
MEDIUM: Observability metrics Deferred - lower priority

Test plan

  • TypeScript compiles
  • ESLint passes
  • Unit tests pass (36 tests)
  • E2E tests pass in CI
  • Manual: Share session URL on Discord/Twitter
  • Validate with Facebook Sharing Debugger

🤖 Generated with Claude Code

claude and others added 12 commits January 6, 2026 17:43
Defines Phase 1 (Cloudflare Worker meta tag injection with Schema.org JSON-LD)
and Phase 2 (dynamic OG image generation with Satori/resvg-wasm).

Includes comprehensive testing strategy (unit, integration, E2E) and
research on technology choices (HTMLRewriter, Satori, crawler detection).
- Change OG image dimensions from 1200×630 to 600×315 (4× faster rasterization)
- Merge all implementation phases into a single consolidated phase
- Adjust Satori code for smaller dimensions (scaled padding, fonts, grid cells)
Critical fixes:
- Add escapeHtml() function with XSS prevention for session names
- Add loadFont() function definition with memory caching
- Fix ctx/url scope in handleOGImageRequest with proper signature
- Add try/catch error handling for Satori failures with fallback

Consistency fixes:
- Fix test file paths to match codebase conventions (co-located + test/)
- Add beforeAll session setup in integration tests
- Update file structure section with correct paths

Best practices:
- Add og:image:width/height meta tags for faster rendering
- Add og:site_name meta tag
- Remove invalid PT0M duration from Schema.org (sessions are loops)
- Add clear route placement instructions in Worker integration

Documentation:
- Add HTMLRewriter limitation note about .on() requiring existing elements
- Add Twitter property vs name attribute clarification
- Expand security section with implementation details
Core Implementation:
- Add social-preview.ts with crawler detection and HTMLRewriter transforms
- Add og-image.tsx with Satori image generation (600x315)
- Integrate into Worker fetch handler with rate limiting
- Bundle Inter font for consistent rendering

Features:
- Dynamic og:title, og:description, og:url for each session
- Dynamic twitter:* cards
- JSON-LD MusicRecording structured data
- Dynamic OG image showing step grid visualization
- Caching: 7 days for immutable, 1 hour for mutable sessions

Testing:
- Unit tests for crawler detection (22 tests)
- Unit tests for condenseSteps and XSS escaping (14 tests)
- Integration tests for meta injection and OG images

Security:
- XSS prevention via escapeHtml() for user-provided session names
- Rate limiting on /og/* route (same as session creation)
- Session ID validation before processing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The BASE_URL was hardcoded to keyboardia.dev, causing social previews
to fail on staging.keyboardia.dev. Now derives the base URL from the
request's origin, making it work correctly in all environments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, session-specific meta tags were only injected when the
User-Agent matched known social media crawlers. This meant validation
tools like OpenGraph.xyz, metatags.io, and Schema.org validator would
see the static fallback HTML with generic Keyboardia branding.

Now we inject session-specific meta tags for ALL requests to /s/{uuid}
routes, regardless of User-Agent. This ensures:
- Social media crawlers see correct previews
- Validation tools see correct previews
- View-source shows correct meta tags
- Better SEO for session pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues were causing dynamic OG images to fail silently:

1. ReferenceError: React is not defined
   - The .tsx file uses JSX which transpiles to React.createElement
   - Added missing React import

2. Satori error: div must have explicit display:flex with multiple children
   - Satori/RESVG requires explicit display:flex on all multi-child divs
   - Added display:flex to text-containing divs
   - Changed interpolated string to template literal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sessions with invalid sampleIds (like "drums:kick" instead of "kick")
would load but produce no sound. Now the API validates sampleIds against
the known instrument catalog and returns clear error messages.

Changes:
- Add VALID_SAMPLE_IDS set and isValidSampleId() to sample-constants.ts
- Import and use validation in worker/validation.ts
- Add 9 new tests for sampleId validation
- Fix integration tests using invalid "drums:" prefix

Valid prefixes: (none), sampled:, synth:, tone:, advanced:
Invalid: drums:, invalid:, or any unknown instrument name

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Track name tooltips now display the canonical sampleId (e.g., "sampled:808-kick")
which helps identify issues like invalid prefixes (e.g., "drums:kick" bug).

Updated in:
- TrackRow.tsx: Shows "ID: {sampleId}" and instrument name when renamed
- MixerPanel.tsx: Shows track name, ID, and original instrument name

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 5 new tests verifying preview content matches session data:
  - og:title contains exact session name
  - og:description matches session stats (track count, tempo)
  - JSON-LD name equals session name
  - og:image URL contains session ID
  - twitter:title matches og:title
- Fix invalid sampleIds in live-session.test.ts:
  - hihat-open -> openhat
  - hihat-closed -> hihat
  - synth -> synth:bass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MusicComposition is semantically more accurate because:
- Sessions are interactive compositions, not fixed recordings
- Sessions loop infinitely (no duration property needed)
- No actual audio file exists at the URL

Changes:
- @type: MusicRecording → MusicComposition
- creator (WebApplication) → composer (Organization)
- Added image property pointing to /og/{id}.png
- Added logo property for Organization
- Removed audio/AudioObject (no actual file)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace raw fetch() with SELF.fetch() from cloudflare:test
- This routes requests through the vitest worker pool
- Tests now work in CI without requiring an external server
- Update test to reflect that meta is injected for all browsers
  (not just crawlers) to support validation tools

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@adewale adewale merged commit a41c533 into main Jan 7, 2026
5 checks passed
@adewale adewale deleted the claude/social-media-preview-tDulx branch January 7, 2026 00:19
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