feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: branded dynamic OG/social cards via @vercel/og - #1334

Merged
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images
Jun 14, 2026
Merged

feat: branded dynamic OG/social cards via @vercel/og#1334
NiallJoeMaher merged 1 commit into
developfrom
feat/dynamic-og-images

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

Replaces the single Lato/planet OG card with the relaunch design-system social cards. Five Satori-safe templates — main, post, profile, publication, job — served from one /og route and wired into every shareable page from real DB models.

Changes

Core (lib/og/)

  • tokens.ts / fonts.ts / templates.tsx — design tokens, edge Google-font loader (Bricolage / Hanken / JetBrains Mono), and the card components (from the design handoff).
  • app/og/route.tsx — single GET /og handler, dispatches on type. Embeds the white wordmark (public/og/wordmark-white.png) as a cached data URI so Satori never resolves a same-origin <img> mid-render (flaky in dev, a round-trip in prod). Long immutablecache-control; runtime stays edge.
  • lib/og/url.ts — typed builders (one per card) that map our models to the route's exact params, cap tags (2 posts / 3 job·interests), and append v=<updatedAt> so edited records get a fresh card.
  • utils/hue.ts — extracted the deterministic per-string hue (was inline in the source-profile client, now shared) so OG avatars/marks match on-site tints.

Page wiringopenGraph.images + twitter (summary_large_image) on:

  • Static/main: root layout (home + fallback), /about, /advertise, /discussions, /jobs, /tag/[slug], /speakers, /volunteer
  • Dynamic: /[username]/[slug] (article + member-link), /d/[slug] (discussion), /s/[sourceSlug]/[slug] (curated link), /s/[sourceSlug] (publication), /[username] (profile), /jobs/[slug] (job)
  • JSON-LD article image uses the same builder.

