feat: Social media preview with dynamic OG images#34
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdCore Features
og:*andtwitter:*tags via HTMLRewriterMusicRecordingstructured dataFiles Added
social-preview.tsog-image.tsxpublic/fonts/inter-regular.woffsocial-preview.test.tsog-image.test.tsxtest/integration/social-preview.test.tsSecurity
escapeHtml()for user-provided session names/og/*route (same limits as session creation)Audit Findings Addressed
Test plan
🤖 Generated with Claude Code