Files
spsays-training/app/api/og/collection/[id]/route.tsx
T
2026-06-14 01:29:08 +05:30

216 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @next/next/no-img-element -- Satori (ImageResponse) only
renders raw <img>; next/image doesn't work inside OG image generation. */
/**
* OG card for a public collection (`/c/<id>`).
*
* Renders a 1200×630 image with the collection title as the centrepiece,
* an item-count line, and the cover image dimmed behind it when present
* (flat cream otherwise). Matches the post/profile card aesthetic.
*
* Data comes from the public BFF collection endpoint. The server fetch is
* unauthenticated, so private collections return 403 → we 404 here (a
* private collection never unfurls, which is the correct privacy stance).
*/
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { bff } from "@/lib/bff";
export const runtime = "nodejs";
type CollectionEnvelope = {
collection: {
id: string;
title: string;
description: string | null;
cover_url: string | null;
is_public: boolean;
item_count: number;
};
};
let notoRegularData: Buffer | null = null;
let notoBoldData: Buffer | null = null;
async function loadFonts() {
const fontsDir = join(process.cwd(), "public/fonts/Noto_Sans/static");
if (!notoRegularData) {
notoRegularData = await readFile(join(fontsDir, "NotoSans-Regular.ttf"));
}
if (!notoBoldData) {
notoBoldData = await readFile(join(fontsDir, "NotoSans-Bold.ttf"));
}
return { regular: notoRegularData, bold: notoBoldData };
}
export async function GET(
_request: Request,
context: { params: Promise<{ id: string }> },
) {
try {
const { id } = await context.params;
const data = await bff<CollectionEnvelope>(
`/collections/${encodeURIComponent(id)}`,
{ revalidate: 60 },
).catch(() => null);
const c = data?.collection;
if (!c || !c.is_public) {
return new Response("Collection not found", { status: 404 });
}
const fonts = await loadFonts();
const title = c.title.length > 90 ? c.title.slice(0, 87) + "…" : c.title;
const description = c.description?.trim()
? c.description.length > 160
? c.description.slice(0, 157) + "…"
: c.description
: "";
const count = `${c.item_count} post${c.item_count === 1 ? "" : "s"}`;
const bgImageUrl = c.cover_url;
const hasImage = !!bgImageUrl;
const textPrimary = hasImage ? "#ffffff" : "#2d2418";
const textSecondary = hasImage ? "rgba(255,255,255,0.85)" : "#5a4a3a";
const accent = hasImage ? "#ffd9a8" : "#c47a5a";
return new ImageResponse(
(
<div
style={{
width: 1200,
height: 630,
display: "flex",
flexDirection: "column",
backgroundColor: hasImage ? "#1a1410" : "#faf5f0",
fontFamily: "Noto Sans, sans-serif",
position: "relative",
padding: 80,
}}
>
{hasImage ? (
<>
<img
alt=""
src={bgImageUrl}
width={1200}
height={630}
style={{
position: "absolute",
top: 0,
left: 0,
width: 1200,
height: 630,
objectFit: "cover",
filter: "brightness(0.45)",
}}
/>
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: 1200,
height: 630,
background:
"linear-gradient(to top, rgba(0,0,0,0.75), rgba(0,0,0,0.1))",
}}
/>
</>
) : null}
{/* Eyebrow */}
<div
style={{
display: "flex",
fontSize: 24,
fontWeight: 700,
letterSpacing: 6,
textTransform: "uppercase",
color: accent,
zIndex: 1,
}}
>
Collection
</div>
{/* Title — the centrepiece */}
<div
style={{
display: "flex",
fontSize: title.length > 48 ? 60 : 76,
fontWeight: 700,
color: textPrimary,
lineHeight: 1.15,
marginTop: 24,
maxHeight: 320,
overflow: "hidden",
zIndex: 1,
}}
>
{title}
</div>
{description ? (
<div
style={{
display: "flex",
fontSize: 28,
color: textSecondary,
lineHeight: 1.4,
marginTop: 24,
maxHeight: 120,
overflow: "hidden",
zIndex: 1,
}}
>
{description}
</div>
) : null}
<div style={{ flex: 1, display: "flex" }} />
{/* Footer: count + branding */}
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
zIndex: 1,
}}
>
<div style={{ fontSize: 24, color: textSecondary, display: "flex" }}>
{count}
</div>
<div style={{ fontSize: 22, color: accent, fontWeight: 700 }}>
prabhupadasays.org
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans",
data: fonts.regular,
style: "normal" as const,
weight: 400,
},
{
name: "Noto Sans",
data: fonts.bold,
style: "normal" as const,
weight: 700,
},
],
},
);
} catch (e) {
console.error("Collection OG image generation failed:", e);
return new Response("Failed to generate OG image", { status: 500 });
}
}