Skip to content

feat(marketing): add dynamic OG image for changelog pages#1209

Merged
saddlepaddle merged 1 commit into
mainfrom
feat/changelog-opengraph-image
Feb 4, 2026
Merged

feat(marketing): add dynamic OG image for changelog pages#1209
saddlepaddle merged 1 commit into
mainfrom
feat/changelog-opengraph-image

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 4, 2026

Summary

  • Add opengraph-image.tsx for changelog/[slug] that dynamically generates branded 1200x630 OG images
  • Renders changelog title, date, and cover image as a background with gradient overlay
  • Removes manual images overrides from changelog metadata (Next.js auto-discovers the route)
  • Matches the existing blog OG image pattern

Test plan

  • Visit /changelog/2026-02-02-file-explorer-workspace-improvements and inspect the OG image via dev tools or opengraph.xyz
  • Verify cover image shows behind text with readable contrast
  • Verify fallback renders for entries without a cover image

Summary by CodeRabbit

  • New Features
    • Changelog entries now display automatically generated social media preview images featuring the entry title, date, and branding when shared across platforms.

Generate branded OpenGraph images for changelog entries with the post
title, date, and cover image as background, matching the blog OG pattern.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

Adds a Next.js Open Graph image generator for individual changelog entries that dynamically renders OG images based on slug, featuring dark backgrounds, optional cover images, and gradient overlays. Removes manual image metadata from the changelog page template since the dedicated OG generator now handles image generation automatically.

Changes