Notes / judgement calls

  • Weekly main card exists in the route but no page is wired — the newsletter lives at /letters/[slug] (MDX, no metadata), not a /weekly index. /articles is a 301 redirect, so it's skipped.
  • Curated + member link posts now use the branded link card (with cover thumbnail) instead of the raw coverImage, per the kind=link mapping.
  • Discussion cards omit the author role (the shared ReaderPost type doesn't carry jobTitle; kept the change minimal rather than widening it).
  • Fixed two latent Satori violations in the handoff templates (a wrapper div needing display:flex; width:auto on the wordmark <img>) — required for the cards to render.

Verification

All card types render 1200×630 PNGs; og:image + twitter:image resolve to https://www.codu.co/og?type=… on real pages (checked /about, /jobs). Typecheck + lint clean; structured-data unit tests pass.

🤖 Generated with Claude Code

Replace the single Lato/planet OG card with the relaunch design system:
five Satori-safe templates (main, post, profile, publication, job) served
from one `/og` route, wired into every shareable page from real DB models.
- lib/og/{tokens,fonts,templates}.ts(x): design tokens, Google-font loader
(edge), and the card components. app/og/route.tsx dispatches on `type` and
embeds the white wordmark (public/og/wordmark-white.png) as a cached data
URI so Satori never resolves a same-origin <img> mid-render.
- lib/og/url.ts: typed builders (one per card) that map our models to the
route's params, cap tags, and append a `v=<updatedAt>` cache-buster.
- utils/hue.ts: extract the deterministic per-string hue (was inline in the
source-profile client) so OG avatars/marks match on-site tints.
- Add openGraph.images + twitter (summary_large_image) to home/fallback,
about, advertise, discussions, jobs, tag, speakers, volunteer, and the
dynamic article/discussion/link/profile/publication/job pages. JSON-LD
article image uses the same builder.
- Long immutable cache-control on the route; runtime stays edge.
Verified: all card types render 1200x630 PNGs and og:image resolves on
real pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code ownerJune 14, 2026 15:10
@vercel

vercelBot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
coduBuildingBuildingPreview, CommentJun 14, 2026 3:10pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c569-95ed-4c9d-9651-84e89b292395

📥 Commits

Reviewing files that changed from the base of the PR and between 9635238 and c3e76d9.

⛔ Files ignored due to path filters (1)
  • public/og/wordmark-white.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (22)
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/[username]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • app/(app)/discussions/page.tsx
  • app/(app)/jobs/[slug]/page.tsx
  • app/(app)/jobs/page.tsx
  • app/(app)/s/[sourceSlug]/[slug]/page.tsx
  • app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx
  • app/(app)/s/[sourceSlug]/page.tsx
  • app/(app)/speakers/page.tsx
  • app/(app)/tag/[slug]/page.tsx
  • app/(app)/volunteer/page.tsx
  • app/(marketing)/about/page.tsx
  • app/(marketing)/advertise/page.tsx
  • app/layout.tsx
  • app/og/route.tsx
  • lib/og/fonts.ts
  • lib/og/templates.tsx
  • lib/og/tokens.ts
  • lib/og/url.ts
  • lib/structured-data/schemas/article.ts
  • utils/hue.ts

Walkthrough

Introduces a new lib/og/ module with design tokens, font loading, and Satori-based JSX card templates (MainCard, PostCard, ProfileCard, PublicationCard, JobCard), plus typed URL builders (lib/og/url.ts). The /og edge route is rewritten to dispatch to these templates. All page generateMetadata functions are updated to use the new typed builders instead of inline query strings.

Changes

Dynamic OG Image System

Layer / File(s)Summary
Hue utility and OG design tokens
utils/hue.ts, lib/og/tokens.ts
hueFromString provides deterministic 0–359 hue from any string; lib/og/tokens.ts exports T/FONT color/font constants plus avatarBg, pubBg, initials, and fmtK helpers used by card templates.
Typed OG URL builders
lib/og/url.ts
Adds internal ogUrl serializer (omits null/undefined/empty params), a version cache-busting helper, and five exported typed builders: ogMainImage, ogPostImage, ogProfileImage, ogPublicationImage, ogJobImage.
OG card templates and font loading
lib/og/fonts.ts, lib/og/templates.tsx
lib/og/fonts.ts loads Google Fonts TTFs via spoofed User-Agent for Satori; lib/og/templates.tsx defines MAIN lookup, MainCard, PostCard (cover/no-cover branch), ProfileCard, PublicationCard, JobCard, and the OgImage dispatcher.
OG edge route rewrite
app/og/route.tsx
Rewrites the handler to export runtime/size/contentType, cache fonts and wordmark base64 per-worker, parse type + query params into OgParams, and return ImageResponse(OgImage(params)) with a year-long immutable cache header; Sentry handles errors.
Post/article/discussion metadata consumers
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, app/(app)/s/[sourceSlug]/[slug]/page.tsx, lib/structured-data/schemas/article.ts
Adds hostFromUrl helper; selects jobTitle in user queries; switches all article/discussion/link-post generateMetadata OG images to ogPostImage including tags, read time, cover, and source hostname.
Profile/publication/job metadata consumers
app/(app)/[username]/page.tsx, app/(app)/s/[sourceSlug]/page.tsx, app/(app)/s/[sourceSlug]/_sourceProfileClient.tsx, app/(app)/jobs/[slug]/page.tsx
Profile metadata adds follower count query and calls ogProfileImage; publication metadata switches to ogPublicationImage; _sourceProfileClient replaces local hue helper with shared hueFromString; job detail page gains generateMetadata with ogJobImage and JOB_TYPE_LABEL.
Static and listing page metadata
app/layout.tsx, app/(app)/discussions/page.tsx, app/(app)/jobs/page.tsx, app/(app)/speakers/page.tsx, app/(app)/tag/[slug]/page.tsx, app/(app)/volunteer/page.tsx, app/(marketing)/about/page.tsx, app/(marketing)/advertise/page.tsx
All static and listing pages switch openGraph.images and twitter.images from hardcoded paths to ogMainImage(id) constants; app/layout.tsx also adds a twitter metadata block.

Sequence Diagram(s)

sequenceDiagram
participant PageServer as Page generateMetadata
participant UrlBuilder as lib/og/url.ts builders
participant OgRoute as /og Edge Route
participant FontCache as coduFonts() cache
participant LogoCache as logo() cache
participant OgImage as OgImage dispatcher
participant Satori as ImageResponse/Satori
PageServer->>UrlBuilder: ogPostImage / ogProfileImage / ogJobImage / ogMainImage
UrlBuilder-->>PageServer: /og?type=X&... URL string
Note over PageServer: URL embedded in metadata openGraph/twitter images
rect rgba(100, 149, 237, 0.5)
Note over OgRoute,Satori: At social preview fetch time
OgRoute->>FontCache: await coduFonts()
OgRoute->>LogoCache: await logo(origin)
OgRoute->>OgImage: OgImage(OgParams { type, ...fields })
OgImage-->>OgRoute: React element (MainCard/PostCard/ProfileCard/etc.)
OgRoute->>Satori: new ImageResponse(element, { fonts, size })
Satori-->>OgRoute: PNG bytes
OgRoute-->>PageServer: 200 image/png (immutable 1yr cache)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codu-code/codu#1144: Modifies the same app/og/route.tsx OG image pipeline and profile metadata in app/(app)/[username]/page.tsx that this PR rewrites.
  • codu-code/codu#1324: Introduces member link-post resolution and generateMetadata in app/(app)/[username]/[slug]/page.tsx, which this PR extends with jobTitle and ogPostImage.
  • codu-code/codu#1322: Modifies app/(app)/[username]/[slug]/page.tsx's generateMetadata for post pages, directly overlapping with this PR's OG image switching for the same route.

Poem

🐇 Hop hop, the images dance,
No more hardcoded strings by chance!
A template for post, job, and face,
Each card rendered with Satori's grace.
The rabbit cached fonts in a flash —
Now every preview looks quite smash! 🎨

✨ 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/dynamic-og-images

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.

@NiallJoeMaher
NiallJoeMaher merged commit 0893dcd into developJun 14, 2026
4 of 7 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the feat/dynamic-og-images branch June 14, 2026 15:10
Sign up for freeto 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.

1 participant

@NiallJoeMaher