/* eslint-disable @next/next/no-img-element -- Satori (ImageResponse) only renders raw ; next/image doesn't work inside OG image generation. */ /** * OG card for a public collection (`/c/`). * * Renders a 1200×1200 (square) 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( `/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( ( // Root carries NO padding — absolutely-positioned children measure // from the padding box, so any root padding would inset the full-bleed // cover image and leave the bg colour showing as a strip. Padding lives // on the inner content layer instead.
{hasImage ? ( <>
) : null} {/* Content layer — holds the padding so the bg image stays full-bleed. */}
{/* Eyebrow */}
Collection
{/* Title — the centrepiece */}
48 ? 60 : 76, fontWeight: 700, color: textPrimary, lineHeight: 1.2, marginTop: 28, maxHeight: 560, overflow: "hidden", }} > {title}
{description ? (
{description}
) : null}
{/* Footer: count + branding */}
{count}
prabhupadasays.org
), { width: 1200, height: 1200, 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 }); } }