Cohort / File(s) Summary
Open Graph Image Generator
apps/marketing/src/app/changelog/[slug]/opengraph-image.tsx
New module that generates dynamic OG images for changelog entries. Loads entry data by slug, renders with optional background image, gradient overlay, title, formatted date, and logo. Includes font loading via Next.js ImageResponse and fallback rendering when entry not found.
Changelog Page Metadata
apps/marketing/src/app/changelog/[slug]/page.tsx
Removes conditional image inclusion from openGraph and twitter metadata sections, delegating image generation to the dedicated OG image generator module.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A changelog's cover now springs to life,
Dynamic images, sans the strife!
With fonts and gradients, dark and deep,
Each entry's OG image we'll keep.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding dynamic Open Graph image generation for changelog pages.
Description check ✅ Passed The description covers the main objective and testing strategy but is missing the required template sections (Type of Change, Related Issues).

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/changelog-opengraph-image

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 4, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/marketing/src/app/changelog/`[slug]/opengraph-image.tsx:
- Around line 11-24: The readFileAsDataUri function fails when filePath begins
with "/" because path.join(process.cwd(), "public", filePath) will ignore
"public"; normalize filePath first (e.g., strip a leading slash: const
normalized = filePath.startsWith("/") ? filePath.slice(1) : filePath) and use
that in path.join to build the absolutePath, and in the catch block do not
swallow the error: add a prefixed console.error like
console.error("[opengraph-image/readFileAsDataUri] Error reading", filePath,
err) before returning null (or rethrow if you prefer) so failures are logged
with context.
- Around line 70-74: Derive the cover MIME from the file extension instead of
hardcoding "image/png": inspect entry.image with path.extname (or similar) and
map extensions like .png/.jpg/.jpeg/.gif/.webp to the correct MIME, then pass
that MIME into readFileAsDataUri (the call that currently uses "image/png");
ensure you handle unknown/uppercase extensions by normalizing and fallback to a
sensible default (e.g., "application/octet-stream" or "image/png") and update
the coverImageUri assignment accordingly.
🧹 Nitpick comments (1)
apps/marketing/src/app/changelog/[slug]/opengraph-image.tsx (1)

48-129: Extract repeated style tokens to top‑level constants

There are several repeated colors, sizes, and the gradient string. Centralizing these makes future tweaks safer and avoids inline magic values.

♻️ Example extraction
+const COLOR_BG = "#0a0a0a";
+const COLOR_TEXT = "#ffffff";
+const COLOR_MUTED = "#999999";
+const GRADIENT_OVERLAY =
+	"linear-gradient(to bottom, rgba(10,10,10,0.65) 0%, rgba(10,10,10,0.25) 50%, rgba(10,10,10,0.7) 100%)";
+const CONTENT_PADDING = "48px 64px";
+
 ...
 				style={{
-					background: "#0a0a0a",
+					background: COLOR_BG,
 					width: "100%",
 					height: "100%",
 					display: "flex",
 					alignItems: "center",
 					justifyContent: "center",
-					color: "#ffffff",
+					color: COLOR_TEXT,
 					fontSize: 48,
 					fontFamily: "Inter",
 				}}
 ...
 					background:
-						"linear-gradient(to bottom, rgba(10,10,10,0.65) 0%, rgba(10,10,10,0.25) 50%, rgba(10,10,10,0.7) 100%)",
+						GRADIENT_OVERLAY,
 				}}
 			/>
 ...
 					padding: "48px 64px",

As per coding guidelines, Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic.

Comment on lines +11 to +24
function readFileAsDataUri({
filePath,
mime,
}: {
filePath: string;
mime: string;
}): string | null {
try {
const absolutePath = path.join(process.cwd(), "public", filePath);
const buffer = fs.readFileSync(absolutePath);
return `data:${mime};base64,${buffer.toString("base64")}`;
} catch {
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize public asset paths and log unexpected read failures

If filePath starts with / (common for public assets), path.join() discards the public prefix and the read fails. Also, the catch currently swallows all errors, making failures hard to diagnose.

🛠️ Suggested fix
 function readFileAsDataUri({
 	filePath,
 	mime,
 }: {
 	filePath: string;
 	mime: string;
 }): string | null {
 	try {
-		const absolutePath = path.join(process.cwd(), "public", filePath);
+		const relativePath = filePath.replace(/^\/+/, "");
+		const absolutePath = path.join(process.cwd(), "public", relativePath);
 		const buffer = fs.readFileSync(absolutePath);
 		return `data:${mime};base64,${buffer.toString("base64")}`;
-	} catch {
+	} catch (err) {
+		const error = err as NodeJS.ErrnoException;
+		if (error.code !== "ENOENT") {
+			console.error("[og/readFileAsDataUri] failed to read asset", {
+				filePath,
+				error,
+			});
+		}
 		return null;
 	}
 }

As per coding guidelines, Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly; and Use prefixed console logging with consistent context pattern: [domain/operation] message for entry/exit of significant operations, external API calls, and error conditions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function readFileAsDataUri({
filePath,
mime,
}: {
filePath: string;
mime: string;
}): string | null {
try {
const absolutePath = path.join(process.cwd(), "public", filePath);
const buffer = fs.readFileSync(absolutePath);
return `data:${mime};base64,${buffer.toString("base64")}`;
} catch {
return null;
}
function readFileAsDataUri({
filePath,
mime,
}: {
filePath: string;
mime: string;
}): string | null {
try {
const relativePath = filePath.replace(/^\/+/, "");
const absolutePath = path.join(process.cwd(), "public", relativePath);
const buffer = fs.readFileSync(absolutePath);
return `data:${mime};base64,${buffer.toString("base64")}`;
} catch (err) {
const error = err as NodeJS.ErrnoException;
if (error.code !== "ENOENT") {
console.error("[og/readFileAsDataUri] failed to read asset", {
filePath,
error,
});
}
return null;
}
}
🤖 Prompt for AI Agents
In `@apps/marketing/src/app/changelog/`[slug]/opengraph-image.tsx around lines 11
- 24, The readFileAsDataUri function fails when filePath begins with "/" because
path.join(process.cwd(), "public", filePath) will ignore "public"; normalize
filePath first (e.g., strip a leading slash: const normalized =
filePath.startsWith("/") ? filePath.slice(1) : filePath) and use that in
path.join to build the absolutePath, and in the catch block do not swallow the
error: add a prefixed console.error like
console.error("[opengraph-image/readFileAsDataUri] Error reading", filePath,
err) before returning null (or rethrow if you prefer) so failures are logged
with context.

Comment on lines +70 to +74
const coverImageUri = entry.image
? readFileAsDataUri({
filePath: entry.image,
mime: "image/png",
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Derive the cover MIME type from the file extension

entry.image isn’t guaranteed to be PNG. Hardcoding image/png can cause non‑PNG covers to render incorrectly. Consider a small lookup map based on path.extname().

✅ Example
+const MIME_BY_EXT: Record<string, string> = {
+	".png": "image/png",
+	".jpg": "image/jpeg",
+	".jpeg": "image/jpeg",
+	".webp": "image/webp",
+	".svg": "image/svg+xml",
+};
+
 const coverImageUri = entry.image
 	? readFileAsDataUri({
 			filePath: entry.image,
-			mime: "image/png",
+			mime: MIME_BY_EXT[path.extname(entry.image).toLowerCase()] ?? "image/png",
 		})
 	: null;
🤖 Prompt for AI Agents
In `@apps/marketing/src/app/changelog/`[slug]/opengraph-image.tsx around lines 70
- 74, Derive the cover MIME from the file extension instead of hardcoding
"image/png": inspect entry.image with path.extname (or similar) and map
extensions like .png/.jpg/.jpeg/.gif/.webp to the correct MIME, then pass that
MIME into readFileAsDataUri (the call that currently uses "image/png"); ensure
you handle unknown/uppercase extensions by normalizing and fallback to a
sensible default (e.g., "application/octet-stream" or "image/png") and update
the coverImageUri assignment accordingly.

@saddlepaddle saddlepaddle merged commit 8511d6a into main Feb 4, 2026
12 of 13 checks passed
@Kitenite Kitenite deleted the feat/changelog-opengraph-image branch February 5, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant