Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions apps/marketing/src/app/changelog/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import fs from "node:fs";
import path from "node:path";
import { ImageResponse } from "next/og";
import { getChangelogEntry } from "@/lib/changelog";
import { formatChangelogDate } from "@/lib/changelog-utils";

export const alt = "Superset Changelog";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

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;
}
Comment on lines +11 to +24
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.

}

const interBold = fetch(
"https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZhrib2Bg-4.ttf",
).then((res) => res.arrayBuffer());

export default async function Image({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const entry = getChangelogEntry(slug);
const fontData = await interBold;
const logoDataUri = readFileAsDataUri({
filePath: "title.svg",
mime: "image/svg+xml",
});

if (!entry) {
return new ImageResponse(
<div
style={{
background: "#0a0a0a",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#ffffff",
fontSize: 48,
fontFamily: "Inter",
}}
>
Superset Changelog
</div>,
{
...size,
fonts: [
{ name: "Inter", data: fontData, weight: 700, style: "normal" },
],
},
);
}

const coverImageUri = entry.image
? readFileAsDataUri({
filePath: entry.image,
mime: "image/png",
})
Comment on lines +70 to +74
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.

: null;

return new ImageResponse(
<div
style={{
background: "#0a0a0a",
width: "100%",
height: "100%",
display: "flex",
position: "relative",
fontFamily: "Inter",
}}
>
{/* Background cover image */}
{coverImageUri && (
// biome-ignore lint/a11y/useAltText: ImageResponse requires native <img>
// biome-ignore lint/performance/noImgElement: ImageResponse requires native <img>
<img
src={coverImageUri}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "cover",
opacity: 0.7,
}}
/>
)}

{/* Dark gradient overlay for text legibility */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
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%)",
}}
/>

{/* Content */}
<div
style={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: "48px 64px",
}}
>
{/* Title + date */}
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div
style={{
fontSize: 56,
fontWeight: 700,
color: "#ffffff",
lineHeight: 1.2,
maxWidth: "90%",
}}
>
{entry.title}
</div>
<div style={{ fontSize: 24, color: "#999999" }}>
{formatChangelogDate(entry.date)}
</div>
</div>

{/* Bottom: logo left-aligned */}
<div
style={{
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
}}
>
{logoDataUri ? (
// biome-ignore lint/a11y/useAltText: ImageResponse requires native <img>
// biome-ignore lint/performance/noImgElement: ImageResponse requires native <img>
<img src={logoDataUri} height={120} />
) : (
<div
style={{
fontSize: 48,
fontWeight: 700,
color: "#ffffff",
}}
>
Superset
</div>
)}
</div>
</div>
</div>,
{
...size,
fonts: [{ name: "Inter", data: fontData, weight: 700, style: "normal" }],
},
);
}
2 changes: 0 additions & 2 deletions apps/marketing/src/app/changelog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,11 @@ export async function generateMetadata({
url,
siteName: COMPANY.NAME,
publishedTime: entry.date,
...(entry.image && { images: [entry.image] }),
},
twitter: {
card: "summary_large_image",
title: entry.title,
description: entry.description,
...(entry.image && { images: [entry.image] }),
},
};
}
Loading