Skip to content

chore(og): add og generation and backfilll old ones#489

Merged
hari merged 8 commits into
mainfrom
feat/og-image-generation
Jun 7, 2026
Merged

chore(og): add og generation and backfilll old ones#489
hari merged 8 commits into
mainfrom
feat/og-image-generation

Conversation

@sudhashrestha

@sudhashrestha sudhashrestha commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Docs now show per-page Open Graph/Twitter images when available, yielding tailored social previews instead of a single default image.
    • New tooling to generate, preview, and publish branded OG images for docs and components, with animated-GIF support where applicable.
  • Documentation
    • Added a how-to for building, previewing, syncing fonts, dry-run publishing, and migrating per-document social images.

sudhashrestha and others added 6 commits June 6, 2026 20:46
Screenshots each doc's primary Storybook story into a branded 1200x630 card,
uploads to Cloudflare R2 via wrangler, and writes a committed slug->{url,hash}
manifest read at build time. Run locally (pnpm og:build), not in CI.

Keys are content-addressed (og/<cat>/<name>.<v>.<hash>.<ext>) so a changed
component yields a new immutable URL the CDN/scrapers refetch. Still PNG by
default; --animate opts into looping GIFs where motion is detected. Hybrid
template fallback for components that don't capture well as a still. Full runs
prune stale manifest entries; preflight checks ffmpeg/wrangler and the command
exits non-zero on failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Look up the per-component OG image by slug and fall back to the site-wide
image for slugs without one (and for local builds where the manifest is
empty). Also set the OG/Twitter image alt to the doc title.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Social (OG) image" section to the component contributing guide covering
the per-component flow (storybook:build, --only with --dry-run, then upload),
the comma-separated multi-component form, content-hash skipping, and that the
card is a still PNG by default with --animate opting into a looping GIF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render every card into .og-out/og/<key> (the local path mirrors the R2 key)
and write the manifest URLs, but skip the wrangler upload — for a first bulk
backfill where the objects are uploaded to R2 by hand. Sync .og-out/og/ to the
bucket root and the committed manifest already points at the matching keys.
Skips the wrangler auth preflight; documents that objects must be uploaded
before the manifest is deployed (a committed entry is used even if its URL
404s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add content-addressed URLs for all 145 component docs, plus og:migrate and
og:upload scripts to re-key flat uploads and bulk-sync to R2.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sudhashrestha sudhashrestha requested a review from hari June 7, 2026 08:58
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5f72e0b-d70c-4873-b382-425d684cae89

📥 Commits

Reviewing files that changed from the base of the PR and between 4407dc4 and 348fd4d.

📒 Files selected for processing (1)
  • scripts/lib/preview-server.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/lib/preview-server.mjs

📝 Walkthrough

Walkthrough

Adds a full Open Graph image pipeline: deterministic card templates, Playwright rendering (still/GIF), R2 upload with content-addressed keys and manifest tracking, CLI scripts for build/upload/migrate, a preview server and font sync, and per-doc metadata integration in the docs page.

Changes

Open Graph Image Generation System

Layer / File(s) Summary
OG Card Template & Preview
scripts/lib/og-template.mjs, scripts/og-template-preview.mjs
HTML/CSS templates and branded-only generators for deterministic 1200×630 OG cards, seeded SVG patterns, font injection helpers, and a Playwright preview script that renders sample flavors.
Build & Rendering Orchestration
scripts/build-og-images.mjs, scripts/lib/preview-server.mjs, package.json, scripts/sync-og-fonts.mjs
CLI-driven Playwright pipeline: collects docs/stories, computes content hashes, prepares story iframes, captures stills or multi-frame sequences, optionally encodes GIFs via ffmpeg, retries on failure, updates/writes lib/og-manifest.json, runs a local preview server, and adds npm scripts and font-sync support.
R2 Upload & Manifest Management
scripts/upload-og-r2.mjs, lib/og-manifest.json
Uploads staged assets to Cloudflare R2 using wrangler r2 object put with bounded concurrency and cache headers, supports dry-run and migration from flat keys, and maintains a content-addressed manifest mapping doc routes to { url, hash }.
Docs Metadata & Contributor Docs
app/(main)/docs/[[...slug]]/page.tsx, content/docs/contributing/components.mdx
Docs page now imports lib/og-manifest.json and selects per-doc Open Graph/Twitter images during generateMetadata (manifest URL or site fallback). Contributor docs describe the build/preview/upload/migrate workflows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • codse/animata#438: Updates Storybook preview build output (public/preview) which the OG generation pipeline relies on for preview assets/index.

Poem

"I'm a rabbit with a lens so bright,
I render cards by day and night,
Templates seeded, hashes neat,
Playwright snaps each social sheet,
Manifest hums — your docs look right!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references the main objective (OG image generation and backfilling) but contains a typo ('backfilll' instead of 'backfill') that slightly undermines clarity.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/og-image-generation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread scripts/lib/preview-server.mjs Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
scripts/og-template-preview.mjs (1)

46-46: ⚡ Quick win

Expand preview coverage to all supported branded flavors.

The harness currently skips d-h, so regressions in generative variants won’t be visible during design review.

Proposed change
-    for (const flavor of ["a", "b", "c"]) {
+    for (const flavor of ["a", "b", "c", "d", "e", "f", "g", "h"]) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/og-template-preview.mjs` at line 46, The preview harness currently
only iterates flavors ["a","b","c"] in the for loop (for (const flavor of ["a",
"b", "c"])), skipping branded variants d–h; update that loop to cover all
supported branded flavors (e.g., ["a","b","c","d","e","f","g","h"]) or, better,
replace the hardcoded array with the canonical SUPPORTED_FLAVORS (or equivalent
constant) so the preview generates outputs for d through h as well.
scripts/build-og-images.mjs (1)

665-709: 💤 Low value

Worker queue is safe but could be clearer with explicit atomic dequeue.

The queue.shift() inside the async worker loop is safe because JavaScript's event loop ensures the synchronous shift() completes atomically before any await yields. However, this relies on implicit JS semantics that may confuse readers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-og-images.mjs` around lines 665 - 709, Extract the synchronous
dequeue operation into a small helper and use it at the top of the worker loop
to make the atomic pop explicit: add a function like getNext() { return
queue.shift(); } and in the worker replace "const item = queue.shift();" with
"const item = getNext(); if (!item) break;" (keep the rest of the loop using
hashItem, renderWithRetry, contentAddressedKey, uploadToR2, manifest, etc.).
This makes the dequeue intent explicit and avoids readers having to reason about
implicit JS event-loop atomicity.
scripts/upload-og-r2.mjs (1)

30-55: 💤 Low value

Minor inconsistency: uses npx wrangler while build-og-images.mjs resolves to local binary.

build-og-images.mjs checks for node_modules/.bin/wrangler and falls back to wrangler, while this script always uses npx wrangler. Both work, but using the local binary when available (as in the other script) is slightly faster and more deterministic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/upload-og-r2.mjs` around lines 30 - 55, The uploadOne function
currently invokes the wrangler CLI via "npx wrangler" which differs from
build-og-images.mjs's approach of preferring the local node_modules binary;
update uploadOne to resolve and prefer the local binary (same logic as
build-og-images.mjs) before falling back to "wrangler" so pexec is called with
the resolved wrangler path (referencing uploadOne and the pexec call), ensuring
deterministic, faster execution when node_modules/.bin/wrangler exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/lib/og-template.mjs`:
- Around line 34-36: The FONTS constant in scripts/lib/og-template.mjs currently
embeds external Google Fonts links which make OG rendering network-dependent;
replace that by removing the preconnect and stylesheet URLs and instead define
local `@font-face` declarations in FONTS that point to self-hosted font files
checked into the repo (e.g., WOFF/WOFF2 assets you add), update any references
to the Outfit/Young Serif/IBM Plex Sans families so they resolve to those local
faces, and ensure the font files are copied into the build/output path used by
the OG renderer; also remove reliance on external fonts in
scripts/og-template-preview.mjs and scripts/build-og-images.mjs by ensuring
document.fonts.ready will resolve with the self-hosted fonts.

---

Nitpick comments:
In `@scripts/build-og-images.mjs`:
- Around line 665-709: Extract the synchronous dequeue operation into a small
helper and use it at the top of the worker loop to make the atomic pop explicit:
add a function like getNext() { return queue.shift(); } and in the worker
replace "const item = queue.shift();" with "const item = getNext(); if (!item)
break;" (keep the rest of the loop using hashItem, renderWithRetry,
contentAddressedKey, uploadToR2, manifest, etc.). This makes the dequeue intent
explicit and avoids readers having to reason about implicit JS event-loop
atomicity.

In `@scripts/og-template-preview.mjs`:
- Line 46: The preview harness currently only iterates flavors ["a","b","c"] in
the for loop (for (const flavor of ["a", "b", "c"])), skipping branded variants
d–h; update that loop to cover all supported branded flavors (e.g.,
["a","b","c","d","e","f","g","h"]) or, better, replace the hardcoded array with
the canonical SUPPORTED_FLAVORS (or equivalent constant) so the preview
generates outputs for d through h as well.

In `@scripts/upload-og-r2.mjs`:
- Around line 30-55: The uploadOne function currently invokes the wrangler CLI
via "npx wrangler" which differs from build-og-images.mjs's approach of
preferring the local node_modules binary; update uploadOne to resolve and prefer
the local binary (same logic as build-og-images.mjs) before falling back to
"wrangler" so pexec is called with the resolved wrangler path (referencing
uploadOne and the pexec call), ensuring deterministic, faster execution when
node_modules/.bin/wrangler exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af304139-c7af-4968-a819-79390658b68f

📥 Commits

Reviewing files that changed from the base of the PR and between d1c7ffc and eb7dcf8.

📒 Files selected for processing (9)
  • app/(main)/docs/[[...slug]]/page.tsx
  • content/docs/contributing/components.mdx
  • lib/og-manifest.json
  • package.json
  • scripts/build-og-images.mjs
  • scripts/lib/og-template.mjs
  • scripts/lib/preview-server.mjs
  • scripts/og-template-preview.mjs
  • scripts/upload-og-r2.mjs

Comment thread scripts/lib/og-template.mjs Outdated
Resolve CodeQL path traversal in the preview server and serve Outfit,
Young Serif, and IBM Plex Sans from public/og-fonts/ so OG renders no
longer depend on Google Fonts CDN.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 7, 2026

Copy link
Copy Markdown

Deploying animata with  Cloudflare Pages  Cloudflare Pages

Latest commit: 348fd4d
Status: ✅  Deploy successful!
Preview URL: https://8bec29fc.animata.pages.dev
Branch Preview URL: https://feat-og-image-generation.animata.pages.dev

View logs

Comment thread scripts/lib/preview-server.mjs Fixed
Comment thread scripts/lib/preview-server.mjs Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/lib/preview-server.mjs`:
- Around line 47-48: The request URL decoding in the createServer callback uses
decodeURIComponent((req.url || "/").split("?")[0]) which will throw on malformed
percent-escapes; wrap that decode in a try/catch inside the createServer handler
(or in startPreviewServer if that wrapper exists), and on catch fall back to a
safe path (e.g., raw pathname or "/") and/or return a 400 response rather than
letting the exception bubble and crash the preview server; update the reference
to urlPath used later in the handler so it uses the safely-decoded value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19acdffa-4c9e-4c77-82a1-be3fe700ac3e

📥 Commits

Reviewing files that changed from the base of the PR and between eb7dcf8 and 4407dc4.

⛔ Files ignored due to path filters (9)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/og-fonts/ibm-plex-sans-latin-400-normal.woff2 is excluded by !**/*.woff2
  • public/og-fonts/ibm-plex-sans-latin-500-normal.woff2 is excluded by !**/*.woff2
  • public/og-fonts/ibm-plex-sans-latin-600-normal.woff2 is excluded by !**/*.woff2
  • public/og-fonts/outfit-latin-400-normal.woff2 is excluded by !**/*.woff2
  • public/og-fonts/outfit-latin-500-normal.woff2 is excluded by !**/*.woff2
  • public/og-fonts/outfit-latin-600-normal.woff2 is excluded by !**/*.woff2
  • public/og-fonts/outfit-latin-700-normal.woff2 is excluded by !**/*.woff2
  • public/og-fonts/young-serif-latin-400-normal.woff2 is excluded by !**/*.woff2
📒 Files selected for processing (7)
  • content/docs/contributing/components.mdx
  • package.json
  • scripts/build-og-images.mjs
  • scripts/lib/og-template.mjs
  • scripts/lib/preview-server.mjs
  • scripts/og-template-preview.mjs
  • scripts/sync-og-fonts.mjs
✅ Files skipped from review due to trivial changes (2)
  • scripts/sync-og-fonts.mjs
  • content/docs/contributing/components.mdx
🚧 Files skipped from review as they are similar to previous changes (3)
  • scripts/og-template-preview.mjs
  • scripts/lib/og-template.mjs
  • scripts/build-og-images.mjs

Comment thread scripts/lib/preview-server.mjs Outdated
Use URL pathname parsing with 400 on malformed input, path.relative
containment, and realpathSync before serving files from the localhost
OG preview server.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hari hari merged commit 42a2a30 into main Jun 7, 2026
6 checks passed
@hari hari deleted the feat/og-image-generation branch June 7, 2026 17:01
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.

3 participants